궤도

[Springboot] 만료된 Access Token이 왜 NPE를 띄울까? (이상하게 해결함) 본문

💻 현생/📕 면적면적(스프링 실습)

[Springboot] 만료된 Access Token이 왜 NPE를 띄울까? (이상하게 해결함)

영이오 2021. 12. 10. 18:41

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

 

Spring Security - Filter, FilterChain

Spring Security (스프링 시큐리티) 스프링 시큐리티를 이용하면 개발시에 필요한 사용자의 인증, 권한, 보안 처리를 간단하지만 강력하게 구현 할 수 있습니다. 일반적인 웹 환경에서 브라우저가

siyoon210.tistory.com

토큰이 유효하면 Authentication에 유저정보를 세팅하는데 내 생각엔 그걸 하지 않아서 다음 필터에 NPE가 발생하는거 아닌가 싶다. (아닐수도)

 

https://velog.io/@sa833591/Spring-Security-5-Spring-Security-Filter-%EC%A0%81%EC%9A%A9

 

Spring Security (5) - Spring Security Filter 적용

Web Security기본 설정시 Spring Security는 일련의 서블릿 필터 체인을 자동으로 구성한다.(web tier에 있는 Spring Security는 Servlet Filter에 기반을 두고 있다.)일반적인 웹 환경에서 브라우저가 서버에게 요

velog.io

이분은 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

 

ExpiredJwtException (JSON Web Token support for the JVM 0.4 API) - Javadoc Extreme

New Blog Post! Astyanax, the Cassandra Java library New blog post: Getting started with Astyanax, the open source Cassandra java library and connect your application to one of the most important NoSQL database. Read Blog Post

javadox.com

커스텀 익셉션을 만들고 있었는데 이런게 있다!

 

https://beemiel.tistory.com/11

 

Spring Security JWT 토큰 검증 시 Exception 예외 처리

Spring Security 예외 Spring Security에서 토큰을 검증할 경우, 예외가 발생한다면 기존에 사용 중이던 Custom Exception으로 처리가 될까? 그러면 편하긴 하겠지만 그건 안될 말이지^^ 🙃아니🙃 왜 안되는

beemiel.tistory.com

한편 이런 글도 발견했다. 진짜인가 싶어 커스텀 익셉션을 대충 만들고 실행했더니

 

진짜다...!

 

https://velog.io/@dltkdgns3435/%EC%8A%A4%ED%94%84%EB%A7%81%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0-JWT-%EC%98%88%EC%99%B8%EC%B2%98%EB%A6%AC

 

스프링시큐리티 JWT 예외처리

🎈시작하며 > 이 포스트는 정답이 아니며, 주관적인 생각에 의해 작성한 글입니다. 해당 포스트를 보고 제가 잘못 알고 있는 점이나, 지적사항이 있으시면 댓글로 남겨주시면 정말 감사하겠습

velog.io

여기를 참고할 것이다.

 

현 상황 : ExpiredJwtException을 띄우긴 했는데 잡히지 않고 계속 NPE만 넘어가는 상태

 

주말동안 깊게는 아니고 대충 생각했다. 구글링 키워드를 바꿔야겠다. 일단 그 전에...

내 기억이 맞았다. 분명 처음에는 토큰 만료시 UsernameNotFoundException이 발생했었다.

 

https://myunji.tistory.com/477?category=1194492 

 

[Spring] @Transational을 붙였는데도 LazyInitializationException이 발생한다면? (To. Spring Security를 쓰는 누군

면적면적 열심히 개발을 하던 중... org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role 이런 에러가 발생했다. 이거 책에서 많이 본건데...지연로딩과 관련해서 생기..

myunji.tistory.com

내 생각엔 이게 수상하다.

-> 아니다 얘는 범인이 아닌가보다

 

그니까 이건 금요일부터 생각한거지만 doFilter에 null이 넘어가는게 문제가 맞는듯하다.

 

테스트 플젝을 파야겠다...

야심차게 플젝을 따로 팠는데 또 NPE다. 이정도면 처음에 떴던 오류가 이상한건가 ㅎㅋ

 

이것만...잡으면 되는데

 

https://brunch.co.kr/@springboot/491#comment

 

JWT & Spring Security

Overview 이번 글에서는 JWT & Spring Security 에 대해서 정리해서 샘플코드와 함께 공유합니다. 외부에 공유되는 이 글에서는, 회사의 소스 코드 및 비즈니스 내용은 보안상 전혀 다루지 않습니다. 모

brunch.co.kr

https://bcp0109.tistory.com/301

 

Spring Security 와 JWT 겉핥기

Introduction 이 글에서는 Spring Boot + JWT + Security 를 사용해서 회원가입/로그인 로직을 구현했습니다. JWT 와 Spring Security 코드는 인프런 Spring Boot JWT Tutorial (정은구) 강의를 수강하면서 만들고..

bcp0109.tistory.com

 

AuthenticationEntryPoint를 구현하는게 핵심인가보다.

https://github.com/ParkJiwoon/practice-codes/tree/master/spring-security-jwt

 

GitHub - ParkJiwoon/practice-codes: 공부용 테스트 Project 모음

공부용 테스트 Project 모음. Contribute to ParkJiwoon/practice-codes development by creating an account on GitHub.

github.com

저 분의 깃허브 소스코드를 실행해보니 잘된다...그럼 이제 이걸 이해하기만 하면 된ㄷ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()));
    }

글로벌 익셉션 핸들러에 예외를 추가해줬다.

 

이게 통한다는게 아주 놀랍지만, 일단 이건 토큰에 문제가 있다는 것만 판단할 수 있고, 컨트롤러에 이 코드를 하나하나 넣어줘야한다는 번거로움이 있다.

Comments