redis를 이용한 Login, Logout

전홍영·2023년 1월 24일
0

Spring

목록 보기
6/21
post-custom-banner

이전 포스트에서 jwt + spring security를 이용하여 로그인을 구현했다. 그러나 로그아웃을 구현하려 하였으나 jwt을 만료시키는 것 외에 따로 방법이 없었다. 그래서 검색해보니 redis를 이용하여 로그아웃을 구현할 수 있었다.

그러나 나는 redis를 한번도 사용해 본 경험이 없었기 때문에 redis에 대해 많이 알지는 못하지만 logout을 구현할 정도로만 공부를 해보았다.

redis란??

레디스 설치 및 application.yml 설정

레디스를 로컬 컴퓨터에 설치하고 application.yml에 설정을 해주었다.

spring:
	redis:
    	host: localhost
        port: 6379

Redis설정 클래스

@RequiredArgsConstructor
@Configuration
@EnableRedisRepositories//Redis Repository 활성화
public class RedisRepositoryConfig {

    //lettuce
    @Value("${spring.redis.host}")
    private String redisHost;//redist host

    @Value("${spring.redis.port}")
    private int redisPort;//redist 포트 번호

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory(redisHost, redisPort);//RedisConnectionFactory를 통해 내장 혹은 외부의 Redis를 연결한다.
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory());//redis와 스프링 연결관리
        //밑에 메소드를 설정하지 않으면 스프링 값들이 redis에 바이트 값으로 저장된다.
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new StringRedisSerializer());
        return redisTemplate;
    }

}

RedisRepositoryConfig 클래스를 스프링 컨테이너에 등록하였다. RdisConnectionFactory 인터페이스를 통해 LettuceConnectionFactory를 생성하여 반환하였고 setKeySerializer, setValueSerializer 설정해주는 이유는 RedisTemplate를 사용할 때 Spring - Redis 간 데이터 직렬화, 역직렬화 시 사용하는 방식이 Jdk 직렬화 방식이기 때문입니다. 동작에는 문제가 없지만 redis-cli을 통해 직접 데이터를 보려고 할 때 알아볼 수 없는 형태로 출력되기 때문에 적용한 설정이다. RedisTemplate가 필요한 클래스에 의존주입을 하여 사용하였다.

로그인 로직

public TokenDTO login(LoginDTO loginDTO) {

        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginDTO.getMemberId(), loginDTO.getMemberPassword());

        Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
        TokenDTO tokenDTO = tokenProvider.createToken(authentication);

        //refresh token Redis에 저장
        redisTemplate.opsForValue().set("RT:" + authentication.getName(), tokenDTO.getRefreshToken(), tokenDTO.getRefreshTokenExpirationTime().getTime(), TimeUnit.MILLISECONDS);

        return tokenDTO;

    }

redis에 키값을 authentication.getName()으로 하고 value로 생성된 JWT Token에서 refresh 토큰을 저장하였다. 왜냐하면 set() 메서드는 key, value 외에 timeout, TimeUnit type의 unit을 인자로 받는다. 따라서 refresh 유효시간 정보를 함께 저장해서 토큰이 만료되었을 때 value 값을 자동으로 삭제하는 기능을 위해 사용했다.

로그인을 하면 위 사진처럼 redis에 id가 키로 저장되었다.

로그아웃 로직

로그아웃을 하기 위해서는 로그인이 되어있어야 하기 때문에 accessToken과 refreshToken 모두 클라이언트가 가지고 있어 로그아웃을 할 때 이 두개의 토큰을 받아야 한다.

public ResponseEntity<?> logout(LogoutDTO logoutDTO) {
        log.info("로그아웃 로직");
        //accessToken 검증
        if (!tokenProvider.validateToken(logoutDTO.getAccessToken())) {
            return new ResponseEntity<>("잘못된 요청입니다.", HttpStatus.BAD_REQUEST);
        }
        //Access Token에서 authentication 가져온다.
        Authentication authentication = tokenProvider.getAuthentication(logoutDTO.getAccessToken());
        //Redis에서 해당 authentication으로 저장된 refresh token이 있을 경우 삭제한다.
        if (redisTemplate.opsForValue().get("RT:" + authentication.getName())!= null) {
            redisTemplate.delete("RT:" + authentication.getName());
        }
        //해당 AccessToken 유효시간 가지고 와서 BlackList로 저장하기
        Long expiration = tokenProvider.getExpiration(logoutDTO.getAccessToken());
        redisTemplate.opsForValue().set(logoutDTO.getAccessToken(), "logout", expiration, TimeUnit.MILLISECONDS);
        return ResponseEntity.ok("로그아웃 되었습니다.");
    }
    
    public Long getExpiration(String accessToken) {
        Date expiration = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(accessToken).getBody().getExpiration();
        long now = (new Date()).getTime();
        return (expiration.getTime() - now);
    }
  1. 클라이언트로부터 받은 accessToken을 검증한다.
  2. accessToken에서 authentication을 가져온다. 즉 유효성 검증을 끝나고 Member의 아이디 정보를 가져온다.
  3. Redis에서 해당 authentication으로 저장된(id값으로 저장된) refreshToken이 있을 경우 삭제한다.
  4. 해당 AccessToken 유효시간 가지고 와서 BlackList로 저장한다.
    여기서 BlackList란 해당 엑세스 토큰을 키 값으로 하고 유효시간을 적용시켜 로그아웃을 확인할 수 있게 redis에 등록한 것이다.
@Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        log.info("인증 시도");
        String token = resolveToken((HttpServletRequest) request);

        if (Strings.hasText(token) && tokenProvider.validateToken(token)) {
            String isLogout = (String) redisTemplate.opsForValue().get(token);
            //redis에 해당 accessToken logout 여부 확인 후 토큰이 유효할 경우 토큰에서 Authentication 객체를 가지고 와서 SecurityContext에 저장
            if (ObjectUtils.isEmpty(isLogout)) {
                Authentication authentication = tokenProvider.getAuthentication(token);
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }
        chain.doFilter(request, response);
    }

로그아웃이 된 토큰으로 다시 리소스에 접근하면 doFilter에서 필터링되어 접근하지 못하게 한다. 블랙리스트로 등록되지 않았다면 기존의 인증로직을 수행한다.

redis를 이용한 로그인 로그아웃

redis란 기술을 cache Server에 많이 이용된다고 어렴풋이 듣기는 해보았지만 실제로 사용해본것은 처음이었다. 캐시로 이용하려고 사용한 것은 아니지만 나름 새로운것을 써본 재밌는 경험이었다. 나중에는 cache 서버로 사용하거나 다른 용도로도 공부해서 써보고 싶다.

참고
Redis란? 레디스의 기본적인 개념 (인메모리 데이터 구조 저장소)

profile
Don't watch the clock; do what it does. Keep going.
post-custom-banner

0개의 댓글