Post

Redis로 JWT 토큰 관리하기

📌 JWT 토큰 관리

JWT Token을 관리하려면 어떻게 하는 것이 좋을까? 기존에 JWT를 이용해서 로그인/회원가입 기능을 개선시킨 경험이 있다. Access Token과 Refresh Token을 발급하여 Token의 수명이 다 하면 새 Token을 발급해주면 된다. 나는 이 Token들을 관리하기 위해 Redis를 사용하였다.

RedisConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Configuration
@EnableRedisRepositories
public class RedisConfig {

    @Value("${spring.redis.host}")
    private String host;

    @Value("${spring.redis.port}")
    private int port;

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory(host, port);
    }

    @Bean
    public RedisTemplate<?, ?> redisTemplate() {
        RedisTemplate<?, ?> redisTemplate = new RedisTemplate<>();
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new StringRedisSerializer());
        redisTemplate.setConnectionFactory(redisConnectionFactory());
        redisTemplate.setEnableTransactionSupport(true);
        return redisTemplate;
    }

    @Bean
    public PlatformTransactionManager transactionManager() {
        return new JpaTransactionManager();
    }
}


🥨 로그인 / 토큰 재발급

Refresh Token을 관리하는 Redis Templete을 구현한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Component
@RequiredArgsConstructor
public class RedisDao {

    private final RedisTemplate<String, String> redisTemplate;

    public void setValues(String key, String data) {
        ValueOperations<String, String> values = redisTemplate.opsForValue();
        values.set(key, data);
    }

    public void setValues(String key, String data, Duration duration) {
        ValueOperations<String, String> values = redisTemplate.opsForValue();
        values.set(key, data, duration);
    }

    public String getValues(String key) {
        ValueOperations<String, String> values = redisTemplate.opsForValue();
        return values.get(key);
    }

    public void deleteValues(String key) {
        redisTemplate.delete(key);
    }

redisTemplete은 아래 코드와 같이 사용된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Transactional
public TokenResponse createTokensByLogin(UserResponse userResponse) throws JsonProcessingException {
    Subject atkSubject = Subject.atk(userResponse);
    Subject rtkSubject = Subject.rtk(userResponse);

    String atk = createToken(atkSubject, atkLive);
    String rtk = createToken(rtkSubject, rtkLive);

    redisDao.setValues(userResponse.getUserId(), rtk, Duration.ofMillis(rtkLive));

    return new TokenResponse(atk, rtk);
}

@Transactional
public TokenResponse reissueAtk(UserResponse userResponse) throws JsonProcessingException {
    String rtkInRedis = redisDao.getValues(userResponse.getUserId());
    if (Objects.isNull(rtkInRedis)) throw new OwnPliForbiddenException("인증 정보가 만료되었습니다.");

    Subject atkSubject = Subject.atk(userResponse);
    Subject rtkSubject = Subject.rtk(userResponse);
    String atk = createToken(atkSubject, atkLive);
    String rtk = createToken(rtkSubject, rtkLive);

    redisDao.deleteValues(userResponse.getUserId());
    redisDao.setValues(userResponse.getUserId(), rtk, Duration.ofMillis(rtkLive));
    return new TokenResponse(atk, rtk);
}

로그인 시 userId를 Key값으로, Refresh Token을 Value로 지정하여 Access Token이 만료되어 재발급해야 할 때 userId가 있는지, Refresh Token이 일치하는지 검증할 수 있다.


🍖 로그아웃

그렇다면 로그아웃 시에는 어떻게 해야할까? 나는 RedisTemplete을 하나 더 선언하여 관리해주었다. Key값으로 Access Token을 저장하고 기존에 Refresh Token을 관리하던 redisTemplete에서 userId를 삭제한다. 아래는 로그아웃 시 AccessToken을 저장할 redisBlackListTemplate을 추가한 코드이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@Component
@RequiredArgsConstructor
public class RedisDao {

    private final RedisTemplate<String, String> redisTemplate;
    private final RedisTemplate<String, String> redisBlackListTemplate;

    public void setValues(String key, String data) {
        ValueOperations<String, String> values = redisTemplate.opsForValue();
        values.set(key, data);
    }

    public void setValues(String key, String data, Duration duration) {
        ValueOperations<String, String> values = redisTemplate.opsForValue();
        values.set(key, data, duration);
    }

    public String getValues(String key) {
        ValueOperations<String, String> values = redisTemplate.opsForValue();
        return values.get(key);
    }

    public void deleteValues(String key) {
        redisTemplate.delete(key);
    }

    public void setBlackList(String key, String data) {
        ValueOperations<String, String> values = redisBlackListTemplate.opsForValue();
        values.set(key, data);
    }

    public void setBlackList(String key, String data, Duration duration) {
        ValueOperations<String, String> values = redisBlackListTemplate.opsForValue();
        values.set(key, data, duration);
    }

    public void deleteBlackList(String key) {
        redisBlackListTemplate.delete(key);
    }

    public boolean hasKeyBlackList(String key) {
        return Boolean.TRUE.equals(redisBlackListTemplate.hasKey(key));
    }

}

해당 코드는 아래와 같이 사용하였다.

1
2
3
4
5
6
7
8
9
10
11
@Transactional
public void setExpirationZeroAndDeleteRtk(String accessToken, String userId) {
    redisDao.setBlackList(accessToken.replace("Bearer ", ""), "logout", Duration.ofMillis(getExpiration(accessToken)));
    redisDao.deleteValues(userId);
}

public void isLogoutUserThenThrowException(HttpServletRequest request) {
    if (redisDao.hasKeyBlackList(request.getHeader("Authorization").replace("Bearer ", ""))) {
        throw new OwnPliException("이미 로그아웃된 유저입니다.");
    }
}
This post is licensed under CC BY 4.0 by the author.