[Spring Security] 예제 프로젝트2. API 기반 커스텀 인증 구현
2023.09.16 - [Spring/Security] - [Spring Security] 예제 프로젝트1. 목표 및 기본 구현 [Spring Security] 예제 프로젝트1. 목표 및 기본 구현 지금까지 정수원님의 Spring Security 강의를 보며 Spring Security 포스팅을
cares-log.tistory.com
현재 포스팅은 위 포스팅에서 이어집니다.
목표
토큰 기반 인증을 구현하기 위해 세션 기반 인증과 토큰 기반 인증이 어떤 차이점이 있는지를 분석한 후, 이 차이점을 바탕으로 어떤 정책을 가지고 토큰 기반 인증을 구현할 것인지 정리해볼 것이다.
세션 기반 인증
세션 기반 인증은 위 그림처럼 사용자의 인증 정보를 서버의 세션에서 관리하여 인증 및 인가를 수행하게 된다. 세션 기반 인증은 다음과 같은 특징이 있다.
1. 인증 정보를 관리하는 주체가 서버이기에 세션 ID의 탈취되더라도 서버 측에서 해당 세션을 무효화 할 수 있다.
2. 세션 ID는 토큰에 비해 크기가 작기 때문에 상대적으로 작은 네트워크 리소스로 요청이 가능하다.
3. 다중으로 애플리케이션을 운영하는 경우 외부 세션 스토리지 등을 구성하는 방식으로 세션을 분리해야 하기에 확장성이 떨어진다.
4. 세션 ID 자체에는 회원의 개인정보가 포함되어 있지 않기에 탈취되더라도 정보가 즉각 유출되는 것은 아니다.
5. 세션에 저장된 인증 정보의 양이 많아질수록 서버의 부담이 커진다.
6. 세션 ID가 담긴 쿠키는 하나의 도메인과 서브도메인에만 작동되기 때문에 여러 도메인을 지원하는 경우 인증이 어렵다.
다중 서버 환경에서 세션 불일치 문제를 해결하기 위한 방법으로는 다음 포스팅에 잘 정리되어 있어 공유한다.
토큰 기반 인증
토큰 기반 인증은 위 그림처럼 사용자의 인증 정보를 토큰에 저장하고 해당 토큰을 파싱하여 인증 및 인가한다. 토큰 기반 인증은 다음과 같은 특징이 있다.
1. 인증 정보를 서버에서 관리하지 않기에 토큰이 탈취되는 경우 유효시간 전까지 서버 측에서 무효화 처리하기 힘들다.
2. 세션 ID보다 대략 50배 정도 크기가 크다.
3. 서버가 분리되어 있더라도 토큰만 있다면 인증 및 인가가 가능하기 때문에 확정성이 높다.
4. 토큰에 정보가 담겨있기 때문에 서버의 부담이 적다.
5. 반대로 토큰에 정보가 담겨있기 때문에 탈취당할 경우 개인정보 노출의 위험이 크다.
토큰 기반 인증 정책 설정
앞서 세션 기반 인증과 토큰 기반 인증의 차이점에 대해 알아보았다.
위 내용에서 가장 중요하게 봐야 할 차이점은 토큰을 이용하기 때문에 서버에서 인증 정보를 다루지 않고, 토큰 내에 인증 정보가 포함되어 있다는 점이다. 즉 토큰은 유저를 식별할 수 있는 정보를 가지고 있고, 토큰을 통해 서버에 대한 접근 권한이 생기는 것이기 때문에 공격자로부터 탈취당하거나, 위조된 요청에 대해 정상적으로 작동하지 못하도록 해야한다.
물론 세션 기반 인증도 보안 취약점이 없는 것은 아니다. 쿠키를 활용하기 때문에 사용자 디바이스 자체에서 요청을 변조하는 공격의 경우에는 요청이 정상 수행될 수 있다. 다만 Spring Security에서는 기본적으로 세션 기반의 인증을 지원한다. 따라서 동시 세션 제어, 세션 고정 보호 등의 관련 기능들을 기본 구현으로 제공이 되어 있다.
하지만 토큰 기반의 인증은 직접 보안 취약점을 파악하고 구현해야 한다. 그러기 위해서 어떤 방식으로 토큰을 발급하는지, 발급된 토큰은 어디에 저장하여 관리할 수 있는지 파악을 해보자.
토큰의 발급 및 관리
토큰 기반 인증에서 가장 취약한 점은 서버 내에서 토큰에 대해 무효화를 시킬 수 없다는 점이다. 한번 발급된 토큰은 만료가 되기 전까지 계속해서 사용할 수 있다. 따라서 토큰의 만료 시간을 되도록 짧게 설정하는 것을 권장한다.
하지만 토큰의 만료 시간을 짧게 설정한다면 사용자는 토큰이 만료될 때마다 로그인을 해야하기 때문에 좋지 않은 사용자 경험을 유발할 수 있다. 따라서 토큰의 만료시간을 짧게 가져가면서, 사용자의 로그인 빈도를 낮추기 위해 Refresh Token을 추가로 발급하여 활용하는 경우가 많다.
Refresh Token은 말 그대로 토큰을 다시 발급해주는 토큰을 의미한다. 기존 Access Token의 유효 시간을 짧게 설정하고 토큰이 만료된 경우에는 Refresh Token을 통해 로그인 없이 Access Token을 발급받을 수 있게 하는 것이다.
다만, Refresh Token 역시 탈취당했을 경우 계속해서 Access Token을 발급할 수 있게 된다. 따라서 Refresh Token Rotation 기법을 사용한다. Refresh Token을 활용해 Access Token을 발급할 경우 Refresh Token도 같이 발급해주도록 하고 한번 재발급에 사용된 Refresh Token은 다시 사용할 수 있도록 로테이션을 돌리는 것이다. 이렇게 한다면 한 번 탈취한 Refresh Token으로 무한정 Access Token을 발급할 수 없다. 실제 사용자가 로그인을 하거나 토큰을 재발급 받는 순간 탈취한 토큰은 사용 불가능한 토큰이 되어버리기 때문이다. 필요한 경우에는 Refresh Token이 Access Token과 함께 발급된 토큰인지 확인하는 과정까지 추가한다면 Refresh Token만 탈취한 경우에는 재발급 받지 못하도록 막을 수 있다.
Refresh Token Rotation 기법을 적용하기 위해서는 Refresh Token을 영속하는 과정이 필요하다. 해당 토큰이 발급된 것이 맞는지, 사용되었는지 확인을 해야하기 때문이다. 주로 Key-Value Store In-Memory DB인 Redis를 사용해서 이를 관리하는 것 같다. In-Memory DB라서 속도에도 이점이 있고, 데이터의 유효기간을 지정할 수도 있기 때문이다. 하지만 이번 프로젝트는 단순 예제이기 때문에 따로 Redis를 구축하여 저장하지는 않겠다. (만약 교체할 경우 Repository를 새로 구현하면 될 것이다.)
토큰의 저장
토큰을 저장할 수 있는 공간은 클라이언트 디바이스에 따라 다르겠지만, 웹만 지원하는 서비스라고 가정을 한다면 Local Storage 혹은 Cookie를 통해 토큰을 저장할 수 있다. 두 저장소에 대해서 알아보자.
LocalStorage
- Web Storage의 한 종류로 클라이언트 로컬의 데이터 저장소이다.
- 4KB의 저장공간을 가지는 쿠키에 비해 저장 가능한 용량이 최소 2MB 정도로 크다.
- key-value 형식의 데이터를 저장할 수 있으며 JS코드를 통해 접근할 수 있다.
- HTTP 헤더를 통해 조작할 수 없다.
- 브라우저나 OS를 재시작해도 데이터가 삭제되지 않는다.
- Origin에 따라 스토리지 객체가 생성되며, 동일 Origin에서만 스토리지에 접근할 수 있다.
Cookie
- 웹 브라우저를 통해 기록되는 key-value 형식의 단순 문자열 데이터이다.
- HTTP 응답의 Set-Cookie 헤더를 통해 설정할 수 있다
- 영속 쿠키(Max-Age 혹은 Expires 속성이 포함된 쿠키)의 경우 유효시간이 지나기 전까지 자동으로 요청에 포함된다.
- HttpOnly 속성이 포함된 경우 JS코드를 통해 접근할 수 없다.
- Secure 속성이 포함된 경우 HTTPS 프로토콜로 요청을 보낼 경우에만 포함된다.
- SameSite=Strict 속성이 포함된 경우 같은 Origin에 대한 요청에서만 포함된다.
그리고 웹의 대표적인 공격 기법은 XSS, CSRF 공격이 있다. 간단하게 XSS는 악성 스크립트를 삽입하는 공격 기법이고, CSRF는 사용자 요청을 위조하는 공격을 의미한다. CSRF에 대해선 이전에 CSRF 필터에 대한 포스팅에 설명이 되어있으니 참고를 하면 좋다.
2023.06.09 - [Spring/Security] - [Spring Security] 9. CSRF 필터
[Spring Security] 9. CSRF 필터
CSRF Cross Site Request Forgery의 약자로, 사이트 간 요청 위조를 의미한다. 클라이언트의 요청을 위조하여 공격자가 의도한 행위를 수행하게 하여 공격하는 기법이다. 공격의 과정은 다음과 같다. 1.
cares-log.tistory.com
Local Storage에 토큰을 저장하는 경우에는 XSS 공격에 취약할 수 있다. JS 코드로 접근이 가능하기 때문이다. 하지만 쿠키 역시 XSS 공격에 안전한 것은 아니다. HttpOnly 속성이 적용된 쿠키의 경우에는 토큰 자체를 JS 코드로 접근할 수 없긴 하지만, 토큰에서 사용자의 정보를 추출하는 것이 공격의 목적이 아니라, 요청을 통해 데이터를 조작하는 것이 목적이라면 쿠키는 자동으로 헤더에 포함되기 때문에 요청에 대한 인가 자체는 이루어질 것이다.
CSRF 공격의 경우에는 요청마다 자동으로 헤더에 담기는 쿠키가 더 취약할 것이다. 다만 Same-Site 속성 및 CSRF 토큰을 활용한다면 방지할 수 있다.
XSS 공격: Local Storage와 Cookie 모두 취약하지만 Cookie가 상대적으로 덜 취약하다.
CSRF 공격: Cookie를 사용할 경우 취약하지만 Same-Site, CSRF 토큰을 이용한다면 예방할 수 있다.
결론적으로 정리해보자면 위와 같다. CSRF 공격의 경우에는 예방할 수 있는 방법이 있다는 소리이고, XSS 공격만 본다면 Cookie를 사용하는 것이 더 적합해 보인다. 하지만 결국 모든 보안 위협에 대처하기에는 힘들다. 따라서 두 저장소 외에 토큰을 보관할 수 있는 다른 저장소를 생각해본다면, 단순히 지역 변수에 토큰을 저장해보는 것도 고려할 수 있다.
Access Token을 지역 변수에 저장하는 방법
JS 지역 변수는 같은 블록 내에서만 접근이 가능하기 때문에, XSS로 인해 스크립트가 삽입되었다고 해서 해당 스크립트에서 Access Token을 초기화 한 지역변수에는 접근할 수 없을 것이다. 따라서 단순히 지역 변수에 Access Token을 초기화 한 후 API 요청 마다 공통적으로 Authorization 헤더가 Access Token 값을 담아 요청하도록 설정하는 것이다.
하지만 이 방법은 사용자가 브라우저를 새로고침하거나, 다른 페이지로 가는 등의 페이지 리로드 발생 시에는 지역변수에 저장했기 때문에 Access Token의 값에 접근할 수가 없게 된다. 이 말은 결국 사용자는 페이지 리로드가 발생할 때마다 로그인을 다시 해주어야 한다는 소리다. 하지만 Refresh Token을 활용하면 된다.
토큰의 재발급을 목적으로 발급했던 Refresh Token은 HttpOnly, Secure, Same-Site=Restrict가 적용된 쿠키에 저장한 다음, Access Token이 사라졌거나 만료된 경우에는 로그인 과정 없이 Refresh Token을 통해 토큰을 재발급 시켜주면 되는 것이다.
그림으로 흐름을 살펴보자. (편의 상 Access Token은 AT로, Refresh Token은 RT로 표시한다.)
먼저 로그인 API를 통해서 최초로 인증이 이루어지는 과정이다. 로그인 성공 시 Access Token, Refresh Token을 발급한다. 그리고 Access Token은 응답 Body에, Refresh Token은 HttpOnly, Secure, Same-Site=Restrict 속성이 적용된 쿠키로 설정하여 응답해준다. 이러면 클라이언트는 우선 Refresh Token은 신경쓰지 않아도 된다. 발급 받은 Access Token만 지역 변수에 저장하여 요청을 보내주면 된다.
만약 Access Token이 만료되었거나, 페이지 리로딩으로 인해 Access Token이 누락되었다면 쿠키로 넘어오는 Refresh Token을 검증하여 Access Token과 Refresh Token을 재발급 해주면 된다.
결론
Access Token은 지역 변수에, Refresh Token은 HttpOnly, Secure, Same-Site=Restrict 속성이 적용된 쿠키에 저장하여 관리하며, Refresh Token은 Rotation이 적용되도록 토큰 기반 인증 정책을 정하였다.
이 방식을 통해 사실상 Access Token 자체가 탈취되는 경우는 없을 것이며, 사용자의 정보가 유출되는 경우도 없을 것이다. Refresh Token은 요청마다 쿠키로 보내지게 되는데 HttpOnly 속성을 적용시켰기 때문에 탈취되는 일은 없을 것이다. 다만 악성 스크립트가 요청을 보내 쿠키로 보내지기는 경우 자체는 방지할 수 없을 것이다. 따라서 프론트 측에서도 XSS 공격을 방어하기 위한 처리가 추가된다면 더 적은 보안 취약점을 지닌 애플리케이션을 개발할 수 있을 것이다.