JWT를 알기 전에 Cookie와 Session에 대해서 먼저 알아보자.
자세한 설명은 아래 링크에서 확인해주세요.
Cookie
간단히 설명하자면 클라이언트가 웹사이트에 접속할 때 그 사이트가 사용하게 되는 일련의 작은 기록 파일이라고 생각하면 된다.
서버가 클라이언트에 정보를 전달할 때 저장하고자 하는 정보를 응답 헤더(Cookie)에 저장하여 전달한다.
Key-Value 형시의 문자열 형태로 저장한다.
쿠키를 사용하는 이유
그러나 쿠키는 아래와 같은 단점들 때문에 쿠키만을 온전히 인증에 사용하지 않는다.
쿠키의 단점
무언가에 대한 특정 인증 정보를 서바가 가지고 있고 그 값을 클라이언트에게 전달하여 마치 키를 주고 자물쇠를 여는 방식으로 인증을 한다. 세션 ID를 특정 저장소에 저장하여 사용한다.
쿠키라는 정보 자체를 주고 받는다는 사실을 다르지 않지만 민감 정보를 다뤄야 하는 로그인과 같은 상황에서의 보완점을 찾기 위해 나온 것이 Session이다.
개인 민감 정보를 그대로 노출했을 때의 단점을 막아내기 위해서 나온 것이 바로 Session이다.
쿠키와 같은 매개체를 주고받음으로써 매번 로그인 시 ID,PW를 작성해서 전달해야 한다
에 대한 단점을 보완하는 것이 기본 틀이고, 여기서 ID,PW를 주고받는 것이 아닌 인증 정보 자체를 특정 세션 저장소에 저장하고, 이 값을 쿠키에 담아 클라이언트가 쿠키를 요청할 때마다 세션 저장소에 있는 정보랑 동일한지 확인하는 것이 핵심이다.
일련의 과정
- 클라이언트가 ID,PW로 서버에 로그인 요청을 한다.
- ID,PW로 인증 후 사용자를 식별할 고유한 세션 ID를 만들어서 자물쇠처럼 세션 저장소에 저장한다.
- 세션 ID를 특정한 형태(Cookie or JSON)로 클라이언트에 반환한다.
- 사용자 인증이 필요한 정보를 요청할 떄마다 세션 ID를 쿠키에 담아 서버에 전달한다.
- 인증이 필요한 API의 경우, 서버는 세션 ID가 세션 저장소에 있는지 확인한다.
- 세션 ID가 세션 저장소에 있다면, 인증 완료 후 API 처리, 없다면 401 에러를 반환한다.
쿠키만을 전송할 때와는 다르게 세션 저장소에 식별 가능한 값을 넣어두고 그 값이 일치했는지 아닌지만 보면 인증이 되니 보완이 조금 더 올라간다.
만약 보안에 문제가 발생한 경우
하지만
위의 장점이 곧 단점이다.
쿠키와 마찬가지로 세션을 사용했을 때의 단점
세션을 사용하면서 단점으로 꼽히는 것은 요청을 진행할 때마다 세션 저장소에 세션 ID를 조회하는 작업을 통해서 DB 접근이라는 로직이 한 번 더 수행된다는 것이다.
이런 과정에서 등장한 것이 JWT이다.
JWT(Json Web Token)은 위와 같은 일련의 과정 속에서 나타난 하나의 인터넷 표준 인증 방식이다. 말 그대로 인증에 필요한 정보들을 Token에 담아 암호화시켜 사용하는 토큰인 것이다.
따라서 기본적인 인증을 진행하는 구조는 Cookie와 크게 다르지는 않다. 다만, 강조되는 점은 JWT는 서명된 토큰이라는 점이다. 공개/개인 키를 쌍으로 사용하여 토큰에 서명할 경우 서명된 토큰은 개인 키를 보유한 서버가 이 서명된 토큰이 정상적인 토큰인지 인증할 수 있다는 것이다.
이러한 JWT의 구조 때문에 인증 정보를 담아 안전하게 인증을 시도하게끔 전달할 수 있는 것이다.
JWT의 구조
JWT는 각각의 구성요소가 점(.)으로 구분이 되어있고 구성 요소는 3개로 나뉜다.
각 구성요소에 대해서 살펴보자.
{
"typ": "JWT",
"alg": "HS512"
}
Header에는 보통 토큰의 타입이나, 서명 생성에 어떤 알고리즘이 사용되었는지 저장한다.
위와 같은 경우에는 타입이 JWT
이고, HS512
알고리즘이 적용되어 암호화가 되어있다.
{
"sub": "1",
"iss": "ori",
"exp": 1636989718
"iat": 1636987918
}
payload에는 보통 Claim이라는 사용자에 대한, 혹은 사용자에 대한 속성을 key-value의 형태로 저장한다. Claim이라는 말 그대로 토큰에서 사용할 정보의 조각인 것이다.
Claim 값은 개발자가 원하는대로 설정할 수 있지만, JWT의 표준 스펙이 존재한다.
JWT 표준스펙
표준 스펙에서 key는 3글자로 정의되어 있다.
- iss(Issuer): 토큰 발급자
- sub(Subject): 토큰 제목 - 토큰에서 사용자에 대한 식별 값이 된다.
- aud(Audience): 토큰 대상자
- exp(Expiration Time): 토큰 만료 시간
- nbf(Not Before): 토큰 활성 날짜(이 날짜 이전의 토큰은 활성화되지 않음을 보장)
- iat(Issued At): 토큰 발급 시간
- jti(JWT Id): JWT 토큰 식별자(issuer가 여러 명일 때 이를 구분하기 위한 값)
이러한 표준 스펙으로 정의되어있는 Claim 스펙이 존재하지만, 7가지픞 모두 포함해야 하는 것이 아니고 상황에 다라 서버가 가져야 할 인증 체계에 따라 사용하면 된다.
물론 위에서 언급한 것처럼 표준 스펙 외에도 필요한 것을 추가해도 된다.
예를 들어 Access Token, Refresh Token을 구분하기 위해 "token_type"라는 Claim을 만들 수 있다.
주의!
중요한 것은 payload에 민감한 정보를 담지 않는 것이다. header와 payload는 json으로 디코딩되어 있고 특별한 암호화가 되어 있는 것이 아니기 때문에 누구나 'https://jwt.io' 와 같은 사이트에서 디코딩을 한다면 header, payload에 담긴 값을 알 수 있다.
때문에 header, payload에는 단순히 식별하기 위한 정보만을 담아두어야 한다.
JWT의 구성요소에서 가장 중요한 것은 Signature(서명)이다. 서명은 암호화가 되어있기 때문에 구조에 대한 이미지를 볼 수 있다.
지금까지 Header, Payload는 인코딩 되어있던 값들을 JWT에 담겨있는 것처럼 디코딩된 상태였다.
header, payload를 디코딩한 값을 위 사진처럼 합치고 이를 your-256-bit-secret. 즉, 서버가 가지고 있는 개인키를 가지고 암호화되어잇는 상태이다.
따라서 signature는 서버에 있는 개인키로만 암호화를 풀 수 있으므로 다른 클라이언트는 임의로 signature를 복호화할 수 없다.
복호화 과정
만약 클라이언트가 payload에 담긴 식별자가 변조된 JWT로 요청을 하더라도 서버가 애초에 발급했던 Signature 안의 payload와 다르기 때문에 인증이 불가능해진다.
장점
쿠키와 세션의 단점을 해결하기 위해 만들어진 것이 JWT인 것이기 때문에 쿠키와 세션의 단점을 뒤집으면 그것이 곧 JWT의 장점이다.
stateful 해야 하는 세션의 단점을 보완하기 위해 만들어진 JWT는 별도의 세션 저장소를 강제하지 않기 때문에 stateless 하여 확장성이 뛰어나고, signature를 통한 보안성까지 갖추고 있다.
1,2번은 큰 문제가 아닐 수 있다. 3번 문제가 핵심이다.
토큰이 탈취당하면 만료될 때까지 대처가 불가능하다.
세션의 경우 세션을 탈취 당했다고 판단 되었을 때 세션 저장소를 끊어서 탈취당한 세션 ID가 있더라도 세션 저장소에 그 값을 지워 탈취된 후의 상황을 보완할 수 있었다.
하지만 이것은 서버에서 클라이언트의 상태를 저장하는 stateful한 상황이고 지금은 stateless한 상황이다.
애초에 JWT를 발급해서 전달하고, 그 이후의 상황은 클라이언트가 관리하는 형태로 체계가 잡혀있기 때문에 서버가 탈취의 상황이 판단 되어도 관리할 수 있는 방법이 없다.
그렇다면 어떻게 해결해야할까?
JWT의 스펙에서 알아보았던 exp, Expiration Time(만료시간)을 짧게 가져가면 된다.
물론 쿠키, 세션도 회사마다 만료시간을 두어 강제로 invalidate하게 두는 경우도 있기는 하다.
JWT도 탈취되었을 경우의 안전성 때문에 만료시간을 짧게 가져가기도 한다. 30분 혹은 1시간 정보의 짧은 토큰을 저장한다면, 만약 토큰이 탈취되어도 서버가 이를 강제로 끊지는 못하겠지만 만료시간이 짧기 때문에 최소한의 보안성을 보장할 수 있기는 하다.
하지만, 서비스를 사용하는 주체는 사용자이기 때문에 사용자의 입장에서 생각을 해봐야 한다. 사용자 입장에서는 만료시간이 짧기 때문에 서비스를 잠깐 이용한 후 다시 로그인을 해야하는 불편한 경험을 겪을 수 있다.
이러한 짧은 유효시간을 보완할 수 있는 두가지 방법이 존재한다.
다만, 접속이 단발성으로 일어난다면 Sliding Session으로 연장시켜줄 수 없는 상황이 생기고, 너무 긴 Access Token을 발급시켜준 상황이라면 Sliding Session 때문에 무한정 사용하는 상황이 발생할 수 있다.
만약 클라이언트가 Access Token이 만료됨을 본인이 인지하거나, 서버로부터 만료됨을 확인받았다면 Refresh Token으로 서버에게 새로운 Acces Token을 발급하도록 요청하여 발급받는 방식입니다.
한 가지 시나리오를 들어서 구체적으로 어떻게 Refresh Token이 동작하는지 알아보자.
- 클라이언트가 ID, PW로 서버에게 인증을 요청하고 서버는 이를 확인하여 Access Token과 Refresh Token을 발급한다.
- 클라이언트는 이를 받아 Refresh Token를 본인이 잘 저장하고 Access Token을 가지고 서버에 자유롭게 요청합니다.
- 요청을 하던 도중 Access Token이 만료되어 더 이상 사용할 수 없다는 요류를 서버로부터 전달받는다.
- 클라이언트는 본인이 사용한 Access Token이 만료되었다는 사실을 인지하고 본인이 가지고 있던 Refresh Token를 서버로 전달하여 새로운 Access Token의 발급을 요청한다.
- 서버는 Refresh Token을 받아 서버의 Refresh Token Storage에 해당 토큰이 있는지 확인하고, 있다면 Access Token을 생성하여 전달한다.
- 이후 2로 돌아가 동일한 작업을 진행한다.
Refresh Token Storage는 세션 저장소와 똑같은 역할을 한다고 생각하면 좋다.
사실상 세션과 큰 차이 없이 특정 저장소에 I/O 작업이 발생하게 되기 때문에 Access Token이 지속되는 짧은 시간 동안만 I/O 작업이 일어나지 않는 것뿐이지 세션의 단점 하나를 가져가는 것과 같다.
다만 이를 사용함으로써 세션처럼 토큰 자체가 탈취되었다고 판단이 되면 Refresh Token Storage를 초기화하여 탈취된 토큰이 더 Refresh 못하도록 막는 등의 부가 옵션이 생긴다.
세션의 단점을 가져가기 때문에 세션과 똑같은게 아닌가 생각할 수 있지만 다르다.
단순히 I/O 횟수를 생각하면 확연히 차이가 난다는 것을 알 수 있다.
출처
https://brunch.co.kr/@jinyoungchoi95/1
https://blog.ull.im/engineering/2019/02/07/jwt-strategy.html
https://cjh5414.github.io/cookie-and-session/
https://tecoble.techcourse.co.kr/post/2021-05-22-cookie-session-jwt/