Intro
저번에 설정한 Security 설정을 기반으로 Jwt 필터에서 어떻게 토큰 인증을 검사하는 지를 작성해보려고 한다.
많은 인터넷 자료를 찾아봤지만 베끼지는 않고 스스로 적용을 해보려 했고, 당연히 정답이 아닐 수 있다.
또한, jjwt 라이브러리를 이용해서 AccessToken과 RefreshToken 두 토큰을 발행한 이후의 과정을 작성하려고 한다.
Filter에서 토큰 인증하기
맨 먼저 코드를 작성하기 이전에, 토큰 인증에 대한 시나리오를 작성해봤다.
AccessToken이 유효한지 만료되었는지? 와 RefreshToken이 유효한지 만료되었는지? 의 경우의 수를 생각해 총 4가지의 시나리오가 있고, 이 때에 어떻게 판별하는 것이 좋은지 생각해보았다.
<이전 시나리오>
AT(AccessToken)와 RT(RefreshToken) 두 개를 모두 검사하는 로직이다.
코드는 아래와 같이 작성해보려 했다.
AT와 RT 둘 다 존재할 경우에만 진행을 하고, RT부터 유효한지 파악을 하고, AT에 대한 유효성 검사등을 진행했다.
그 후에 정상적이라고 판단이 된다면
Authentication 유저 객체를 생성해서 SecurityContextHolder에 저장을 하고 다음 필터로 진행을 했다.
또한, 만료되거나 로그아웃을 진행한 RT의 경우 ForbiddenToken으로 변경하여 Redis에 보관하도록 진행했다.
이 글을 작성하는 순간까지도, 이 시나리오의 방향성으로 진행을 하고 있었다. 그런데, 글 작성을 하며 생각해보았을 때, (2)번에서 AT유효/RT만료일 때, RT를 재발급해준다면 문제가 생긴다는 것을 파악했다
이유는, 만약 AT와 RT를 탈취당한 상태에서, RT가 만료된다면 탈취자가 새로운 RT를 받게 될 것이다. 그렇다면 이를 이용해서 (3)번의 새로운 AT를 발급받을 수 있게 되며, 탈취자는 사용자가 새로운 접속을 하지 않는 이상 자유롭게 이용을 할 수 있게 된다. 즉, 치명적인 보안적 허점이 발생할 수 있을 것이라 생각했다.
그래서 시나리오를 다음과 같이 변경하였다.
<1차 변경 시나리오>
맨 처음에 작성한 시나리오의 경우 AT가 유효하더라도, RT의 유효성 여부를 검사하였다. 하지만, 이번 시나리오에서는 AT가 유효하다면 RT에 대한 부분을 검사하지 않고 그대로 진행하기로 했다.(AT의 만료 시간을 짧게 준 것도, 탈취당하였을 때의 위험을 최소화 하기 위함. 하지만 모든 부분에서 이를 책임질 수는 없다고 생각했다.)
따라서, AT가 만료되었을 때만, RT에 대한 유효성 부분을 검증하는 것으로 변경을 하였다.
그럼 여기서 한 가지 의문점이 생기게 되었다. 만약 단순히, AT의 유효성만 검사를 하고 필터를 통과시키게 된다면, RT가 존재하지 않거나, 이상한 값(더미값 등)으로 들어오더라도 통과시키게 된다는 점이었다.
따라서, RT의 존재 유무와 만료 여부 유무를 체크하는 분기를 만들어야겠다고 생각했다.
<2차 변경 시나리오>
조금 수정해본 로직이다. AT가 유효한 경우에는 RT를 따로 체크하지는 않지만, 쿠키로 넘어온 RT를 resolve하는 과정에서 올바른 서명인지 체크는 진행을 하는 것이 옳다고 생각했다.
따라서 AT가 유효하며 RT가 존재하고 서명이 맞을 경우 그냥 넘어가게 진행을 했다.
반대로, AT가 유효하지 않을 경우 에는 RT의 유효성을 체크해서 RT가 만료되지 않았다면 새로운 AT를 내려주고, 그것이 아니라면 로그인을 요청하는 분기로 나누게 되었다.
가장 먼저, AT와 RT가 존재하는지 여부를 체크하고,
AT의 만료체크, 유효한 RT인지 체크, 로그아웃을 해서 유효하지만 Forbidden된 RT인지를 판별하는 3가지 체크 방식을 만들었다.
(로그아웃을 하게 될 경우, 현재 존재하는 RT를 Forbidden으로 판단하여 Redis에 따로 저장하게 만들었다.)
새로운 AT를 내려주는 경우에는, 현재 내 AT발급 방식이 CustomUserDetails나 Oauth2User을 토대로 memberId 등을 가져오게 했기 때문에, 객체를 생성해 주었다.(토큰에서 따온 값으로 내려주는 방식으로 변경할 수 있을 것 같다.)
만약 AT가 만료된 경우에는 인증 실패라는 response를 보내주었다.
그렇게 모든 과정을 통과하게 된다면
UserDetail 객체를 생성하여 Authentication 객체를 세팅하고 다음 필터로 이동을 하게 하였다.
다 끝난것일까?
사실 처음 작성해보는 과정이다 보니, 모든 로직이 완벽하지도 않을 것이고 내가 생각하지 못한 부분에서 에러가 생길 수도 있다.
또한, 프론트와 연결을 하지 않아본 상태이니, 연결을 하게 된다면 또 다른 이유에서 수정이 필요할 수도 있다.
또한, 현재 상태에서는 회원 / 비회원을 구분하기 위해서 ignoreUrl을 통해야만 한다.
추후 포스팅에서는 이 부분을 @PreAuthorize 나 커스텀 어노테이션을 통해서 컨트롤러단에서 직접 사용할 수 있게 해볼 생각이다.