[백업] Redis를 통한 JWT Refresh Token 관리

박솔찬·2022년 6월 12일
1

+ 이 글을 작성하던 당시(2021년 9월 3일) 비교적 적은 스프링부트에 대한 지식을 바탕으로 작성된 게시글입니다.

전체적인 Redis를 통한 JWT Refresh Token 관리 플로우 및 포인트만 확인 후, 코드를 클린하게 수정하여 작성하시길 바랍니다.

보통 세션 관리를 위해 Redis를 사용한다.
JWT의 Refresh Token도 관리하기 위해선 저장소에 저장이 필요하다.
디스크에 저장하여 관리할 수 있으나, 로그인 아웃 등 세션과 비슷하게 작동하고 로그아웃 된 Refresh Token에 대해서 좀 다르지만 블랙리스트 방식처럼 관리가 필요하기 때문에 Redis를 사용해보자.

이번에는 소스코드의 변경이 매우 적다.
이전 포스트와 방식은 동일하나 저장되는 환경이 디스크가 아닌 인메모리이다.
Refresh Token 발급과 Access Token 재발급

토큰 유효시간과 메모리에 존재하는 시간은 다음과 같이 설정하였다.
Access Token = 1m
Resfresh Token = 3m
memory = 3m

포인트

사실, 아래 3가지 포인트만 잘 기억하면 끝이다.

첫 로그인 후, 3분이 지나면 Refresh Token을 사용할 수 없으며 memory에서 제거된다.
로그아웃을 해도 memory에서 제거된다.
Refresh Token이 유효하더라고 memory에 존재하지 않으면 Access Token을 재발급할 수 없다.

코드는 지난 포스트에서 많은 변화가 없기 때문에, 프로젝트 구조와 수정되거나 새로 작성된 코드만 다루었다.

프로젝트 구조

프로젝트 구조
새로 생성된 클래스는 (new)로 명시하였다.

redis
|
|----config
|----|
|----|--AppConfig
|----|--RedisConfig (new)
|----|--SecurityConfig
|----controller
|----|
|----|--UserController
|----|
|----domain
|----|
|----|--User
|----|--UserDTO
|----|--UserRepositoryㅏ바
|----jwt
|----|
|----|--JwtAuthenticationFilter
|----|--JwtTokenProvider
|----|
|----servicer
|----|
|----|--CuntomUserDetailService
|----|--RedisService (new)
|----|--UserService

수정 및 생성 코드

이제 수정 및 생성된 코드를 확인해보자.

RedisConfig와 RedisService

Redis와 연결하기 위한 설정파일이다.
Redis와 연결은 아래 포스트에서 다루었다.
Redis를 SpringBoot 프로젝트에서 사용해보자

새로 추가된 클래스이지만 설명은 위 포스트에서 하였다.

// RedisConfig
@Configuration
@EnableRedisRepositories
public class RedisConfig {
    @Value("${spring.redis.host}")
    private String redisHost;

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

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory(redisHost, redisPort);
    }

    @Bean
    public RedisTemplate<?, ?> redisTemplate() {
        RedisTemplate<byte[], byte[]> redisTemplate = new RedisTemplate<>();
        // 아래 두 라인을 작성하지 않으면, key값이 \xac\xed\x00\x05t\x00\x03sol 이렇게 조회된다.
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new StringRedisSerializer());
        redisTemplate.setConnectionFactory(redisConnectionFactory());
        return redisTemplate;
    }
}
// RedisSerivce
@Service
@RequiredArgsConstructor
public class RedisService {

    private final RedisTemplate redisTemplate;

    // 키-벨류 설정
    public void setValues(String token, String email){
        ValueOperations<String, String> values = redisTemplate.opsForValue();
//        values.set(name, age);
        values.set(token, email, Duration.ofMinutes(3));  // 3분 뒤 메모리에서 삭제된다.
    }

    // 키값으로 벨류 가져오기
    public String getValues(String token){
        ValueOperations<String, String> values = redisTemplate.opsForValue();
        return values.get(token);
    }

    // 키-벨류 삭제
    public void delValues(String token) {
        redisTemplate.delete(token.substring(7));
    }
}

UserController

TokenRepository를 통해 save하던 로직을 RedisService를 통해 저장하는 방식으로 바뀌었다.

추가로 로그아웃에 대한 메서드가 추가되었다.
"/api/logout"으로 URI를 설정한 이유는 "/logout"은 시큐리티 필터에 걸리게 되기 때문이다. 이거 설정 귀찮아서..

@RestController
@RequiredArgsConstructor
public class UserController {

    private final JwtTokenProvider jwtTokenProvider;
    private final UserService userService;
    private final RedisService redisService;
    ////private final TokenRepository tokenRepository;

    // 회원가입
    @PostMapping("/join")
    public ResponseEntity join(@RequestBody UserDTO user) {
        Integer result = userService.join(user);
        return result != null ?
                ResponseEntity.ok().body("회원가입을 축하합니다!") :
                ResponseEntity.badRequest().build();
    }

    // 로그인
    @PostMapping("/login")
    public ResponseEntity login(@RequestBody UserDTO user, HttpServletResponse response) {
        // 유저 존재 확인
        User member = userService.findUser(user);
        // 비밀번호 체크
        userService.checkPassword(member, user);
        // 어세스, 리프레시 토큰 발급 및 헤더 설정
        String accessToken = jwtTokenProvider.createAccessToken(member.getEmail(), member.getRoles());
        String refreshToken = jwtTokenProvider.createRefreshToken(member.getEmail(), member.getRoles());
        jwtTokenProvider.setHeaderAccessToken(response, accessToken);
        jwtTokenProvider.setHeaderRefreshToken(response, refreshToken);

        // Redis 인메모리에 리프레시 토큰 저장
        redisService.setValues(refreshToken, member.getEmail());
        // 리프레시 토큰 저장소에 저장
        ////tokenRepository.save(new RefreshToken(refreshToken));

        return ResponseEntity.ok().body("로그인 성공!");
    }

    // 로그아웃
    @GetMapping("/api/logout")
    public ResponseEntity logout(HttpServletRequest request) {
        redisService.delValues(request.getHeader("refreshToken"));
        return ResponseEntity.ok().body("로그아웃 성공!");
    }

    // JWT 인증 요청 테스트
    @GetMapping("/test")
    public String test(HttpServletRequest request) {
        return "Hello, User?";
    }

}

JwtTokenProvider

여기서도 TokenRepository를 통해 진행되던 로직만 수정되었다.

저장소에서 Refresh Token 존재유무 확인 메서드만 수정되었다.

   // RefreshToken 존재유무 확인
    public boolean existsRefreshToken(String refreshToken) {
        return redisService.getValues(refreshToken) != null;
       //// return tokenRepository.existsByRefreshToken(refreshToken);
    }

테스트

이제 회원가입 후 로그인을 하고나면 redis 인메모리에 Refresh Token이 존재함을 조회해 볼 수 있다.

Access Token가 만료되면 Refresh Token의 유효성과 Redis 인메모리에 Refresh Token 존재유무를 확인한 후 재발급을 진행한다.

3분이 지난 후 다시 조회를 해보면 키-벨류가 제거된 것을 확인할 수 있다.

혹은 로그아웃을 진행하면, 즉시 키-벨류가 제거된다.


본 포스트는 공부를 하면서 알게된 내용으로 작성됩니다.
혹시나 잘못된 내용이 포함되어 있다면 언제든 피드백 감사하겠습니다!

profile
Why? How? What?

1개의 댓글

comment-user-thumbnail
2023년 3월 24일

혹시 소스파일도 제공해주시나요?

답글 달기