[Server] 토큰 기반 인증 VS 서버(세션) 기반 인증
기존의 시스템에서는 서버 기반의 인증방식을 사용하였다. 하지만 시스템의 규모가 커짐에 따라 서버 기반의 인증 방식은 한계점을 보이기 시작하였고, 토큰 기반의 인증 방식이 등장하게 되었
mangkyu.tistory.com
세션과 JWT의 차이점
- 저장 위치: 세션은 서버측에서 유저의 인증 정보를 저장하고, JWT는 클라이언트 측에서 저장됩니다.
- 상태의 유지: 세션은 서버측에서 유저 상태를 유지합니다. 반면 JWT는 유저 상태를 유지하지 않습니다.
- 확장성: 세션은 확장성이 떨어집니다. 서버 부하가 많아지면, 세션 관리를 위해 추가 서버가 필요해질 수 있습니다. 하지만 JWT는 클라이언트에서 관리되므로 서버 부하를 줄일 수 있습니다.
- 보안: JWT는 암호화된 토큰을 사용하여 인증 정보를 전송합니다. 반면, 세션은 쿠키를 사용합니다. 이로 인해 CSRF(Cross-Site Request Forgery)와 같은 보안 문제가 발생할 가능성이 있습니다.
- Scale-out: 세션은 분산 환경에서 유저 상태를 유지하는 것이 어렵습니다. 이에 반해 JWT는 분산 환경에서도 유저 상태를 유지하는데 어려움이 없습니다.
JWT를 사용하는 이유
JWT는 세션과 비교했을 때 여러 가지 이점이 있습니다.
- 확장성: JWT는 클라이언트 측에서 인증 정보를 저장하므로 서버 부하를 줄일 수 있습니다.
- 무상태(Stateless): JWT는 클라이언트 측에서 관리되므로 서버측에서 상태를 유지할 필요가 없습니다.
- 분산 환경: JWT는 클라이언트측에서 관리되므로 분산 환경에서도 쉽게 사용할 수 있습니다.
- 보안: JWT는 암호화된 토큰을 사용하여 인증 정보를 전송하기 때문에 CSRF와 같은 보안 문제를 방지할 수 있습니다.
- 유연성: JWT는 JSON 형식을 사용하기 때문에 다양한 클라이언트와 서버 언어에서 쉽게 사용할 수 있습니다.
JWT의 문제점
사용이 쉽고, 서버의 개발 부담을 덜어줄 수 있다는 여러가지 장점을 가지고 있으나, 그만큼 또 단점도 가지고 있습니다.
1. 토큰 크기의 증가
JWT는 일반적으로 대칭키 또는 공개키/개인키 쌍을 사용하여 서명됩니다. 이 서명을 통해 클라이언트에서 서버로 전송되는 JWT의 무결성을 보장할 수 있습니다. 그러나 이 서명은 JWT의 크기를 더욱 크게 만듭니다. 이로 인해 네트워크 대역폭이 증가하고, 저장소 공간이 더 필요하게 됩니다.
Claim에 넣는 데이터가 많아질수록, JWT 토큰의 길이가 길어집니다. API 호출등에 사용할 시에, 매 호출마다 헤더에 붙어서 가야하기 때문에, 길이가 길다는 것은 그만큼 네트워크 대역폭 낭비가 심하다는 의미입니다.
2. 세션 관리
JWT는 토큰 기반 인증 방식이므로 서버에 대한 세션 상태를 유지하지 않습니다. 이는 로그인한 사용자에 대한 정보를 저장하고 새로 고침에 대한 세션 상태를 유지하는 세션 기반 인증 방식과는 다릅니다. 따라서 JWT를 사용하는 경우, 세션 상태를 처리하는 추가 작업이 필요합니다.
3. 보안 문제
JWT는 서명된 토큰을 사용하여 인증 정보를 전송합니다. 그러나 JWT의 서명을 유지하려면 서버에서 사용되는 비밀 키를 보호해야 합니다. 이 비밀 키가 노출되면 공격자는 JWT의 서명을 조작하거나 유효하지 않은 JWT를 생성할 수 있습니다.
JWT는 기본적으로 Claim에 대한 정보를 암호화하지 않는다. 단순히 BASE64로 인코딩만 하기 때문에, 중간에 패킷을 가로채거나, 기타 방법으로 토큰을 취득했으면 토큰 내부 정보를 통해서 사용자 정보가 누출 될 수 있는 가능성이 있습니다. 특히 자바스크립트 기반의 웹 클라이언트의 경우 브라우저상의 디버거등을 통해서 토큰이 노출될 가능성이 높습니다. 그래서, 이를 보완하는 방법으로는 토큰 자체를 암호화하는 방법이 잇습니다. JSON을 암호화하기 위한 스펙으로는 JWE(JSON Web Encryption)이 있습니다.
4. 만료 처리
JWT에는 만료 시간이 있습니다. 그러나 만료된 JWT는 여전히 서명됩니다. 만료된 JWT를 사용하면 공격자가 인증을 우회하고 사용자 권한을 얻을 수 있습니다. 따라서 만료된 JWT를 처리하는 추가 작업이 필요합니다.
한번 발급된 토큰은 값을 수정하거나 폐기가 불가능하다.
JWT는 토큰 내에 모든 정보를 다 가지고 있기 때문에, 한번 발급된 토큰에 대한 변경은 서버에서는 더이상 불가능합니다. 예를 들어 토큰을 잘못 발행해서 삭제하고 싶더라도, Sinagture만 맞으면 맞는 토큰으로 인식을 하기 때문에, 서버에는 한번 발급된 토큰의 정보를 바꾸는 일등이 불가능합니다.
그래서 만약에 JWT를 쓴다면, Expire time을 꼭 명시적으로 두도록하고, refresh token등을 이용해서, 중간중간 토큰을 재발행하도록 해야합니다.
5. 취약한 클라이언트 저장소
JWT는 클라이언트 측에 저장되므로 공격자가 클라이언트의 저장소를 액세스하면 JWT를 훔칠 수 있습니다. 이는 보안상 위험할 수 있습니다.
6. 토큰 자체의 안전성
JWT는 클라이언트 측에 저장되므로 공격자가 토큰 자체를 훔치는 것이 가능합니다. 이는 토큰이 탈취되어 인증 정보가 노출될 수 있으며, 이를 방지하기 위해 추가 보안 조치가 필요하다.
Self-contained: 토큰 자체에 정보를 담고 있으므로 양날의 검이 될 수 있다.
JWT는 Claim 기반이라는 방식을 사용하는데, Claim이라는 사용자에 대한 프로퍼티나 속성을 이야기 한다. 토큰자체가 정보를 가지고 있는 방식인데, JWT는 이 Claim을 JSON을 이용해서 정의한다.
다음은 Claim을 JSON으로 서술한 예이다.이 JSON 자체를 토큰으로 사용하는 것이 Claim 기반의 토큰 방식이다.
{
"id":"uhanuu",
"role":["admin","user"],
"company":"gwnu"
}
이 토큰을 이용해서 요청을 받는 서버나 서비스 입장에서는 이 서비스를 호출한 사용자에 대한 추가 정보는 이미 토큰 안에 다 들어가 있기 때문에 다른 곳에서 가져올 필요가 없다.
"사용자 관리"라는 API 서비스가 있다고 가정해보면, (이 API는 "관리자(admin)" 권한을 가지고 있는 사용자만이 접근이 가능하며, "관리자" 권한을 가지고 있는 사용자는 그 관리자가 속해 있는 "회사(company)"의 사용자 정보만 관리할 수 있습니다.)라고 정의해보면, 이 시나리오에 대해서 일반적인 스트링 기반의 토큰과 JWT와 같은 Claim 기반의 토큰이 어떤 차이를 가질 수 있는지 알아보면 다음과 같다.
OAuth 토큰의 경우
1. API 클라이언트가 Authorization Server(토큰 발급서버)로 토큰을 요청합니다. 이때, 토큰 발급을 요청하는 사용자의 계정과 비밀번호를 넘기고, 이와 함께 토큰의 권한(용도)를 요청합니다. 여기서는 일반 사용자 권한(enduser)과 관리자 권한(admin)을 같이 요청하였습니다.
2. 토큰 생성 요청을 받은 Authorization Server는 사용자 계정을 확인한 후, 이 사용자에게 요청된 권한을 부여해도 되는지 계정 시스템 등에 물어본 후, 사용자에게 해당 토큰을 발급이 가능하면 토큰을 발급하고, 토큰에 대한 정보를 내부(토큰 저장소)에 저장해 둡니다.
3. 이렇게 생성된 토큰은 API 클라이언트로 저장이 됩니다.
4. API 클라이언트는 API를 호출할때 이 토큰을 이용해서 Resource Server(API 서버)에 있는 API를 호출합니다.
5. 이때 호출되는 API는 관리자 권한을 가지고 있어야 사용할 수 있기 때문에, Resource Server가 토큰 저장소에서 토큰에 관련된 사용자 계정, 권한 등의 정보를 가지고 옵니다. 이 토큰에 (관리자)admin 권한이 부여되어 있기 때문에, API 호출을 허용합니다. 위에 정의한 시나리오에서는 그 사용자가 속한 "회사"의 사용자 정보만 조회할 수 있습니다. 라는 전제 조건을 가지고 있기 때문에 API 서버는 추가로 사용자 데이터베이스에서 이 사용자가 속한 "회사" 정보를 찾아와야 합니다.
6. API 서버는 응답을 보냅니다.
JWT와 같은 Claim 기반의 토큰 흐름
1. 토큰을 생성 요청하는 방식은 동일합니다. 마찬가지로 사용자를 인증한 다음에 토큰을 생성합니다.
2. 다른 점은 생성된 토큰에 관련된 정보를 별도로 저장하지 않는다는 것입니다. 토큰에 연관되는 사용자 정보나 권한 등을 토큰 자체에 넣어서 저장합니다.
3. API를 호출하는 방식도 동일합니다.
4. Resource Server (API 서버)는 토큰 내에 들어있는 사용자 정보를 가지고 권한 인가 처리를 하고 결과를 리턴합니다.
결과적으로 차이점은 토큰을 생성하는 단계에서는 생성된 토큰을 별도로 서버에서 유지할 필요가 없으며 토큰을 사용하는 API 서버 입장에서는 API 요청을 검증하기 위해서 토큰을 가지고 사용자 정보를 별도로 계정 시스템 등에서 조회할 필요가 없습니다.
JWT 란?
JWT(Json Web Token)란 Json 포맷을 이용하여 사용자에 대한 속성을 저장하는 Claim 기반의 Web Token이다. JWT는 토큰 자체를 정보로 사용하는 Self-Contained 방식으로 정보를 안전하게 전달한다.
애플리케이션이 실행될 때, JWT를 static 변수와 로컬 스토리지에 저장하게 된다. static 변수에 저장되는 이유는 HTTP 통신을 할 때마다 JWT를 HTTP 헤더에 담아서 보내야 하는데, 이를 로컬 스토리지에서 계속 불러오면 오버헤드가 발생하기 때문이다. 클라이언트에서 JWT를 포함해 요청을 보내면 서버는 허가된 JWT인지를 검사한다. 또한 로그아웃을 할 경우 로컬 스토리지에 저장된 JWT 데이터를 제거한다. (실제 서비스의 경우에는 로그아웃 시, 사용했던 토큰을 blacklist라는 DB 테이블에 넣어 해당 토큰의 접근을 막는 작업을 해주어야 한다.)
JWT 구조
JWT는 Header, Payload, Signature 3개의 부분으로 구성되어 있다.
Json 형태인 각 부분은 Base64Url로 인코딩 되어 표현된다. 또한 각각의 부분을 이어 주기 위해 . 구분자를 사용하여 구분한다. 추가로 Base64Url는 암호화된 문자열이 아니고, 같은 문자열에 대해 항상 같은 인코딩 문자열을 반환한다.
https://jwt.io/
1. Header(헤더)
토큰의 헤더는 typ과 alg 두 가지 정보로 구성된다. alg는 헤더(Header)를 암호화 하는 것이 아니고, Signature를 해싱하기 위한 알고리즘을 지정하는 것이다.
- typ: 토큰의 타입을 지정 ex) JWT
- alg: 알고리즘 방식을 지정하며, 서명(Signature) 및 토큰 검증에 사용 ex) HS256(SHA256) 또는 RSA
{
"alg": "HS256",
"typ": JWT
}
2. PayLoad(페이로드)
토큰의 페이로드에는 토큰에서 사용할 정보의 조각들인 클레임(Claim)이 담겨 있다.
클레임은 총 3가지로 나누어지며, Json(Key/Value) 형태로 다수의 정보를 넣을 수 있다.
io.jsonwebtoken 패키지에서 Claims를 구현한 것을 보자
public interface Claims extends Map<String, Object>, ClaimsMutator<Claims> {
/** JWT {@code Issuer} claims parameter name: <code>"iss"</code> */
public static final String ISSUER = "iss";
/** JWT {@code Subject} claims parameter name: <code>"sub"</code> */
public static final String SUBJECT = "sub";
/** JWT {@code Audience} claims parameter name: <code>"aud"</code> */
public static final String AUDIENCE = "aud";
/** JWT {@code Expiration} claims parameter name: <code>"exp"</code> */
public static final String EXPIRATION = "exp";
/** JWT {@code Not Before} claims parameter name: <code>"nbf"</code> */
public static final String NOT_BEFORE = "nbf";
/** JWT {@code Issued At} claims parameter name: <code>"iat"</code> */
public static final String ISSUED_AT = "iat";
/** JWT {@code JWT ID} claims parameter name: <code>"jti"</code> */
public static final String ID = "jti";
/**
* Returns the JWT
* <code>iss</code></a> (issuer) value or {@code null} if not present.
*
* @return the JWT {@code iss} value or {@code null} if not present.
*/
2.1 등록된 클레임(Registered Claim)
등록된 클레임은 토큰 정보를 표현하기 위해 이미 정해진 종류의 데이터들로, 모두 선택적으로 작성이 가능하며 사용할 것을 권장한다. 또한 JWT를 간결하게 하기 위해 key는 모두 길이 3의 String이다. 여기서 subject로는 unique한 값을 사용하는데, 사용자 이메일을 주로 사용한다.
- iss: 토큰 발급자(issuer)
- sub: 토큰 제목(subject)
- aud: 토큰 대상자(audience)
- exp: 토큰 만료 시간(expiration), NumericDate 형식으로 되어 있어야 함 ex) 1480849147370
- nbf: 토큰 활성 날짜(not before), 이 날이 지나기 전의 토큰은 활성화되지 않음
- iat: 토큰 발급 시간(issued at), 토큰 발급 이후의 경과 시간을 알 수 있음
- jti: JWT 토큰 식별자(JWT ID), 중복 방지를 위해 사용하며, 일회용 토큰(Access Token) 등에 사용
2.2 공개 클레임(Public Claim)
공개 클레임은 사용자 정의 클레임으로, 공개용 정보를 위해 사용된다. 충돌 방지를 위해 URI 포맷을 이용하며, 예시는 아래와 같다.
{
"https://mangkyu.tistory.com": true
}
2.3 비공개 클레임(Private Claim)
비공개 클레임은 사용자 정의 클레임으로, 서버와 클라이언트 사이에 임의로 지정한 정보를 저장한다. 아래의 예시와 같다.
{
"token_type": access
}
3. Signature(서명)
서명(Signature)은 토큰을 인코딩하거나 유효성 검증을 할 때 사용하는 고유한 암호화 코드이다. 서명(Signature)은 위에서 만든 헤더(Header)와 페이로드(Payload)의 값을 각각 BASE64Url로 인코딩하고, 인코딩한 값을 비밀 키를 이용해 헤더(Header)에서 정의한 알고리즘으로 해싱을 하고, 이 값을 다시 BASE64Url로 인코딩하여 생성한다.
생성된 토큰은 HTTP 통신을 할 때 Authorization이라는 key의 value로 사용된다.
HTTP 헤더부분에 Authorization : Bearer "${토큰값}" 있을 때만 글쓸 수 있게 인증 인가 구현 (유효한 토큰만)
{
"Authorization": "Bearer {생성된 토큰 값}",
}
[Server] JWT(Json Web Token)란?
현대 웹서비스에서는 토큰을 사용하여 사용자들의 인증 작업을 처리하는 것이 가장 좋은 방법이다. 이번에는 토큰 기반의 인증 시스템에서 주로 사용하는 JWT(Json Web Token)에 대해 알아보도록 하
mangkyu.tistory.com
[정보보안] REST JWT(JSON Web Token) 이란?
JWT(JSON Web Token)을 이용한 API 인증 개인 학습 목적으로 원본블로그로부터 그대로 옮긴 포스팅입니다. 원본 출처는 조대협의 블로그 : http://bcho.tistory.com/999, http://bcho.tistory.com/1000 REST API에 대한 보
12bme.tistory.com
'Web' 카테고리의 다른 글
OAuth2(Open Authorization) (0) | 2023.04.08 |
---|---|
Redis(Remote Dictionary Server) (0) | 2023.03.30 |
HTTP 메소드 (0) | 2022.12.29 |
상태 코드(2) (0) | 2022.12.29 |
상태 코드(1) (0) | 2022.12.29 |