이어서...
1. 토큰의 유효성 검사 (Authorization)
이제 로그인 할 때 토큰을 발급해주었으니
- 클라이언트가 이후 엑세스 토큰을 들고 요청 오면, 해당 토큰의 유효성을 검사한 뒤 요청을 처리해주고,
- 엑세스 토큰이 만료되었을 경우, 클라이언트는 리프레시 토큰을 사용하여 새로운 엑세스 토큰을 얻을 수 있어야 한다.
이제 사용자는 매번 로그인을 하는 것이 아닌,
이전에 발급 받은 Access Token을 들고 서버로 요청을 하면
서버는 해당 토큰을 검증하여 유효한 토큰인지 확인 후 클라이언트 요청을 처리해주면 된다.
2. JwtAuthorizationFilter (인가 필터)
OncePerRequestFilter는 한 번의 요청 처리 중에 한 번만 실행되도록 보장하는 필터 기본 클래스이다.
이를 상속해주자!
그리고, OncePerRequestFilter에 들어가보면 이러한 주석이 달려있다!
doFilterInternal 메서드를 재정의하여 필터의 실제 동작을 구한다.
이 메서드는 모든 요청에 대해 실행되며, HTTP 요청과 응답 객체를 전달받는다.
이 메서드는 요청에 대한 처리를 수행하고 필요한 경우 다음 필터로 요청을 전달한다.
원래 이 doFilterInternal은
헤더에 Authorization : Basic *** 방식으로 인증을 시도하면 BasicAuthenticationFilter에서 해당 토큰을 검증하여 인증을 처리한다.
또한, 인증에 통과하지 못한 사용자는 아예 Controller 단에 접근하지 못하게 한다!
우리는 이를 오버라이드 하여, JWT 토큰을 검증할 것이다!
우리는 "Basic 방식이 아니기 때문"
2-1. JwtUtil 수정
해당 부분을 추가해주자!
// HTTP 요청의 'Authorization' 헤더에서 JWT 액세스 토큰을 검색
public String resolveAccessToken(HttpServletRequest request) {
String authorization = request.getHeader("Authorization");
if (authorization == null || !authorization.startsWith("Bearer ")) {
log.warn("[*] No Token in req");
return null;
}
log.info("[*] Token exists");
return authorization.split(" ")[1];
}
// 토큰 유효성 검사
public void validateToken(String token) {
try {
// 구문 분석 시스템의 시계가 JWT를 생성한 시스템의 시계 오차 고려
// 약 3분 허용.
long seconds = 3 *60;
boolean isExpired = Jwts
.parser()
.clockSkewSeconds(seconds)
.verifyWith(secretKey)
.build()
.parseSignedClaims(token)
.getPayload()
.getExpiration()
.before(new Date());
log.info("[*] Authorization with Token");
if (isExpired) {
log.info("만료된 JWT 토큰입니다.");
}
} catch (SecurityException | MalformedJwtException e) {
log.info("잘못된 JWT 서명입니다.");
} catch (ExpiredJwtException e) {
log.info("만료된 JWT 토큰입니다.");
} catch (UnsupportedJwtException e) {
log.info("지원되지 않는 JWT 토큰입니다.");
} catch (IllegalArgumentException e) {
log.info("JWT 토큰이 잘못되었습니다.");
}
}
사실 유효성 검사는 ".verifyWith(secretKey)" 한 줄 에서 이루어지며,
우리가 내부적으로 가지고 있는 secretKey를 사용하여, 서명 검사를 통과하면, "우리 secretKey로 발급한 토큰" 이라는 뜻이다!
+
유효기간이 만료되었는지 현재 기간을 기준으로 검사해준다.
* clockSkewSeconds() 메서드는 뭐야?
해당 메서드는 JWT의 만료 시간을 검증할 때 클라이언트와 서버의 시간 차이를 고려하기 위해 사용된다.
JWT의 만료 시간을 검증할 때 JWT의 payload에 있는 만료 시간을 확인하고, 이 시간이 현재 시간보다 이전인지 확인한다.
그러나 네트워크 지연이나 클라이언트와 서버의 시간 차이 등으로 인해 시간의 불일치가 발생할 수 있다.
이 때 clockSkewSeconds 메서드를 사용하여 클라이언트와 서버의 시간 차이를 고려할 수 있다.
해당 링크 참고 : https://github.com/jwtk/jjwt?tab=readme-ov-file#accounting-for-clock-skew
2-2. JwtAuthorizationFilter 작성
@Slf4j
@RequiredArgsConstructor
public class JwtAuthorizationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final RedisUtil redisUtil;
@Override
protected void doFilterInternal(
@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain
) throws ServletException, IOException {
log.info("[*] Jwt Filter");
try {
String accessToken = jwtUtil.resolveAccessToken(request);
// accessToken 없이 접근할 경우
if (accessToken == null) {
filterChain.doFilter(request, response);
return;
}
// 유효성 검사
jwtUtil.validateToken(accessToken);
// accesstoken을 기반으로 principalDetail 저장
PrincipalDetails principalDetails = new PrincipalDetails(
jwtUtil.getUsername(accessToken),
null,
jwtUtil.getRoles(accessToken)
);
// 스프링 시큐리티 인증 토큰 생성
Authentication authToken = new UsernamePasswordAuthenticationToken(
principalDetails,
null,
principalDetails.getAuthorities());
// 컨텍스트 홀더에 저장
SecurityContextHolder.getContext().setAuthentication(authToken);
filterChain.doFilter(request, response);
} catch (ExpiredJwtException e) {
try {
HttpResponseUtil.setErrorResponse(response, HttpStatus.UNAUTHORIZED, "엑세스 토큰이 유효하지 않습니다.");
} catch (IOException ex) {
log.error("IOException occurred while setting error response: {}", ex.getMessage());
}
log.warn("[*] case : accessToken Expired");
}
}
현재 우리는 Token 서명으로 무결성을 검증했다!
그리고, accesstoken을 가지고 principalDetail을 만들고
시큐리티 인증 토큰을 생성하여, Spring Context Holder (세션)에 저장하였다!
그런데, 이전 포스팅 에서도 말했듯이
권한관리를 위한 세션 저장은 필요가 없다면 생략해도 된다!
(유효성 검사만 하면 된다는 뜻!)
다음 포스팅에서 "RefreshToken을 사용하여 토큰 재발급" 하는 방법에 대해 알아보겠다!