반응형
이어서...
1. 왜 Refresh Token을 사용하는가?
AccessToken만을 사용했을 때의 단점이 있기 때문!
1) 일단, 탈취의 경우에 보안에 취약하다는 점
2) 그럼 그냥 ATK의 만료기간을 짧게 두면 되는 거 아니야?
3) 그러면 사용자 입장에서 매우 귀찮게 된다....
만약... 10분이라고 치면, 우리는 10분마다 재로그인을 해야 한다 ㅠㅠ
4) 그래서? -> AccessToken을 발급하는 용도로만 사용할 RefreshToken을 로그인할 때 같이 발급!
5) 그리고, RTK의 유효기간은 적당히 길게 둔다~
RefreshToken의 목적은 단지 "AccessToken의 재발급" 뿐!!
2. Access, Refresh 토큰 재발급 원리
1. 로그인 시 두개의 토큰 모두 발급한다.
- Refresh Token만 서버측에 저장한다! (인메모리 방식인 Redis에 저장하는 것이 가장 효율적!)
- 클라이언트 측에서는 Refresh Token과 Access Token을 쿠키 혹은 웹스토리지에 저장한다.
2. 사용자가 인증이 필요한 API에 접근하고자 하면 가장 먼저 헤더에 있는 AccessToken을 검사한다!
- 토큰을 검사함과 동시에 각 경우에 대해서 토큰의 유효기간을 확인하여 재발급 여부를 결정한다.
- Success : 두 개의 토큰 모두 유효 ⇒ 정상처리 (접근 허가)
- case1 : access 토큰은 만료, refresh 토큰 유효 ⇒ refresh 토큰을 검증하여 Token 쌍 모두 재발급
- case2 : refresh 토큰이 만료된 경우 ⇒ "다시 로그인해!" ⇒ 재 로그인 ⇒ 두개 모두 새로 발급
우리는 ATK가 만료된 경우, 매번 ATK와 RTK를 두 개 다 재발급 해주었다.
아마, RTK가 만료되었다는 것은 ATK도 만료되었을 확률이 매우 크다.
RTK 가 만료된 경우, 바로 재로그인 하라는 응답을 보내주면 된다.
3. 로그아웃 시 두개의 토큰 모두 만료시킨다.
- AccesToken과 RefreshToken을 만료시켜야 한다...
- 참, Stateless한 토큰을 사용할 때 항상 고민되는 부분이다. 다음 포스팅에서 다루겠다!
3. Refresh Token 의 인증과정
Access Token 만료마다 4~7 과정을 반복할 필요는 없다.
Client는 Access Token의 Payload를 통해 유효기간을 알 수 있으며,
API 요청하여 만료 신호를 받기전에 미리 검증하여 재발급 요청을 서버로 보낼 수 있다!
선택 사항이다
4. Code 작성 순서
- RTK 발급 코드 (로그인 에서 만들었다!)
1) RTK를 들고 /reissue에 접근하는 권한 처리 열어두기 (수정)
2) reissue 서비스 만들기
2-1) RTK 유효성 검사
2-2) Token 쌍 재발급
2-3) throw new NoSuchElementException("Redis에 " + username + "에 해당하는 키가 없습니다."); 될 경우,
-> 프런트에서 로그인하도록
5. RTK를 들고 /reissue에 접근하는 권한 처리 열어두기 (수정)
5-1. SecurityConfig 수정
private final String[] allowedUrls = {"/reissue", "/login"};
// 경로별 인가
http.
authorizeHttpRequests(authorizeRequests ->
authorizeRequests
.requestMatchers(allowedUrls).permitAll()
.requestMatchers("/user/**").authenticated()
.requestMatchers("/manager/**").hasAnyRole("ADMIN", "MANAGE")
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().permitAll()
);
확장성을 위해 allowedUrls를 만들고,
해당 url은 permitAll을 해주자!
6.reissue 서비스 만들기
6-1. IndexController 수정
@GetMapping("/reissue")
public ApiResponse<JwtDto> reissueToken(@RequestHeader("RefreshToken") String refreshToken) {
try {
jwtUtil.validateRefreshToken(refreshToken);
return ApiResponse.onSuccess(
jwtUtil.reissueToken(refreshToken)
);
} catch (ExpiredJwtException eje) {
throw new SecurityCustomException(TokenErrorCode.TOKEN_EXPIRED, eje);
} catch (IllegalArgumentException iae) {
throw new SecurityCustomException(TokenErrorCode.INVALID_TOKEN, iae);
}
}
Header { RefreshToken : token~~~ } 형식으로 들고,
/reissue로 Get method를 보내면 해당 controller가 돌아간다!
6-2. JwtUtil 수정
- RTK 유효성 검사
public boolean validateRefreshToken(String refreshToken) {
// refreshToken validate
String username = getUsername(refreshToken);
//redis 확인
if (!redisUtil.hasKey(username)) {
throw new SecurityCustomException(TokenErrorCode.INVALID_TOKEN);
}
return true;
}
- Token 쌍 재발급
public JwtDto reissueToken(String refreshToken) throws SignatureException {
UserDetails userDetails = customUserDetailsService.loadUserByUsername(getUsername(refreshToken));
return new JwtDto(
createJwtAccessToken((CustomUserDetails)userDetails),
createJwtRefreshToken((CustomUserDetails)userDetails)
);
}
끗! 다음으로는 조금 복잡한 로그아웃에 대해 다루어 보겠다!ㅠㅠ
반응형