이전 포스팅에서 스프링 시큐리티와 JWT를 사용하여 단일 토큰 인증/인가에 대해서 실습하였다.
이전 포스팅
https://jiy0ung.tistory.com/26
[Spring] 스프링 시큐리티 JWT 구현 해보기 (단일 토큰)
스프링 시큐리티를 활용하여 JWT 기반의 인증/인가를 구현하고, MySQL 데이터베이스를 활용하여 회원 정보 저장을 해볼 것이다.서버는 웹 페이지를 응답하는 것이 아닌 API 클라이언트 요청을 통해
jiy0ung.tistory.com
이어서 다중토큰(RefreshToken, AccessToken)에 대해 알아보려한다.
스프링 시큐리티 JWT 단일 토큰은 아래와 같이 사용된다.
- 로그인 성공 JWT 발급 : 서버측 -> 클라이언트로 JWT 발급
- 권한이 필요한 모든 요청 : 클라이언트 -> 서버측 JWT 전송
권한이 필요한 요청은 서비스에서 많이 발생한다. (회원 CRUD, 게시글/댓글 CRUD, 주문 .....)
따라서, JWT는 매시간 수많은 요청을 위해 클라이언트의 JS 코드로 HTTP 통신을 통해 서버로 전달된다.
해커는 클라이언트 측에서 XSS를 이용하거나 HTTP 통신을 가로채 토큰을 훔칠 수 있다.
그래서 이번 포스팅에서는 다중토큰 (RefreshToken, AccessToken) 기술을 도입하여 탈취를 방지하고 탈취되었을 경우 대비 로직을 구현하려한다.
다중 토큰 : RefreshToken 과 생명주기
자주 사용되는 토큰의 생명주기는 짧게(약 10분) 한다. 이 토큰이 만료되었을때 함께 받은 Refresh 토큰(24시간 이상)으로 토큰을 재발급한다.
생명 주기가 짧으면 만료시 매번 로그인을 진행하는 문제가 발생한다. 생명주기가 긴 RefreshToken도 함께 발급한다.
1. 로그인 성공 시 생명주기와 활용도가 다른 토큰 2개 발급 : AccessToken/RefreshToken
- AccessToken : 권한이 필요한 모든 요청 헤더에 사용될 JWT로 탈취 위험을 낮추기 위해 약 10분 정도의 짧은 생명주기를 가진다.
- RefreshToken : Access 토큰이 만료되었을 때 재발급 받기 위한 용도로만 사용되며 약 24시간 이상의 긴 생명주기를 가진다.
2. 권한이 필요한 모든 요청 : Access 토큰을 통해 요청
Access 토큰만 사용하여 요청하기 때문에 Refresh 토큰은 호출 및 전송을 빈도가 낮음.
3. 권한이 알맞다는 가정하에 2가지 상황 : 데이터 응답, 토큰 만료 응답
4. 토큰이 만료된 경우 Refresh 토큰으로 Access 토큰 발급
AccessToken이 만료되었다는 요청이 돌아왔을 경우 프론트엔드 로직에 의해 "1"에서 발급 받은 RefreshToken을 가지고 서버의 특정 경로(RefreshToken을 받는 경로)에 요청을 보내어 AccessToken을 재발급 받는다.
5. 서버측에서는 RefreshToken을 검증 후 AccessToken을 새로 발급한다.
다중 토큰 구현 포인트
- 로그인이 완료되면 successHandler에서 Access/Refresh 토큰 2개를 발급해 응답한다. 각 토큰은 각기 다른 생명 주기, payload 정보를 가진다.
- AccessToken 요청을 검증하는 JWTFilter에서 AccessToken이 만료된 경우는 프론트 개발자와 협의된 상태코드와 메시지를 응답한다.
- 프론트측 API 클라이언트(axios, fetch) 요청 시 AccessToken 만료 요청이 오면 예외문을 통해 RefreshToken을 서버측으로 전송하고 AccessToken을 발급 받는 로직을 수행한다.(기존 AccessToken은 제거)
- 서버측에서는 RefreshToken을 받을 엔드포인트(컨트롤러)를 구성하여 RefreshToken을 검증하고 AccessToken을 응답한다.
RefreshToken이 탈취되는 경우
단일 -> 다중 토큰으로 전환하며 자주 사용되는 AccessToken이 탈취되더라도 생명주기가 짧아 피해 확률이 줄 수 있다.
하지만, RefreshToken 또한 사용되는 빈도만 적을 뿐 탈취될 수 있는 확률이 존재한다. 그래서 RefreshToken에 대한 보호 방법도 필요하다.
Access/Refresh 토큰의 저장 위치 고려
- 로컬/세션 스토리지 및 쿠키에 따라 XSS, CSRF 공격의 여부가 결정되기 때문에 각 토큰 사용처에 알맞은 저장소를 설정해야 한다.
RefreshToken Rotate
- AccessToken을 갱신하기 위한 RefreshToken 요청 시 서버측에서 RefreshToken도 재발급을 진행하여 한 번 사용한 RefreshToken은 재사용하지 못하도록 한다.
Access/Refresh 토큰 저장 위치
클라이언트에서 발급 받은 JWT를 저장하기 위해 로컬 스토리지와 쿠키에 대해 많은 고려를 한다. 각 스토리지에 따른 특징과 취약점은 아래와 같다. (이 설정은 필수적이지 않아 주관적인 판단으로 커스텀 하면 된다.)
- 로컬 스토리지 : XSS 공격에 취약함 -> AccessToken 저장
- httpOnly 쿠키 : XSRF 공격에 취약함 -> RefreshToken 저장
AccessToken
- AccessToken은 주로 로컬 스토리지에 저장된다. 짧은 생명 주기로 탈취해서 사용까지 기간이 매우 짧고, 에디터 및 업로더에서 XSS를 방어하는 로직을 작성하여 최대한 보호할 수 있지만 CSRF 공격의 경우 클릭 한 번으로 단시간에 요청이 진행되기 때문이다.
AccessToken은 권한이 필요한 모든 경로에 사용되기때문에 CSRF 공격을 받는 위험보다는 XSS 공격을 받는게 더 나은 선택일 수도 있다.
RefreshToken
- RefreshToken은 주로 쿠키에 저장된다. 쿠키는 XSS 공격을 받을 수 있지만 httpOnly를 설정하면 완벽히 방어할 수 있다.
그럼 가장 중요한 CSRF 공격에 대해 위험하지 않을까 의구심이 생긴다.
하지만, RefreshToken의 사용처는 단 하나인 토큰의 재발급 경로다. CSRF는 AccessToken이 접근하는 회원 정보 수정, 게시글 CRUD등과 같은 요청에 취약하지만 토큰 재발급 경로에서는 크게 피해를 입을 만한 로직이 없다.
RefreshToken Rotate
저장소 특징에 알맞은 JWT 보호 방법을 수행해도 탈취를 당할 수 있다. 따라서 생명 주기가 긴 RefreshToken에 대한 추가정인 방어 조치도 취하면 좋다.
AccessToken이 만료되어 RefreshToken을 가지고 서버 특정 엔드포인트에 재발급을 진행하면 RefreshToken 또한 재발급하여 프론트측으로 응답하는 Refresh Rotate를 적용하면된다.
로그아웃과 RefreshToken 주도권
로그아웃을 구현하면 프론트측에 존재하는 Access/Refresh 토큰을 제거하게 된다. 그럼 프론트측에서 요청을 보낼 JWT가 없기 때문에 로그아웃이 되었다고 생각하지만, 이미 해커가 JWT를 복제 했다면 요청을 수행할 수 있다.
이러한 문제가 존재하는 이유는 단순하게 JWT를 발급해준 순간 서버측의 주도권이 없기 때문이다. (세션 방식은 상태를 STATE하게 관리하기 때문에 주도권이 서버측에 있다.)
로그아웃 케이스뿐 아니라 JWT가 탈취 되었을 경우 서버 측 주도권이 없기 때문에 피해를 막을 방법은 생명주기가 끝이나길 기다리는 방법이다. 그래서 이러한 문제를 해결하기위해 생명주기가 긴 RefreshToken은 발급과 함께 서버측 저장소에도 저장하여 요청이 올때마다 저장소에 존재하는지 확인하는 방법으로 서버측에서 주도권을 가질 수 있다.
만약, 로그아웃을 진행하거나 탈취에 의해 피해가 진행되는 경우 서버측 저장소에서 해당 JWT를 삭제하여 피해를 방해할 수 있다.
(RefreshToken 블랙리스팅 이라고도 불린다. / 블랙리스트 처리)
또한, 네이버 서비스를 이용하다 보면 평소에 사용하지 않던 IP나 브라우저에서 접근한 경우 사용자의 계정으로 이메일 알림이 발생한다.
이때 내가 아닐경우 "아니오"를 클릭하면 서버측 토큰 저장소에서 해당 유저에 대한 RefreshToken을 모두 제거하여 앞으로의 인증을 막을 수 있게 되는 것이다.
참고
'# Study > Spring' 카테고리의 다른 글
[Spring] @Scheduled로 만료 RefreshToken 정리하기 (0) | 2024.11.30 |
---|---|
[Spring] Spring Security + JWT 구현 해보기 (다중 토큰) (2) (0) | 2024.11.27 |
[Spring] Spring Security + JWT 구현하기 (단일 토큰) (0) | 2024.11.26 |
[Spring] Spring Security 개념과 작동방식 (0) | 2024.11.16 |
[Spring] Spring Security, JWT 간단 정리 (1) | 2024.05.29 |