궤도
[Springboot] 만료된 Access Token이 왜 NPE를 띄울까? (이상하게 해결함) 본문
https://myunji.tistory.com/452?category=1194492
https://myunji.tistory.com/466?category=1216387
여기에서 이어지는 글
어케저케 Spring Security를 잘 구현했는데 문제가 발생했다.
만료된 토큰이 NPE를 띄우던 것...
물론 당시에도 이런식으로 처리하면 안되다는 것을 알았지만 나중을 기약하며 대충 처리해버렸다.
이제 기약했던 나중이 온 것이다.
일단 도대체 저 익셉션이 어디서 던져진 것인지 궁금했다.
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends GenericFilterBean {
private final JwtTokenProvider jwtTokenProvider;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
//헤더에서 JWT 받아오기
String token = jwtTokenProvider.resolveToken((HttpServletRequest) request);
if (token == null)
System.out.println("Token is NULL!!!!!!!!");
//유효한 토큰인지 확인
if (token != null && jwtTokenProvider.validateToken(token)) {
//토큰이 유효하면 토큰으로부터 유저 정보 반환
Authentication authentication = jwtTokenProvider.getAuthentication(token);
//SecurityContext에 Authentication 객체를 저장
SecurityContextHolder.getContext().setAuthentication(authentication);
}
chain.doFilter(request, response);
}
}
첫번째 후보는 필터였다. 그래서 저렇게 token이 널이면 무언가를 출력하게 했는데...
토큰이 없을 때 저런걸 띄우질 않나 정작 토큰이 만료되면 저기까지 가지도 못하고 익셉션이 던져졌다.
그나저나 출력 결과에 의하면 필터 -> 컨트롤러 로그 출력인데 이론으로 배웠던걸 직접 보는건 또 처음이라 신기하다.
그럼 도대체 익셉션은 언제 던지는걸까??
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends GenericFilterBean {
private final JwtTokenProvider jwtTokenProvider;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
//헤더에서 JWT 받아오기
System.out.println("Hi Hi");
String token = jwtTokenProvider.resolveToken((HttpServletRequest) request);
if (token == null)
System.out.println("Token is NULL!!!!!!!!");
//유효한 토큰인지 확인
if (token != null && jwtTokenProvider.validateToken(token)) {
//토큰이 유효하면 토큰으로부터 유저 정보 반환
Authentication authentication = jwtTokenProvider.getAuthentication(token);
//SecurityContext에 Authentication 객체를 저장
SecurityContextHolder.getContext().setAuthentication(authentication);
}
chain.doFilter(request, response);
}
}
이렇게 수정해봤다
...! 토큰이 만료된 이후에도 하이하이가 출력된다. 그럼 범인은 이 근처에 있다.
//Request의 Header에서 token 값 가져오기. "X-AUTH-TOKEN" : "TOKEN값"
public String resolveToken(HttpServletRequest request) {
System.out.println("I'm Okay");
String header = request.getHeader("X-AUTH-TOKEN");
if(header==null)
System.out.println("FOUND");
return header;
}
두번째 후보 resolveToken 메서드
나의 가설은 토큰 만료시 FOUND가 출력되는 것이었다.
토큰이 만료됐을 때에도 header는 널이 아니다...생각해보면 당연한거긴 했다...
근데 이상한 점이 있다. NPE는 토큰이 만료됐을 때 뿐아니라 토큰이 제대로 된 토큰이 아닐 때...그니까 오타같은게 있던 때에도 발생했다.
흐으으으음
아무튼 resolve token은 무죄다
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends GenericFilterBean {
private final JwtTokenProvider jwtTokenProvider;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
//헤더에서 JWT 받아오기
System.out.println("Hi Hi");
String token = jwtTokenProvider.resolveToken((HttpServletRequest) request);
//유효한 토큰인지 확인
if (token != null && jwtTokenProvider.validateToken(token)) {
System.out.println("Am I valid...?");
//토큰이 유효하면 토큰으로부터 유저 정보 반환
Authentication authentication = jwtTokenProvider.getAuthentication(token);
//SecurityContext에 Authentication 객체를 저장
SecurityContextHolder.getContext().setAuthentication(authentication);
}
chain.doFilter(request, response);
}
}
세번째 후보 validateToken 메서드가 일을 하지 않는다면?
네 일을 하네요
나는 본질로 돌아가보기로 했다...왜냐하면 남은 범인 후보가 chain.doFilter 밖에 없기 때문이었다.
https://siyoon210.tistory.com/32
토큰이 유효하면 Authentication에 유저정보를 세팅하는데 내 생각엔 그걸 하지 않아서 다음 필터에 NPE가 발생하는거 아닌가 싶다. (아닐수도)
https://velog.io/@sa833591/Spring-Security-5-Spring-Security-Filter-%EC%A0%81%EC%9A%A9
이분은 expire 체크를 하고 만료됐다면 토큰을 재발급하신다. 하지만 난 그걸 리프레시토큰을 받아서 처리하니까...
//토큰의 유효성과 만료일자 확인
public boolean validateToken(String jwtToken) {
try {
Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwtToken);
return !claims.getBody().getExpiration().before(new Date()); //만료일자
} catch (Exception e) { //유효성
return false;
}
}
여기서 예외를 던져야겠다.
http://javadox.com/io.jsonwebtoken/jjwt/0.4/io/jsonwebtoken/ExpiredJwtException.html
커스텀 익셉션을 만들고 있었는데 이런게 있다!
https://beemiel.tistory.com/11
한편 이런 글도 발견했다. 진짜인가 싶어 커스텀 익셉션을 대충 만들고 실행했더니
진짜다...!
여기를 참고할 것이다.
현 상황 : ExpiredJwtException을 띄우긴 했는데 잡히지 않고 계속 NPE만 넘어가는 상태
주말동안 깊게는 아니고 대충 생각했다. 구글링 키워드를 바꿔야겠다. 일단 그 전에...
내 기억이 맞았다. 분명 처음에는 토큰 만료시 UsernameNotFoundException이 발생했었다.
https://myunji.tistory.com/477?category=1194492
내 생각엔 이게 수상하다.
-> 아니다 얘는 범인이 아닌가보다
그니까 이건 금요일부터 생각한거지만 doFilter에 null이 넘어가는게 문제가 맞는듯하다.
테스트 플젝을 파야겠다...
야심차게 플젝을 따로 팠는데 또 NPE다. 이정도면 처음에 떴던 오류가 이상한건가 ㅎㅋ
이것만...잡으면 되는데
https://brunch.co.kr/@springboot/491#comment
https://bcp0109.tistory.com/301
AuthenticationEntryPoint를 구현하는게 핵심인가보다.
https://github.com/ParkJiwoon/practice-codes/tree/master/spring-security-jwt
저 분의 깃허브 소스코드를 실행해보니 잘된다...그럼 이제 이걸 이해하기만 하면 된ㄷr...
NPE의 원인을 찾은 것 같다.
얘가 문제인가보다.
이 때 이렇게 나왔던 것 역시 이때는 @AuthenticationPrincipal을 사용하지 않았던 것으로 추정...
지금 가장 그나마 쉬운 방법은 저 위에 있는 깃허브 코드대로 거의 모든 부분을 뜯어 고치는 것이다...
저 코드랑 내 코드는 dependency도 좀 다르고 filter 구현할 때 상속한 클래스도 다른데, 익셉션을 처리한 대다수의 코드는 다 이 방식이다. 내가...내가 뜯어 고쳐야 하나보다.
일단 @AuthenticationPrincipal 없이 현재 사용자를 가져오려 했다. 하지만 다들 NPE가 뜨거나 작동하지 않는다.
근데 현재 사용자 정보를 가져오는데에는 많이들 @AuthenticationPrincipal을 사용하는데 이게 무슨 일일까~
이건 좀 더 한참 공부해야겠다.
이거 진짜 어려운 내용이었구ㄴr
라고 글을 맺으며 순순히 패배를 인정하려 했는데
내가 한 일에 대해 정리하다가 이런 생각이 들었다.
@AuthenticationPrincipal로 넘어온 유저의 토큰이 invalid 해서 유저 정보가 null이고, 그래서 NPE가 발생한다면 컨트롤러에서 NPE 체크를 하면 되지 않나?
놀랍게도 이 생각이
통했다...
내가 한거라곤
@GetMapping("/")
public ResponseEntity<?> home(@AuthenticationPrincipal User user,
@RequestParam(required = false) Integer year,
@RequestParam(required = false) Integer month) {
if (user == null)
throw new InvalidTokenException("Check Access Token");
log.info("[Request] home " + user.getEmail());
HomeDto result = homeService.home(user, year, month);
if (result.getSize() == 0)
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
return new ResponseEntity<>(homeService.home(user, year, month), HttpStatus.OK);
}
그냥 여기 널체크를 추가하고
@ExceptionHandler(InvalidTokenException.class)
public ResponseEntity<?> handleInvalidTokenExceptions(InvalidTokenException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(errorMsg("InvalidTokenException", e.getMessage()));
}
글로벌 익셉션 핸들러에 예외를 추가해줬다.
이게 통한다는게 아주 놀랍지만, 일단 이건 토큰에 문제가 있다는 것만 판단할 수 있고, 컨트롤러에 이 코드를 하나하나 넣어줘야한다는 번거로움이 있다.