토이 프로젝트를 진행하며 사용자를 인증하는 부분에서 JWT를 사용하였다.
사용자의 인증 정보는 Access Token에서 관리하였고, Access Token의 보안을 위해 Refresh Token을 도입하였다.
이 과정에서 Refresh Token을 관리하기 위해 Redis를 도입하였다.
전체 로직을 도입한 이유를 복습하기 위해 글로 정리해보려 한다.
1. JWT란
Json Web Token의 약어.
서버와 클라이언트 사이에서 통신할 때 권한을 확인하기 위해 사용하는 토큰이다.
Token 인증 과정
1. 클라이언트가 서버에 접속할 경우(로그인), 서버가 해당 클라이언트에게 인증되었다는 의미로 토큰을 부여한다.
2. 토큰을 발급받은 클라이언트는 서버에 또 다른 요청을 보낼 때 요청 헤더에 토큰을 심어서 보낸다.
3. 서버는 해당 토큰과 서버 내의 토큰의 일치 여부를 판별하여 인증 과정을 처리한다.
JWT Token 구성 요소
JWT는 .를 구분자로 나누어지는 세 가지 문자열의 조합이다.
좌측부터 헤더(header), 페이로드(Payload), 서명(Signature) 로 나누어진다.
- 헤더
- JWT에서 사용할 타입과 알고리즘 종류가 담겨있다.
- 페이로드
- 서버에서 첨부한 사용자 권한 정보와 데이터가 담겨있다.
- 페이로드는 노출과 수정이 가능한 지점이기 때문에 최소한의 정보를 담아야한다. (아이디, 비밀번호는 지양하는 것이 좋다.)
- 주로 권한의 범위, 토큰의 발급일과 만료일시 등이 담긴다.
- 서명
- 헤더와 페이로드를 합친 뒤 서버가 지정한 Secret Key로 서명한 뒤, 헤더에 적힌 알고리즘 종류로 해싱하여 암호화한다.
- 헤더와 페이로드는 단순히 인코딩 된 값이기 때문에 외부에서 복호화 할 수 있지만, Signature는 서버 측에서 관리하는 Secret Key가 유출되지 않는 이상 복호화할 수 없다. 따라서 Signature는 토큰의 위변조를 확인하는데에 사용된다.
알기쉬운 예시)
A 유저의 JWT가 A(헤더) + B(페이로드) + C(서명) 이라고 가정했을 때,
B 유저가 해킹을 목적으로 B를 수정할 경우(B*로 표기) JWT는 A + B* + C 가 된다.
수정한 토큰을 서버에 보낼 경우 서버는 토큰의 일치 여부를 판단하기 위해 검증을 한다.
서버는 클라이언트로부터 JWT를 받으면 헤더, 페이로드를 서버의 Secret key값을 이용해 시그니처를 다시 만들고 이를 비교하여 일치했을 경우 통과시킨다.
즉, 서버는 클라이언트의 JWT내의 A + B로 C를 다시 만든 후 요청 때 받은 C와 서버가 만든 C를 비교후 일치해야 통과시키는 것이다.
다시 위로 돌아가서, 만약 A + B* + C 로 위조된 토큰을 받았을 때, 서버는 A + B* 로 C*을 만든다.
C와 C*은 다르므로 서버는 이 요청을 신뢰할 수 없다고 판단한다.
사용자 정보가 들어간다면 JWT는 사용자의 정보 보호가 안되지 않나?
JWT의 목적은 인증이다.
JWT의 목적은 토큰 안에 적혀있는 정보가 아니라 해당 토큰이 신뢰 가능한지 확인하는 것이다.
따라서 위변조에 특화되있다. (이 부분은 위에서 충분히 Signature로 설명하였다.)
페이로드 내에 담기는 정보는 Base 64를 통해 암호화하기 때문에 쉽게 디버거를 사용해 복호화할 수 있다.
따라서 반드시 민감한 정보를 넣어서는 안된다.
JWT 도입 이유
- Header와 Payload를 가지고 Signature를 생성하므로 데이터 위변조를 막을 수 있다.
- 인증 정보에 대한 별도의 저장소가 필요하지 않다.
- 세션과는 달리 무상태성(stateless)을 띈다.
- 모바일에서도 동작 가능하다. (세션은 앱 어플리케이션에서 불가능)
2. Refresh Token 도입 이유
하지만 그럼에도 불구하고 JWT도 탈취의 위험이 있다.
토큰 기반의 단점은 만약 토큰이 탈취되었을 경우, 서버에서 이를 판별할 방법이 없다는 것이다.
현업에서는 이러한 위험성을 방지하기 위해, JWT 방식을 Access Token과 Refresh Token으로 나누어 인증한다.
- Access Token
- 클라이언트가 갖고 있는 실제 유저의 정보가 담긴 토큰으로, 서버는 해당 토큰을 활용하여 인증을 진행
- 탈취의 위험을 방지하기 위해 유효기간을 짧게 설정한다. (30분 ~ 1시간)
- 다만 유효기간이 짧을 경우 클라이언트가 자주 로그인을 해야한다는 단점이 있다.
- 따라서 Refresh Token을 통해 Access Token을 재발급하여 사용자가 매번 로그인을 하지 않도록 한다.
- Refresh Token
- 새로운 Access Token을 발급하기 위한 토큰
- 재발급을 위한 Token 이기 때문에 Access Token 보다는 긴 유효기간을 가진다. (토이 프로젝트에서는 2주로 설정)
- 해당 토큰은 보통 데이터베이스에 기록한다.
Refresh Token 재발급 과정
1. 클라이언트가 서버에 접속할 경우(로그인), DB에서 정보가 일치하는지 확인한다.
2. 정보가 일치하다면 서버가 해당 클라이언트에게 인증되었다는 의미로 Access Token과 Refresh Token을 부여한다.
3. 이 때 Refresh Token은 DB에 저장된다.
4. 클라이언트는 또 다른 요청을 할 때마다 요청 헤더에 Access Token을 심어서 보낸다.
5. 서버에서는 Access Token의 유효성을 검사한 뒤 응답을 내린다.
6. 만약 Access Token의 만료시간이 다 되었을 경우, (하지만 클라이언트는 이 같은 로직들은 알지 못한다.)
7. 클라이언트는 요청헤더에 Access Token을 심어서 보낸다.
8. 서버에서는 유효성을 검사할 때 만료시간이 다 되었다는 것을 확인한다.
9. 서버에서 "토큰이 만료되었으니 Refresh Token을 보내세요" 라고 응답한다.
10. 클라이언트에서 만료된 Access Token과 Refresh Token을 전송한다.
11. Refresh Token의 유효성을 확인한다. (만료기간이 지났는지 확인, 3번에서 DB에 저장했던 토큰과 비교)
12. 유효하다면 Access Token을 재발급해준다.
13. 만약 Refresh Token도 만료기간이 지났다면 다시 로그인을 해줘야 한다.
Refresh Token을 왜 DB에 저장하는데?
Refresh Token은 Access Token에 비해 긴 유효시간을 가진다.
만약 Refresh Token이 탈취될 경우, Access Token과 마찬가지로 서버는 탈취 사실을 판별할 수 없다.
따라서 서버는 탈취 사실을 알지 못한 채 Refresh Token 유효기간이 만료될 때까지 해커에게 계속해서 Access Token을 재발급해줄 수 있다.
따라서 Refresh Token의 보안 문제를 위해 두 가지 방법을 도입했다.
1. Refesh Token Rotation (RTR)
- Access Token 만료 시, Access Token을 재발급 받으며 Refresh Token 또한 함께 갱신하는 것
- Refresh Token을 1회성으로 두어 Refresh Token의 만료기간 또한 짧게 설정하는 방식이다.
- 다만, 해커가 먼저 Refresh Token을 갱신한다면 정상 유저의 기존 Refresh Token이 삭제되었기 때문에 재발급을 받을 수 없다는 문제가 발생하고, 해커는 계속해서 재발급을 받을 수 있을 것이다. (2번으로 넘어가서 해답을 찾았다.)
2. Refesh Token을 유저정보와 함께 DB에 저장한다.
- Key : 유저정보
- Value : Refresh Token
1. RTR의 문제점처럼 해커가 먼저 Access Token을 갱신하며 Refresh Token 또한 갱신하였다고 가정하였을 때, DB에 저장된 Value를 RT* 라고 가정
2. 정상유저가 재발급을 받으려고 할 때, 정상 유저의 Refresh Token(RT라고 가정) 내의 유저정보를 통해 DB의 Key와 비교하여 RT*를 불러온다.
3. 서버는 정상유저의 RT와 RT*가 다른 것을 판단하고 해당 유저에 대한 악의적인 침투가 있었음을 알 수 있게 된다.
4. Redis 내의 데이터를 삭제하고, 해당 유저는 재로그인을 하도록 클라이언트를 리턴한다.
3. Redis란
디스크가 아닌 메모리에 데이터를 저장하는 In-Memory 방식의 데이터베이스
- RAM에 데이터를 저장하는 In-Memory 방식의 Redis의 읽기 및 쓰기 연산은 기존 디스크 기반 데이터베이스보다 빠르다.
- 휘발성 메모리
- TTL을 통해 데이터 만료일을 지정할 수 있다.
Refresh Token을 Redis에 저장한 이유
- Refresh Token은 유효기간이 있다. Redis의 경우 TTL을 통해 데이터 만료일을 지정할 수 있는데, Refresh Token의 유효기간과 똑같이 설정할 경우 토큰이 만료되면 Redis에서도 데이터가 삭제되도록 하여 데이터를 효율적으로 관리할 수 있다.
- 대체로 30분~1시간 간격으로 Access Token은 만료되기 때문에, Access Token의 재발급을 위해 Refresh Token은 자주 조회될 수밖에 없다. 이렇듯 호출의 빈도가 높기 때문에 기존 디스크 기반 데이터베이스보다 읽기 및 쓰기 연산이 빠른 In-Memory 방식의 Redis가 적합하다고 생각하였다.
+)
사실 이 이후에도 refresh Token을 DB에 저장하는게 맞는 것인지에 대한 고민이 든다. (HTTP 통신은 statless 한 구조여야 한다고 생각하는데, DB에 저장한다면 이를 위반하는게 아닌가 싶어서..)
이 부분은 조금 더 고민해봐야 할 것 같다.
참고
https://velog.io/@hahan/JWT%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80
JWT란 무엇인가?
JWT(Json Web Token) > 정보를 비밀리에 전달하거나 인증할 때 주로 사용하는 토큰으로, Json객체를 이용함 JWT는 Json Web Token의 약자로 일반적으로 클라이언트와 서버 사이에서 통신할 때 권한을 위해
velog.io
access token과 refresh token의 작동 방식
프론트회원가입회원가입 한 아이디, 패스워드로 로그인서버에서 자동으로 cookie에 액세스 토큰, 리프레시 토큰 저장.인증 필요할 때만 end point로 인증 요청쿠키에 액세스, 리프레시 토큰 다 들어
velog.io
'DB' 카테고리의 다른 글
[JPA] 동시성 이슈 해결 - 트랜잭션 격리 수준과 낙관적 락 (0) | 2024.04.30 |
---|---|
[JPA] N+1 문제와 해결법 (0) | 2024.04.24 |
[JPA] 다대다(N:M) 관계를 지양하는 이유와 해결법 (0) | 2024.04.20 |
SQL 기본 문법 정리 (0) | 2023.06.21 |