ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Spring boot] Spring Security + JWT + Redis (3/3)
    프로젝트/Share Your Trip 2024. 2. 7. 22:08
    이번 포스팅에서는 유효한 RefreshToken을 활용하여 만료된 AccessToken을 갱신하고 MySQL에 RefreshToken을 저장하는 것이 아닌 Redis에 저장하여 효율적으로 토큰을 관리해보려고 한다. 또한, MySQL에서 refreshToken을 조회했을 때와 Redis를 통해서 refreshToken을 조회했을 때의 성능 차이를 비교해보려고 한다.

    MySQL에서 RefreshToken 관리

    @Override
    public UserDto.UserInfoResponseDTO login(final UserDto.LoginRequestDTO requestDTO) {
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(requestDTO.getUserId(), requestDTO.getUserPassword());
    
        Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
    
        User user = userMapper.findById(requestDTO.getUserId())
                .orElseThrow(() -> new RestApiException(CustomResponseCode.USER_NOT_FOUND));
    
        JwtToken jwtToken = jwtTokenProvider.generateToken(authentication);
    	
        userMapper.updateRefreshToken(jwtToken.getRefreshToken());
    
        return UserDto.UserInfoResponseDTO.builder()
                .userId(user.getUserId())
                .userName(user.getUserName())
                .joinDate(user.getJoinDate())
                .profile(user.getProfile())
                .role(user.getRole())
                .email(user.getEmail())
                .accessToken(jwtToken.getAccessToken())
                .refreshToken(jwtToken.getRefreshToken())
                .build();
    }
    • 사용자는 만료된 AccessToken을 보내면 서버에서는 401에러를 내려줄 것이다. 401 에러를 받은 사용자는 가지고 있는 RefreshToken을 서버로 전송하여 새로운 AccessToken 발급을 요청한다.
    • regenerateToken() 에서 getAuthentication()을 통해 토큰을 검증하고 generateToken에 사용될 Authentication을 생성한다.
    • 토큰에서 추출한 사용자 ID를 활용하여 Login 할 때 저장한 MySQL에서의 RefreshToken을 조회한다. 사용자가 보낸 RefreshToken과 MySQL에 저장된 값을 비교하여 유효성을 검증하게 된다.
    • 이후, 새로운 토큰을 발급하고 RefreshToken을 MySQL에 update 하여 새로운 값으로 갱신시킨다.
    • 사용자에게 새로운 토큰들을 반환하며 응답을 마치게 된다.
    @Override
    public JwtToken regenerateToken(UserDto.RegenerateTokenDto requestDto) {
        Authentication authentication = jwtTokenProvider.getAuthentication(requestDto.getRefreshToken());
    
        String refreshToken = userMapper.findById(authentication.getName())
               .orElseThrow(() -> new RestApiException(CustomResponseCode.USER_NOT_FOUND)).getRefreshToken();
            
        if(!requestDto.getRefreshToken().equals(refreshToken)) {
           throw new RestApiException(CommonResponseCode.INVALID_PARAMETER);
        }
    
        JwtToken jwtToken = jwtTokenProvider.generateToken(authentication);
    
        userMapper.updateRefreshToken(jwtToken.getRefreshToken());
        return jwtToken;
    }

    MySQL 성능 테스트

    Redis에서 RefreshToken 관리

    • 사용자가 Login 할 때, Redis에 만료 시간을 지정하여 RefreshToken을 저장한다.
    • 시간이 만료되면 자동으로 소멸되게 된다.
    @Override
    public UserDto.UserInfoResponseDTO login(final UserDto.LoginRequestDTO requestDTO) {
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(requestDTO.getUserId(), requestDTO.getUserPassword());
    
        Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
    
        User user = userMapper.findById(requestDTO.getUserId())
                .orElseThrow(() -> new RestApiException(CustomResponseCode.USER_NOT_FOUND));
    
        JwtToken jwtToken = jwtTokenProvider.generateToken(authentication);
    
        redisTemplate.opsForValue().set(
                user.getUserId(),
                jwtToken.getRefreshToken(),
                refreshTokenExpiresIn,
                TimeUnit.MILLISECONDS
        );
    
        return UserDto.UserInfoResponseDTO.builder()
                .userId(user.getUserId())
                .userName(user.getUserName())
                .joinDate(user.getJoinDate())
                .profile(user.getProfile())
                .role(user.getRole())
                .email(user.getEmail())
                .accessToken(jwtToken.getAccessToken())
                .refreshToken(jwtToken.getRefreshToken())
                .build();
    }
    • 마찬가지로 Redis에 접근하여 Login에서 저장한 RefreshToken을 가져오고 클라이언트가 전송한 RefreshToken과 비교하여 토큰을 다시 한 번 검증한다.
    • 유효한 토큰일 경우 새로운 AccessToken과 RefreshToken을 생성하고 RefreshToken의 경우 Redis에서 업데이트 해준다.
    • 생성된 토큰들을 사용자에게 반환하며 응답을 마치게 된다.
    @Override
    public JwtToken regenerateToken(UserDto.RegenerateTokenDto requestDto) {
        Authentication authentication = jwtTokenProvider.getAuthentication(requestDto.getRefreshToken());
    
        // Redis에서 refresh token 값을 가져온다.
        String refreshToken = redisTemplate.opsForValue().get(authentication.getName());
    
        if(!requestDto.getRefreshToken().equals(refreshToken)) {
            throw new RestApiException(CommonResponseCode.INVALID_PARAMETER);
        }
    
        JwtToken jwtToken = jwtTokenProvider.generateToken(authentication);
    
        redisTemplate.opsForValue().set(
                authentication.getName(),
                jwtToken.getRefreshToken(),
                refreshTokenExpiresIn,
                TimeUnit.MILLISECONDS
        );
        return jwtToken;
    }

    Redis 성능 테스트

    MySQL을 활용하여 RefreshToken을 처리한 결과 55ms의 응답 시간이 걸린 것을 확인할 수 있다. Redis의 경우 16ms의 응답 시간을 나타내며 디스크 기반인 MySQL에서 관리할 때보다 인메모리 방식인 Redis에서 관리할 때 효율적으로 처리할 수 있는 것을 확인할 수 있었다.
Designed by Tistory.