0. 들어가기 전
해당 포스트를 보고 오길 바란다!
1. JWT란?
드디어 드디어 JWT가 무엇인지 본론으로 들어왔다...
(서론이 너무 길었다..ㅠㅠ)
해당 사이트에 들어가보면, JWT에 대한 설명이 나와 있다.
- JWT로 주고받는 정보는 디지털 서명이 되어있으므로 확인하고 신뢰할 수 있다.
- JWT는 HMAC or RSA or ECDSA 알고리즘을 사용한다.
- JWT는 정보를 암호화하여 주고받을 수 있지만, 서명된 토큰에 중점을 둘 것이다.
2. JWT의 구조
Encoded 된 코드를 보면서 다음과 같은 형식을 띈다.
xxxxx.yyyyy.zzzzz
해당 코드를 Decoded해보면 세 구조로 나눠볼 수 있다.
앞에서부터 Header, Payload, Signature 에 해당한다.
1) HEADER
{
"alg": "HS256",
"typ": "JWT"
}
- 어떤 알고리즘으로 암호화 & 토큰의 타입
- JSON은 Base64Url로 인코딩 되어 있다!
- 사실 중요하지 않다. "헤더"라는 게 있구나!
2) PAYLOAD
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022,
"userID": "12" // 개인 클레임
}
- Registered Claims (등록된 클레임)
- 특정한 의미를 가진 클레임으로, 표준적으로 정의된 클레임
- 예시: iss (발급자), sub (주체), aud (대상자), exp (만료 시간), iat (발급 시간), nbf (Not Before), jti (JWT ID) 등.
- Public Claims (공개 클레임)
- 사용하는 사람들이 원하는대로 정의 가능
- 충돌을 방지하기 위해 URI 형식을 따르는 것이 좋다.
- URI 형식 예시 :"https://example.com/claim"
- Private Claims (개인 클레임)
- 클라이언트와 서버 간에 합의, 정보를 공유하기 위해 생성된 맞춤 클레임 (Key : Value)
3) SIGNATURE
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
your-256-bit-secret
)
- Base64Url로 인코딩 된 header, payload, 시크릿 키를 가지고 HMACSHA256 알고리즘으로 암호화를 한 것 (서명)
- HMAC : secret_key 를 사용하여 토큰의 서명을 생성 -> 무결성 보장
- SHA256 : 256비트 길이의 해시 함수
3. JWT 생성 / 검증 방식
1) JWT 생성
2) JWT 검증
서버 입장에서는 사실 해당 payload의 정보를 모두 알고 싶은게 아니라, "JWT 토큰이 유효한 토큰인지" 를 알고 싶은 것이다.
이전 포스팅을 보면, Load Balancing을 할 때 나왔던 세션의 단점.
서버마다 세션 저장소를 둔다거나 스티키 세션을 사용한다거나
이러한 번거러운 해결책을 사용하지 않아도,
모든 서버가 🔑 secret_key 만 알고 있으면 인증을 마칠 수 있다!!!!!!
4. JWT의 단점
한 발자국만 더 생각해보자!
JWT는 발급된 후에는 내용을 변경할 수 없으며, 유효기간이 만료될 때까지 계속해서 사용할 수 있다.
따라서 토큰을 탈취당하면 대처하기 어렵다.
토큰은 한 번 발급되면 유효기간이 만료될 때 까지 계속 사용이 가능하기 때문!
즉, JWT 또한 암호화를 했다고는 하지만,
쿠키와 마찬가지로 탈취시에 문제가 있으며 로그아웃 처리가 쉽지 않다.
이를 해결하기 위해 추가적인 보안전략을 고려해야한다.
5. 추가적인 보안전략
첫 번째로 아주 쉽게 생각할 수 있는 것은 만료기간을 매우 짧게 설정하는 것이다!
토큰이 탈취되더라도 빠르게 만료되기 때문에 피해를 최소화할 수 있다.
그러나 사용자가 자주 재로그인해야 하는 불편함이 있다.
이 불편함만 해결한다면 우리가 고려했던 보안 문제를 대부분 해결할 수 있을 것 같다!
Refresh Token 사용하기
- Access Token을 짧은 주기로 설정한다면,
반대로 Refresh Token은 주로 장기적인 인증을 위해 사용된다. - 결론부터 말하면, client의 로그인 요청 시
서버는 Access Token과 함께 그보다 긴 만료 기간을 가진 Refresh Token을 발급하는 전략이다.
- 일반적으로, JWT에는 유효기간이 설정되어 있어서 만료되면 서버에 새로운 토큰을 요청해야 한다.
- 하지만 이 과정에서 인증이 필요한 사용자는 잠시 서비스를 이용할 수 없게 될 수도 있다.
- 이를 방지하기 위해 Refresh Token을 사용합니다.
(호텔에서 방 키(Access Token)를 잃어버려도 마치 신분증(Refresh Token)을 보여주면, 다시 방 키를 줄 수 있는 원리이다.)
- 만약 Access Token이 만료되면, 클라이언트는 Refresh Token을 사용하여 새로운 엑세스 토큰을 발급받을 수 있다.
- 이를 통해 사용자는 서비스 이용 중에도 로그인 상태를 유지할 수 있다.
- 자, 다시 보안의 딜레마에 빠졌다...
- 토큰 기반 인증방식에서 토큰은 Stateless하다.
- 서버가 토큰의 상태를 보관하지 않고 있기 때문에 한 번 발급한 토큰에 대해서 제어권을 가지고 있지 않다는 뜻이다.
- Refresh Token을 탈취당한다면, 오히려 몇주~몇달 (만료 기간) 동안 Access Token을 무제한 발급받을 수 있다는 뜻...
- 각각 어디에 저장해야 될까..? (보안전략)
6. 그렇다면, Token은 어디에 저장해야해???
- BackEnd라면 6-2 만 참고
6-1. Access Token (Front-End 고려)
서버가 발급해 준 Access Token은 클라이언트(웹 브라우저, 모바일 앱 등)에 저장한다고 했다.
보통 Local Stroage, Session Storage, Cookie 중에 저장한다.
유효 기간이 짧고, 민감한 정보를 담고 있지 않기 때문에
메모리에 저장되어도 보안상 큰 문제가 발생하지 않는다.
CSRF(Cross-Site Request Forgery) 공격을 막기 위해 SameSite 속성을 설정하는 것이 좋을 것이다.
로컬 스토리지와 세션 스토리지의 차이점은 '영구적이냐 아니냐' 정도이다.
로컬 스토리지에 저장된 데이터는 사용자가 지우지 않는 한 남아있지만,
세션 스토리지의 데이터는 새로고침을 하거나 브라우저 탭을 닫을 경우에는 제거된다.
따라서, 지속적으로 필요한 데이터(예: 자동 로그인)는 로컬 스토리지에 저장하고,
잠깐 필요한 정보(예: 일회성 로그인)는 세션 스토리지에 저장하는 것이 일반적이다.
하지만 이 두 스토리지는 치명적인 단점이 있는데, 바로 XSS(Cross Site Scripting) 공격에 취약하다는 것이다.
따라서, 스토리지에 민감한 정보를 저장하는 것은 안전하지 않다..
- Cookie
쿠키 또한 자바스크립트로 접근이 가능하지만, HTTP ONLY 옵션을 설정하여 자바스크립트로 접근하는 것을 방지할 수 있다.
하지만 쿠키에 토큰을 담으면 CSRF(Cross-Site Request Forgery) 공격에 취약해진다.
XSS 공격은 토큰의 값을 가져오지만, CSRF 공격은 로그인된 상태에서 특정 동작을 요청하게 만든다.
이는 CSRF 방어를 실시하여 어느 정도 상쇄할 수 있다.
https://jinhos-devlog.tistory.com/entry/Spring-Security-%EC%9B%B9-%EB%B3%B4%EC%95%88-%EC%9D%B4%ED%95%B4-JWT%EB%A5%BC-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0-%EC%A0%84#5.%20CORS%EC%97%90%20%EB%8C%80%ED%95%98%EC%97%AC-1
5. CORS에 대하여 참고
즉, CSRF는 쿠키에 저장된 토큰의 값을 직접적으로 가져오지는 않기 때문에 쿠키에 토큰을 저장하는 것이 합리적입니다.
6-2. Refresh Token
Refresh Token 은 보통 매우 긴 시간을 설정하기 때문에 보안적으로 매우 중요하다. 따라서 서버에서 관리하여야 한다.
과연 어디에 저장해야 잘 저장했다고 소문이 날까..?
1) DB
가장 생각하기 좋은 방법은 DB에 저장하는 방법.
그러나, 이는 추가적인 I/O 작업이 발생함을 뜻한다.
빠른 인증 처리라는 JWT의 장점을 상쇄시키는 꼴...
2) Redis
https://jinhos-devlog.tistory.com/entry/Database-Redis%EB%9E%80
결론만 이야기하자면, Redis에 저장하는 방식이 일반적이다.
- 메모리 기반이며, 캐싱 기능을 제공하기 때문에 "빠르다!"
- 분산 서버 환경에서 "확장성이 좋다!"
- 데이터의 만료를 손쉽게 관리할 수 있는 기능을 제공한다.
따라서, "Refresh Token의 만료 기간을 유연하게 관리할 수 있다!"
결론
가장 좋은 방식은