[Spring] JWT 블랙리스트 with Redis

Jongmyung Choi·2023년 10월 8일
0
post-custom-banner

개요

로그인을 구현하는데에 JWT 인증방식을 많이 사용하는데 무상태성, 확장성, 성능.. 등등 이점이 많은 인증 방식이다.
하지만 이러한 특성 때문에 토큰이 한번 발급되면 수정이 불가능하다는 보안적인 약점을 가지고 있다. 즉 토큰이 탈취되었거나, 로그아웃시에 특정 토큰을 무력화 시킬 수 없다는 것이다.
이를 해결하고자 Redis를 활용하여 블랙리스트를 구현 하였다.

이번 글에서 Redis 설치, Spring 연동방법 등은 생략한다.

기존의 JWT 인증 방식

우리 서비스의 기존 JWT 인증 방식의 과정을 살펴보자

Access Token

  1. 사용자가 인증(OAuth)을 완료하면 JWT(Access Token, Refresh Token) 를 발급해준다
  2. Access Token의 만료기간을 30분, Refresh Token의 만료기간을 2주로 설정하여 탈취에 대비한다.
  3. 클라이언트는 이를 로컬/세션 스토리지에 저장하고 Header에 Access Token을 담아 요청을 한다.
  4. 필터에서 JWT 를 검증하고 유효할 시에 로직을 처리하고 응답한다.

Refresh Token

  1. 3번 과정에서 Access Token이 만료되었을시에 401을 반환하여 Access Token이 만료 되었음을 알린다.
  2. 클라이언트는 Header에 Refresh Token을 담아 Access Token을 재발급하는 요청을 한다.
  3. Refresh Token을 검증하고 Access Token을 재발급 한다.

JWT 방식의 인증은 위와 같은 과정으로 이루어 졌다.
Access Token의 만료기간을 짧게 설정하고 Refresh Token으로 재발급 하도록 하여 탈취에 대비하였지만 로그아웃 혹은 토큰이 탈취되었을 상황과 같은 즉각적으로 토큰을 무력화 시킬 방법이 없었다.

물론 클라이언트에서 저장한 JWT 를 삭제하는 식으로 로그아웃을 구현할 수 있지만 만료기간이 남아있고 누군가가 탈취하여 악의적인 요청을 보낸다면 막을 수 없을 것이다.

그래서 특정 토큰을 무력화 하기 위해 Redis 를 사용했다.

Blacklist

특정 토큰을 Blacklist에 추가하고, 토큰을 검증하는 과정에서 Blacklist에 존재하는 토큰인지 확인하는 과정을 추가한다.

만약 로그아웃 혹은 특정 토큰이 탈취 당했다는것을 알게되어 해당 토큰을 Blacklist에 추가한 상황이다.
그러면 전체적인 JWT 검증 과정은 다음과 같이 바뀐다.

  1. Header 에서 Refresh Token 을 추출한다.
  2. Refresh Token이 존재하면 Access Token이 만료되어 재발급을 요청하는 것이므로 Refresh Token을 검증하여 재발급을 한다.
  3. Refresh Token이 존재하지 않으면 Access Token 을 추출한다.
  4. Access Token 을 검증한다.
  5. Access Token 이 Blacklist 에 추가되어 있는지 확인한다.

이렇게 5번 과정이 추가된다.

스프링 코드를 통해 살펴보자.

구현

@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
		FilterChain filterChain) throws
		ServletException,
		IOException {
		if (isPermitURI(request.getRequestURI(), request.getContextPath())) {
			filterChain.doFilter(request, response);
			return;
		}
		String refreshToken = jwtService.extractRefreshToken(request)
			.filter(jwtService::isTokenValid)
			.orElse(null);

		if (refreshToken != null) {
			jwtService.checkRefreshToken(request, response, refreshToken);
		} else {
			jwtService.checkAccessToken(request, response, filterChain);
		}
	}

1,2,3 의 과정을 필터에서 수행한다.

public void checkAccessToken(HttpServletRequest request, HttpServletResponse response,
		FilterChain filterChain) throws
		ServletException,
		IOException {
		extractAccessToken(request)
			.ifPresentOrElse(accessToken -> {
				if (!isTokenBlacklist(accessToken) && isTokenValid(accessToken)) {
					// ....
				} else {
					throw new AuthException(ErrorCode.ACCESS_TOKEN_INVALID);
				}
			}, () -> {
				throw new AuthException(ErrorCode.ACCESS_TOKEN_NOT_EXIST);
			});
		filterChain.doFilter(request, response);
	}
public boolean isTokenBlacklist(String token) {
		if (redisTemplate.opsForValue().get(token) != null) {
			throw new AuthException(ErrorCode.ACCESS_TOKEN_BLACKLIST);
		}
		return false;
	}
    
public boolean isTokenValid(String token) {
		try {
			JWT.require(Algorithm.HMAC512(secretKey)).build().verify(token);
			return true;
		} catch (Exception e) {
			return false;
		}
	}

4~5 번 과정인 Access Token 을 검증하는 과정을 살펴보면
헤더에서 Access Token을 추출하고 검증하는 과정에서 isTokenBlacklist 메소드를 통해 token이 Redis에 블랙리스트로 등록 되어있는지를 체크한다.

Redis에 블랙리스트로 추가 하는 방법은 간단하다.

public void logout(String accessToken, String userId) {
		redisTemplate.opsForValue()
			.set(accessToken, "BlackList", jwtService.getExpiration(accessToken), TimeUnit.MINUTES);
		redisTemplate.opsForValue().getAndDelete(userId);
	}

저장된 Refresh Token을 삭제하고, Access Token을 블랙리스트로 추가해주면 된다.
블랙리스트로 추가할 때는 토큰의 남은 만료기간만큼만 TTL을 적용하여 저장한다.
key 값은 Access Token이고 value 값은 아무거나 들어가면 된다.

profile
총명한 개발자
post-custom-banner

0개의 댓글