이 Project의 Front-End는 React로 개발되어 있고, Back-End는 Springboot로 개발 되어있어 Rest API를 통해 통신하고 있다. 두 개의 서로 다른 Framework에서 사용자 인증, 인가를 효율적으로 처리하기 위해 Token 기반의 인증 방식을 사용하였다.
Token 기반의 인증 방식이란?
Client가 가입된 아이디, 비밀번호로 로그인을 하면 Server는 Client에게 Access Token과 Refresh Token을 준다.
그 다음부터는 Client가 권한이 필요한 API에 접근할 때마다 Access Token을 가지고 본인을 인증하여 접근한다.
Access Token 안에는 유효 기간과 사용자의 아이디가 들어있다. 그래서 Server에서 Access Token을 따로 보관하지 않아도 Access Token 자체를 통해 인가를 할 것인지 판단할 수 있다.
Access Token은 공격자가 탈취하여 악용할 수 있다. 그렇기 때문에 유효 기간을 짧게 만들고, 유효 기간이 만료되면 Refresh Token을 가지고 Access Token을 다시 재발급 받을 수 있게 한다.
Refresh Token이 탈취 당했다면 Server는 이를 무력화 시킬 수 있어야 한다. 그래서 DB에 Refresh Token을 저장해 놓는다. 이 Project에서는 회원 DB에 Refresh Token Field를 만들어 저장해 놓았다.
loadUserByUsername은 입력된 아이디, 비밀번호를 확인한 후,회원 정보가 맞았을 때와틀렸을 때의 두 가지 logic으로 나누어진다.
⦁ 1.회원정보가 맞으면회원 정보와 권한이 포함된UserDetails를 생성하여 반환한다.
그리고generateTokenDtoMethod를 통해 AccessToken과 RefreshToken을 생성하여 Client로 반환한다.
⦁ 2. 정보가 틀리면BadCredentialsException예외를 발생 시킨다.
⦁ 이 Project는 예외 처리를 전역 적으로 관리하기 위해 Spring의@RestControllerAdvice를 사용해 예외 처리 Class를 따로 생성하였고,BadCredentialsException예외도 해당 Class에서 처리를 하고 있다.
⦁ 그리고 Front-End 측에서 Error Message를 쉽게 구분할 수 있도록 응답 Dto를 따로 만들어 전달해 주었다.
⦁ 또, Project에서 발생하는 Error를 Log로 관리하기 위해 AWS의 CloudWatch 로그 그룹에 전송, 저장하였다.
☝ 인증이 필요한 API 접근
Client는 로그인 후 부여 받은 Access Token을 가지고 API에 접근하게 되며, Access Token이 유효할 시에 접근이 허용된다.
Access Token을 검증한 Logic은 다음과 같다.
1.Access Token 통과 O
Spring Security에서는 인증, 인가를 위해FilterChain을 통과해야 하는데, 내가 추가한 Filter는
JwtExceptionFilter→JwtFilter→UsernamePasswordAuthenticationFilter순으로 통과하게 되어있다.
JwtExceptionFilter는Token 재발급을 위해 추가한 Filter로 아래의”2.Access Token 통과 X”설명에서 자세하게 다룬다.
Access Token 통과 O의 경우에는 JwtFilter에서 Access Token의 유효성을 검사하고, 통과하면 UsernamePasswordAuthenticationFilter에서 사용자의 정보를SecurityContextHolder에 저장하게 되고, 로그아웃 하기 전까지 사용자가 웹 서비스를 이용할 수 있게 된다.
2.Access Token 통과 X
Access Token 통과를 못하는 경우는 다시 두 가지로 나뉘어진다.
2-1. Access Token 없이 접근한 경우
2-2. Access Token이 있지만 유효하지 않은 경우
두 경우 모두인증을 통과 못한 CASE이기 때문에SecurityFilterChain에서AuthenticationEntryPoint를 실행하게 된다.
AuthenticationEntryPoint는 예외 처리로 다음과 같이”비회원 접근 불가”라는 Message를 Client로 전송해주고 있다.
그런데 여기서 문제는Access Token이 만료되었을 때와잘못된 경로로 접근 했을 때모두 Client는 동일한 Message를 전송해 주고 있다는 것이다.
이렇게 되면 Front-End에서는 Access Token이 만료되었을 때를 구분할 수 없어Access Token을 재발급받는 API 호출을 할 수 없게 된다.
이 문제를 해결하기 위해JwtFilter ➜UsernamePasswordAuthenticationFilter앞에JwtExceptionFilter를 만들어 추가하였다.
JwtExceptionFilter의 역할은 다음과 같다.
우선 다음 Filter인JwtFilter를 실행하고,JwtException을 catch한다.
JwtFilter에서는 Access Token 유효성 검증 Method를 호출하고 있고, 만약 Access Token이 유효하지 않다면JwtException예외를 발생 시킨다.
그리고 이 예외는 다시JwtExceptionFilter로 돌아가sendJwtErrorResponseMethod를 호출하여 Client에Access Token 만료Message를 전송하게 된다.
이렇게 Access Token 만료 처리를 따로 해주면 Front-End에서 Access Token이 만료되었을 때, Refresh Token으로 새로운 Token을 재발급 받는 과정을 성공적으로 처리해줄 수 있다!
☝ Refresh Token으로 Access Token 재발급
Access Token은 공격자가 탈취하여 악용할 수 있다. 이 점을 보완하기 위해 Access Token의 유효 기간을 짧게 설정하고, 이 유효 기간이 만료되면 Refresh Token을 통해 Access Token을 다시 발급 받는 방법이 있다.
Refresh Token은 DB에 저장해 놓고 있기 때문에 탈취를 당하더라도 Server가 이를 무력화 시킬 수 있는 기회가 있다.
사용자 한 명당, Refresh Token은 한 개를 보유할 수 있기 때문에 이 Project에서는 DB의 사용자의 테이블에 저장하였다.
위에서 다뤘듯이 Client에서 유효 기간이 만료된 Access Token으로 인증이 필요한 API에 접근 시,”Access Token 만료”메시지를 받고 있다.
Front-End에서는 위의 메시지를 받으면 Access Token이 만료 되었다는 것은 인지하게 되고, Access Token을 재발급 받을 수 있는 API를 호출하게 된다.
✔ Token 재발급 Service
Refresh Token도 유효 기간을 가지고 있다. Token을 재발급 하는 Service에서는 우선, Refresh Token의 유효성을 검증한다.
1. Refresh Token의 유효성 통과X
Refresh Token의 유효성을 검사하고, 만약 유효 기간이 만료되었다면JwtException예외를 발생 시킨다.
그리고 전역 적으로 예외 처리를 관리하고 있는 Class에서 에러 메시지를 생성해 Client에 알려준다.
Client 측은 이 메시지를 받고, 사용자에게다시 로그인하라는 알림을 띄워준다.
2. Refresh Token 유효성 통과 O
만약 Refresh Token이 유효성 검사를 통과 했다면, DB에 저장된 Refresh Token 데이터와 일치 한지 확인한 후, 새로운 Token을 발급해준다.
이와 같이 사용자는 Refresh Token으로 Access Token을 여러 번 재발급 받으며 로그인 상태를 오랫동안 유지하면서, Token 탈취 문제를 해결할 수 있게 된다.
✍ 회고
JWT Token 인증 방식은 Server에서 사용자의 인증 Token을 저장하지 않아도 되어 무상태(Stateless)로 관리된다고 말한다.
이는 Server의 리소스 부담을 줄여주어 큰 장점이 된다. 하지만 Access Token이 탈취 되었을 시, 대응하기 어렵기 때문에 위험을 대비할 수 있는 방법들과 함께 사용하도록 해야 한다.