spring security jwt 사용하기 ver.3 (+redis)

박도영·2022년 3월 14일
1

security+jwt+redis

목록 보기
3/3

전편
깃허브 전체코드

이번에는 전편에서 h2 저장소 대신 redis를 사용해 보겠습니다! 실은 어차피 둘 다 인메모리 저장소이기 때문에 별 차이는 없을거라 생각합니다. 하지만 필자가 redis를 써보고 싶기도 하고, 보통은 디스크에 저장하는 데이터베이스보다 빠른 성능을 기대할 수 있습니다.

@Configuration
@EnableRedisRepositories
public class RedisConfig {

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

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

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

    @Bean
    public RedisTemplate<?, ?> redisTemplate() {
        RedisTemplate<byte[], byte[]> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory());
        return redisTemplate;
    }
}

먼저 Redis를 세팅해줍니다. Redis connector는 두 종류가 있는데 Lettece와 jedis입니다. 공식문서를 보면 Lettece는 Netty-based 오픈 소스라고 설명되어 있습니다. jedis는 community-based 라고 되어있는데 무슨 말인지 잘 모르겠네요. 필자는 Lettece를 선택했는데 이유는 여기를 보고 참고했기 때문입니다 하하하;

public interface RefreshRedisRepository extends CrudRepository<RefreshRedisToken, String> {

}

redis를 쉽게 사용하고 hash 타입의 데이터를 저장하기 위해 repository 방식을 사용했습니다. 마치 jpa를 사용하는 것과 비슷한 방식이라서 금방 적응했던것 같습니다.


    @Transactional(readOnly = true)
    public TokenResponseDto reissueAccessToken(String token) {

        //token 앞에 "Bearer-" 제거
        String resolveToken = resolveToken(token);

        //토큰 검증 메서드
        //실패시 jwtTokenProvider.validateToken(resolveToken) 에서 exception을 리턴함
        jwtTokenProvider.validateToken(resolveToken);

        Authentication authentication = jwtTokenProvider.getAuthentication(resolveToken);
        // 디비에 있는게 맞는지 확인
        RefreshRedisToken refreshRedisToken = refreshRedisRepository.findById(authentication.getName()).get();

        // 토큰이 같은지 확인
        if(!resolveToken.equals(refreshRedisToken.getToken())){
            throw new RuntimeException("not equals refresh token");
        }

        // 재발행해서 저장
        String newToken = jwtTokenProvider.createRefreshToken(authentication);
        RefreshRedisToken newRedisToken = RefreshRedisToken.createToken(authentication.getName(), newToken);
        refreshRedisRepository.save(newRedisToken);

        // accessToken과 refreshToken 모두 재발행
        return TokenResponseDto.builder()
                .accessToken("Bearer-"+jwtTokenProvider.createAccessToken(authentication))
                .refreshToken("Bearer-"+newToken)
                .build();
    }

코드는 h2를 사용했을때와 유사합니다. 특이한 점은 redis를 사용하므로 @Transactional(readOnly = true)를 걸어도 잘 동작한다는 점과 redis는 key값으로 save 했을 경우 값이 이미 존재하면 덮어쓰기 하고 없으면 생성하는 특징이 있습니다.

@SpringBootTest
class RefreshRedisRepositoryTest {

    @Autowired
    private RefreshRedisRepository refreshRedisRepository;

    @AfterEach
    public void tearDown() throws Exception {
        refreshRedisRepository.deleteAll();
    }

    @Test
    void 기본_등록_조회기능() {
        String id = "dyparkkk";
        RefreshRedisToken token = RefreshRedisToken.builder()
                .userId(id)
                .token("token")
                .build();

        // when
        refreshRedisRepository.save(token);

        RefreshRedisToken findToken = refreshRedisRepository.findById(id).get();
        assertThat(findToken.getToken()).isEqualTo("token");
    }

    @Test
    void 수정기능() {
        String id = "dyparkkk";
        refreshRedisRepository.save(RefreshRedisToken.builder()
               .userId(id)
               .token("token")
               .build());

        //when
        RefreshRedisToken findToken = refreshRedisRepository.findById(id).get();
        findToken.reissue("new_token");
        refreshRedisRepository.save(findToken);

        //then
        RefreshRedisToken refreshToken = refreshRedisRepository.findById(id).get();
        assertThat(refreshToken.getToken()).isEqualTo("new_token");
    }

}

Redis를 처음 써보기 때문에 간단한 테스트 코드를 작성하고 잘 되는지 테스트 해봤습니다.

잘 동작하는군요 !

refresh token과 access token을 재발급 받아봤습니다. 특이한 것은 h2가 redis보다 조금 더 빠르다는 것입니다.

h2 저장소 사용

5번정도 테스트 해봤는데 h2가 약간 더 빨랐습니다. 약 3ms정도...
물론 유의미한 속도차이는 아니라고 생각하고 오차범위 일수도 있습니다만, jpa가 최적화가 정말 잘 되어 있다고 짐작해 볼 수 있을것 같습니다.

정리

Redis를 사용해본 이유는 Redis가 강력한 자원이라고 생각했기 때문이다. 파레토법칙을 가정하면 캐시 기능이 많은 성능 문제를 해결 할 수 있다. 또한 Redis는 다양한 데이터 구조를 지원하며, single thread라는 특징으로 트랜잭션 충돌을 걱정하지 않아도 된다. 정말 만능한 아키택처가 아닐 수 없다. (물론 그런 것은 없다)

다음에는 캐시기능을 중점적으로 Redis를 사용해면 좋을 것 같다.

참고자료 :
https://docs.spring.io/spring-data/redis/docs/current/reference/html/#redis.repositories
https://wildeveloperetrain.tistory.com/59
https://bcp0109.tistory.com/328
https://www.javainuse.com/webseries/spring-security-jwt/chap7

profile
좋은 개발자란?

0개의 댓글