Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
Tags
- 1차캐시
- Spring Spring boot 차이
- jwt메서드
- Q 클래스
- jpa에러
- ERD 작성
- REST API 규칙
- JoinColumn
- @IdClass
- Error creating bean with name
- REST란
- queryDSL
- JPA
- 인텔리제이
- json gson 차이
- Unsatisfied dependency
- spring서버
- 복합키
- uncheck Exception
- 스프링 부트 기능
- 스프링부트오류
- JPA주의사항
- jpa회원가입
- 스프링 부트 공식 문서
- 최종 프로젝트
- git
- 빈생성안됨
- Filter
- json
- github
Archives
- Today
- Total
Everyday Dev System
Access Token 만료시 재요청 없이 재발급 본문
Access Token과 Refresh Token을 처리할 때 문제점을 발견했습니다.
로직을 요약하면,
- 클라이언트의 로그인 요청
- 서버에서 Access Token과 Refresh Token 생성 및 반환 (Refresh Token Redis에 저장)
- 클라이언트에서 Access Token과 Refresh Token을 쿠키에 저장 (HttpOnly설정)
이와 같습니다.
먼저, Access Token이 만료되기 이전에 요청은 Spring Security Filter Chain을 잘 거쳐
Controller 단에 도달하여 해당 요청(재발급)을 잘 처리합니다.
문제는 만료된 Access Token 으로 요청시 Controller 단에 도달조차 못 한다는 것입니다.
이는 Filter에서 토큰 검증을 거치기 때문에 검증 실패 시 에러를 반환하기 때문입니다.
Access Token 만료시, 재요청 과정 개선 방안
이를 해결하기 위해 Filter 단에서 처리 로직을 추가하였습니다.
1. 매 요청마다 Access Token의 유효성 검사를 거칩니다.
OncePerRequestFilter 를 상속한 Filter class 내에 doFilterInternal() 메서드 에서 토큰 검증 코드를 추가합니다.
- 요청에 따른 헤더에서 토큰을 모두 추출합니다. (Decode 필요)
- 추출한 토큰에서 "Bearer "을 삭제합니다.
- 해당 토큰을 통해 유효성 검사를 거칩니다.
- Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException, IOException {
String AccesstokenValue = jwtUtil.getAccessTokenFromRequest(request);
String refreshTokenValue = jwtUtil.getRefreshTokenFromRequest(request);
if (StringUtils.hasText(AccesstokenValue)) {
// JWT 토큰 substring
AccesstokenValue = jwtUtil.substringToken(AccesstokenValue);
refreshTokenValue = jwtUtil.substringToken(refreshTokenValue);
if (!jwtUtil.validateToken(AccesstokenValue)) {
log.error("Access Token does not valid.");
--중략--
public String getRefreshTokenFromRequest(HttpServletRequest req) {
Cookie[] cookies = req.getCookies();
if(cookies != null) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals(REFRESH_HEADER)) {
return URLDecoder.decode(cookie.getValue(), StandardCharsets.UTF_8); // Encode 되어 넘어간 Value 다시 Decode
}
}
}
return null;
}
public String substringToken(String tokenValue) {
if (StringUtils.hasText(tokenValue) && tokenValue.startsWith(BEARER_PREFIX)) {
return tokenValue.substring(7);
}
logger.error("Not Found Token");
throw new NullPointerException("Not Found Token");
}
public boolean validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
return true;
} catch (SecurityException | MalformedJwtException | SignatureException e) {
logger.error("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.");
} catch (ExpiredJwtException e) {
logger.error("Expired JWT token, 만료된 JWT token 입니다.");
} catch (UnsupportedJwtException e) {
logger.error("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.");
} catch (IllegalArgumentException e) {
logger.error("JWT claims is empty, 잘못된 JWT 토큰 입니다.");
}
return false;
}
2. Access Token 만료 시, Refresh Token의 유효성 검사를 거친 후 토큰 재발급
redisServiceImpl 클래스의 generateAccessToken() 메서드를 통해 유효성 검사를 합니다.
- 요청에 따른 헤더에서 refresh token을 추출합니다. (Decode 필요)
- 추출한 토큰에서 "Bearer "을 삭제합니다.
- 해당 토큰을 통해 유효성 검사를 거칩니다.
1. Refresh Token이 만료되었거나 없을 경우에는 에러를 반환합니다.
2. Refresh Token이 유효하다면, Access Token을 재발급합니다.
redisService.generateAccessToken(request, response);
response.sendRedirect("/");
@Override
public Boolean generateAccessToken(HttpServletRequest request, HttpServletResponse response) {
// 클라이언트 쿠키에서 refresh token 추출
String InputRefreshToken = jwtUtil.getRefreshTokenFromRequest(request);
String InputRefreshTokenValue = jwtUtil.substringToken(InputRefreshToken);
// refresh token 없을 경우 예외 처리
if (!StringUtils.hasText(InputRefreshToken)) {
log.error("RefreshToken is null. please login");
return false;
//throw new IllegalArgumentException("RefreshToken is null. please login");
}
// refresh token 유효성 검사 불일치
if (!jwtUtil.validateToken(InputRefreshTokenValue)) {
log.error("Refresh Token does not valid.");
jwtUtil.deleteCookie(request, response);
return false;
}
// 유저 정보 추출
Claims claims = jwtUtil.getUserInfoFromToken(InputRefreshTokenValue);
String username = claims.getSubject();
UserRoleEnum role = jwtUtil.getUserRole(claims);
// Redis 의 리프레시 토큰과 일치 여부 판단
RefreshToken refreshToken = refreshTokenRepository.findByUsername(username).get();
if (InputRefreshToken.equals(refreshToken.getRefreshToken())) {
// 엑세스 토큰 생성
createAccessToken(response, username, role);
return true;
}
return false;
}
'내배캠 주요 학습 > TIL : Today I Learned' 카테고리의 다른 글
Redis 캐시 (0) | 2023.09.05 |
---|---|
JWT 토큰은 어디에 저장해야 할까? (2) | 2023.08.25 |
ajax 통신 시 success , fail, done, fail 차이 (0) | 2023.08.23 |
Cookie.get() 이 안될 때 살펴봐야 할 사항 (0) | 2023.08.23 |
Redis 설치 및 기본 명령어 (0) | 2023.08.21 |