JWT 기반 로그아웃 구현 (Spring + Redis)

서규범·2023년 1월 24일
0

팀 프로젝트를 진행하면서, JWT 기반 로그인 기능을 구현한 후에, 로그아웃 기능 또한 구현해야 했다.
여러 로그아웃 구현 방법들을 찾아보다가, 세션 로그인의 경우에는 세션에서 해당 유저 데이터를 삭제하면 되지만, 토큰의 경우에는 이미 발급된 토큰을 무효화 시키거나 조작할 수 없다고 한다. 따라서 주로 요청시에 보낸 토큰을 블랙리스트에 넣어 다음 요청부터는 유효하지 않도록 설정하는 식으로 구현하였다.

Redis 사용이유

  • 유효성을 검증하기 위해 블랙리스트로 토큰을 등록하는 과정에서, 해당 데이터는 일정 시간 이후(토큰 유효 시간)에 자동으로 삭제되어야 한다.
  • 기존 사용하고 있던 Mysql 에는 자동으로 데이터 삭제가 이루어지지 않기 때문에, DB에서 실제 일일이 삭제 작업을 진행해야 했다.
  • 따라서 Redis에서 제공하는 데이터 자동 삭제 기능을 이용하기로 했다.
  • 또한 토큰 데이터의 경우, 사용자 데이터와 같이 영구적으로 저장할 필요가 없어, 하드디스크에 저장하는게 아닌, in memory 방식의 redis를 사용함으로써 속도 측면에서도 효율적이라고 생각했다.


의존성 추가 및 설치

Redis 다운 링크 : https://github.com/microsoftarchive/redis

implementation 'org.springframework.boot:spring-boot-starter-data-redis'

Redis를 설치한 후에, gradle에 위와 같이 의존성을 추가해준다.


구현 코드

RedisConfig.java

@Configuration
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.setConnectionFactory(redisConnectionFactory());
        return redisTemplate;
    }
}
  • 먼저 RedisConfig 클래스를 생성하고, @Configuration을 통해 Bean을 등록해준다.
  • RedisConnectionFactory는 redis와 connection을 생성해 주는 객체이다.
  • RedisTemplate은 redis 서버와 통신을 처리하고, 사용자로 하여금 redis 모듈을 사용하기 쉽도록 다양한 기능을 제공하기 위해 스프링에서 높은 수준의 추상화를 통해서 오퍼레이션을 제공한다.
  • host, port의 경우에는 application.yml에서 설정한 변수를 사용하였다.

MemberController.java
    // 로그아웃
    @PostMapping("/members/logout")
    public ResponseEntity<Void> logout(@RequestHeader("Authorization") String accessToken) {

        memberService.logout(accessToken);

        return new ResponseEntity(HttpStatus.OK);
    }
  • 컨트롤러 단에서 로그아웃 메서드를 실행하는 코드이다
  • 헤더를 통해 받은 토큰을 통해 서비스 계층의 logout 메서드를 실행한다.

MemberService.java

    @Transactional
    public void logout(String accessToken) {
        Long expiration = jwtTokenProvider.getExpiration(accessToken);

        redisTemplate.opsForValue()
                .set(accessToken, "blackList", expiration, TimeUnit.MILLISECONDS);
    }
  • 서비스 계층에서 실행되는 메서드이다.
  • 먼저 프론트 측에서 받은 토큰을 통해 해당 토큰의 유효시간을 꺼내온다.
  • Redis에 토큰의 유효시간 이후에 자동으로 삭제되도록 Key, Value 형식으로 저장한다.

JwtAuthenticationFilter.java

@RequiredArgsConstructor
public class JwtAuthenticationFilter extends GenericFilterBean {

    private final JwtTokenProvider jwtTokenProvider;
    private final RedisTemplate redisTemplate;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
        // 헤더에서 JWT 를 받아옴
        String token = jwtTokenProvider.resolveToken((HttpServletRequest) request);
        // 유효한 토큰인지 확인
        if (token != null && jwtTokenProvider.validateToken(token)) {

            String isBlackList = (String)redisTemplate.opsForValue().get(token);

            // 블랙리스트에 해당 토큰이 존재하지 않을 경우
            if (ObjectUtils.isEmpty(isBlackList)) {

                // 토큰이 유효하면 토큰으로부터 유저 정보를 받아옴
                Authentication authentication = jwtTokenProvider.getAuthentication(token);
                // SecurityContext 에 Authentication 객체를 저장
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }

        }
        chain.doFilter(request, response);
    }
}
  • 토큰의 유효성을 검증하는 필터이다.

    if (ObjectUtils.isEmpty(isBlackList))

  • 기존 토큰 검증 로직 이외에 위와 같이 if 문을 추가하여, 해당 토큰이 로그아웃을 통해 블랙리스트에 등록된 토큰인지를 검사한 후에, 다음 로직을 진행하도록 구현하였다.

https://github.com/Att-ies/backend

전체 소스코드는 위 깃허브에서 확인할 수 있다.

profile
하려 하자

0개의 댓글