https://wonsjung.tistory.com/453
위 게시글은 지난 게시글로 Spring Security로 JWT를 발급하는 과정까지만 작성했었다.
오늘 작성하는 현재의 글은 Access Token의 만료 시간이 다 되었을 경우 Refresh Token이 유효한지 검사하고 유효하다면 Access Token을 재발급하는 과정이다. 또한, 전 게시글은 Access Token과 Refresh Token을 Http 헤더에 담아 전송하였는데 이번에는 쿠키에 담아서 전송한다.
위 게시글에서 쿠키에 담는 이유를 알 수 있으며, 보안상의 이유 + 쿠키는 별도로 헤더에 담지 않아도 요청을 보낼 때 자동으로 담아서 보내지기 때문에 매 번 서버로의 요청에 담아야하는 토큰과 성격이 잘 맞기 때문이다.
마지막으로, Access Token은 유효시간이 짧아 재발급 과정이 진행하는데 Refresh Token은 유효시간을 넉넉히 두고 유효시간이 끝났을 경우 만료시켜야한다. 이를 휘발성을 가진 데이터 베이스 Redis에 저장해두고 관리한다.
정리하자면, 전 게시글에서 추가되는 내용은
- Refresh 토큰을 이용한 Access Token 재발급
- Access Token + Refresh Token 모두 쿠키에 담아 전송
- 휘발성 데이터 베이스 Redis로 Refresh Token 관리하기
정도가 될 것이다.
1. 리팩토링 코드 과정
[ 0. Cookie 생성을 위한 Utils 작성 ]
@Component
public class CookieUtils {
public Cookie createCookie(String cookieName, String value){
Cookie cookie = new Cookie(cookieName, value);
cookie.setHttpOnly(true);
cookie.setMaxAge((int)TokenUtils.RefreshTokenValidTime);
cookie.setPath("/");
return cookie;
}
public Cookie getCookie(HttpServletRequest request, String cookieName){
Cookie[] cookies = request.getCookies();
if(cookies==null) return null;
for (Cookie cookie : cookies) {
if(cookie.getName().equals(cookieName)){
return cookie;
}
}
return null;
}
}
[ 1. 로그인이 성공하여 JWT를 발급 한 후 이를 Cookie에 담아서 클라이언트에게 보내준다. ]
@RequiredArgsConstructor
public class CustomFormLoginSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
private final TokenUtils tokenUtils;
private final RedisUtils redisUtils;
private final CookieUtils cookieUtils;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
final Member member = ((MyUserDetails) authentication.getPrincipal()).getMember();
final Token token = tokenUtils.generateToken(member.getEmail(), UserRole.USER.getKey());
Cookie accessToken = cookieUtils.createCookie(AuthConstants.AUTH_HEADER, token.getAccessToken());
Cookie refreshToken = cookieUtils.createCookie(AuthConstants.REFRESH_HEADER, token.getRefreshToken());
redisUtils.setDataExpire(token.getRefreshToken(), member.getEmail(), tokenUtils.getRefreshTokenValidTime());
response.addCookie(accessToken);
response.addCookie(refreshToken);
String tkn = new ObjectMapper().writeValueAsString(token);
log.info("Token = {}", tkn);
}
}
JWT를 생성하여 클라이언트에게 전송한다는 것은 로그인을 성공적으로 진행하였을 경우에만 진행해야한다. 따라서, 전에 작성한 CustomFormLoginSuccessHandler에서 작성해준다. 또한, redisUtils.setDataExpire()를 통해 Redis에 Refresh Token 유효시간 후에는 만료될 수 있도록 작성해준다.
[ 2. Redis 설정 및 Utils 작성 ]
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
build.gradle에 위 코드를 추가해준다.
@RequiredArgsConstructor
@Component
public class RedisUtils {
private final StringRedisTemplate stringRedisTemplate;
public String getData(String key){
ValueOperations<String, String> valueOperations = stringRedisTemplate.opsForValue();
return valueOperations.get(key);
}
public void setData(String key, String value){
ValueOperations<String, String> valueOperations = stringRedisTemplate.opsForValue();
valueOperations.set(key, value);
}
public void setDataExpire(String key, String value, long duration){
ValueOperations<String, String> valueOperations = stringRedisTemplate.opsForValue();
Duration expireDuration = Duration.ofSeconds(duration);
valueOperations.set(key, value, expireDuration);
}
public void deleteData(String key){
stringRedisTemplate.delete(key);
}
}
[ 3. Access Token이 만료되었는지 확인한 후, 재발급이 필요할 경우 재발급 진행 ]
@Slf4j
@RequiredArgsConstructor
public class JwtTokenInterceptor implements HandlerInterceptor {
private final TokenUtils tokenUtils;
private final CookieUtils cookieUtils;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
Cookie accessCookie = cookieUtils.getCookie(request, AuthConstants.AUTH_HEADER);
String accessToken = accessCookie.getValue();
if(accessToken != null && tokenUtils.isValidToken(accessToken)){
return true;
}
Cookie refreshCookie = cookieUtils.getCookie(request, AuthConstants.REFRESH_HEADER);
String refreshToken = refreshCookie.getValue();
if(refreshToken != null && tokenUtils.isValidToken(refreshToken)) {
Cookie newAccessTokenCookie = tokenUtils.reissueAccessToken(response, refreshToken);
response.addCookie(cookie);
response.setContentType("application/json;charset=UTF-8");
log.info("New AccessToken : " + newToken.getAccessToken());
log.info("New RefreshToken : " + newToken.getRefreshToken());
return true;
}
response.sendRedirect("/error/unauthorized");
return false;
}
}
인터셉터에서 JWT 유효성 검사를 진행하는 코드는 전 게시글에서도 작성했었다. 현재는 유효성 검사를 진행할 때 accessToken은 유효하지 않으나 refreshToken이 유효할 때 accessToken을 재발급하는 과정을 진행한다.
위 코드의 흐름은 다음과 같다.
- 쿠키에서 AccessToken을 추출하여 유효성 검사를 진행한다.
- AccessToken이 유효할 경우 바로 true를 반환하여 다음 과정을 진행시킨다.
- AccessToken이 유효하지 않을 경우에는 RefreshToken을 추출하여 유효성 검사를 진행한다.
- RefreshToken이 유효한 경우에는 AccessToken을 재발급한 후 이를 다시 쿠키 형태로 담는다.
- RefreshToken도 유요하지 않을 경우에는 에러를 발생시킬 수 있도록 한다.
[ 4. TokenUtils에 AccessToken 재발급 코드 추가 ]
public Cookie reissueAccessToken(HttpServletResponse response, String refreshToken) {
log.info("AccessToken 재발급 진행");
String email = tokenUtils.getUid(refreshToken);
Token newToken = tokenUtils.generateToken(email, UserRole.USER.getKey());
Cookie cookie = cookieUtils.createCookie(AuthConstants.AUTH_HEADER, newToken.getAccessToken());
return cookie;
}
새로운 AccessToken을 재발급한 후 쿠키에 담아 반환하는 코드
메소드 안 첫 번째 줄 코드에 log를 적은 이유는 아래에서 확인할 때 가시적으로 판단하기 쉽게 하기 위해 작성해본 코드이다.
2. 위에서 작성한 코드들이 올바르게 작동하는지 간단한 API호출을 통해 확인
회원가입이 되어있는 사용자 정보로 로그인을 진행하려 한다. 위 처럼 올바르게 정보를 전송하면
200 OK와 함께 쿠키에 AccessToken과 RefreshToken이 담아져 있는 것을 확인할 수 있다.
AccessToken 유효시간이 지났을 때 재발급 과정이 잘 이루어지는 지 확인해보자. 위에서 로그를 통해 확인할 수 있도록 log.info()한 줄을 적어놓았으니 재발급이 성공적으로 진행된다면 찍혀있을 것이다.
로그에 성공적으로 "AccessToken 재발급 진행"이 찍혀있고,
인가기능도 성공적으로 진행되었다.
마지막으로, Refresh Token의 유효시간이 모두 끝났을 경우에는? 인터셉터에 아래 한 줄을 작성해주어 확인해보자.
log.info("Refresh Token 유효시간이 모두 경과하였으므로 인가 기능 처리 실패");
RefreshToken 유효시간을 3분으로 지정해두고,
처음으로 JWT를 발급받은 시간 3분 후에 결과를 확인해보자.
·························3분 기다리기·························
정상적으로 로그에 찍혀있으며
401 Unauthorized와 함께 인가기능도 실패하는 것을 확인할 수 있다.
여기까지,
Spring Security를 공부한 후 Spring Boot + Spring Security + JWT로 회원가입 및 로그인 구현 + JWT 재발급 및 Redis를 이용한 Refresh Token관리까지 모두 진행해보았다.
추가적으로 프로젝트를 진행하며 보완이 필요한 부분은 보완한 후 블로그에 관련 게시글을 업로드 할 예정이다.
'2022 > 2022-1' 카테고리의 다른 글
[SpringBoot] SpringBoot + SpringSecurity + JWT (회원가입 / 로그인 구현) (0) | 2022.08.10 |
---|---|
로그인 처리 - 필터 / 인터셉터 (1) (필터 개념 및 예제 코드) (0) | 2022.08.07 |
[SpringBoot] SpringBoot + SpringSecurity + JWT 구현하기 (0) | 2022.07.20 |
JWT(Json Web Token)란? (0) | 2022.07.19 |
[인증/인가] Token(토큰) 기반 인증과 Session(세션) 기반 인증 (0) | 2022.07.14 |