로그인을 구현하는데에 JWT 인증방식을 많이 사용하는데 무상태성, 확장성, 성능.. 등등 이점이 많은 인증 방식이다.
하지만 이러한 특성 때문에 토큰이 한번 발급되면 수정이 불가능하다는 보안적인 약점을 가지고 있다. 즉 토큰이 탈취되었거나, 로그아웃시에 특정 토큰을 무력화 시킬 수 없다는 것이다.
이를 해결하고자 Redis를 활용하여 블랙리스트를 구현 하였다.
이번 글에서 Redis 설치, Spring 연동방법 등은 생략한다.
우리 서비스의 기존 JWT 인증 방식의 과정을 살펴보자
JWT 방식의 인증은 위와 같은 과정으로 이루어 졌다.
Access Token의 만료기간을 짧게 설정하고 Refresh Token으로 재발급 하도록 하여 탈취에 대비하였지만 로그아웃 혹은 토큰이 탈취되었을 상황과 같은 즉각적으로 토큰을 무력화 시킬 방법이 없었다.
물론 클라이언트에서 저장한 JWT 를 삭제하는 식으로 로그아웃을 구현할 수 있지만 만료기간이 남아있고 누군가가 탈취하여 악의적인 요청을 보낸다면 막을 수 없을 것이다.
그래서 특정 토큰을 무력화 하기 위해 Redis 를 사용했다.
특정 토큰을 Blacklist에 추가하고, 토큰을 검증하는 과정에서 Blacklist에 존재하는 토큰인지 확인하는 과정을 추가한다.
만약 로그아웃 혹은 특정 토큰이 탈취 당했다는것을 알게되어 해당 토큰을 Blacklist에 추가한 상황이다.
그러면 전체적인 JWT 검증 과정은 다음과 같이 바뀐다.
이렇게 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 값은 아무거나 들어가면 된다.