필수 선행지식
- Base64
알면 조금 더 읽기 편한 선행지식
- 세션과 쿠키
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
→ A.B.C
JWT는 이렇게 생겼다. 구조를 쪼개면 세 부분으로 나눌 수 있다. 그 A,B,C가 각각 뭔지는 일단 놔두고, 이 JWT라는게 어떻게 동작하는지부터 알아보자.
A와 B는 암호화 되어있지 않고 단순히 Base64로 인코딩 되어있을 뿐이라서 똑같이 Base64로 디코딩 하면 누구나 내용을 확인할 수 있다.
C는 A와 B를 합친 후 비밀키로 암호화하고, 그 값({A+B}x암호화)을 다시 Base64로 인코딩한 값이다. 즉 JWT에서 정말로 암호화가 되어있는 부분은 C뿐이며, A,B와 달리 Base64로 디코딩해도 C의 내용은 알 수 없다.
C는 비밀키로 암호화하는 것이기 때문에 A와 B중에 조금이라도 값이 달라진다면 C는 전혀 다른 값이 되어버린다.
따라서 토큰이 유효한지 확인하려면 확인하는 쪽에서 똑같이 A와 B를 합쳐서 암호화를 해본 값과 수신한 토큰이 값이 같은지 확인하면 된다.
내가 받은 JWT가 A.B.C 로 되어있고 암호화 키가 D라고 가정해보자.
A와 B를 더하고 내가 가지고 있는 비밀키로 D를 C'를 만들어본다. 이 때 내가 새로 만들어본 값 C'와 받아온 JWT의 C값이 일치하면 해당 토큰이 인증되는 것이다.
헤더라고 불리는 부분이다. 알고리즘과 토큰의 타입을 명시해놓는 부분이다.
토큰의 타입은 JWT
로 고정이며, 알고리즘은 C를 어떻게 암호화했는지 적어놓는 부분이다.
대체로 알고리즘은 HMAC SHA-256(256비트의 해쉬값을 생성하는 함수)
를 선택하는 경우가 많으므로, A는 이렇게 생기게 된다.
{
"alg": "HS256",
"typ": "JWT"
}
이것을 Base64로 인코딩하면 이런 모습이 된다.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
페이로드라고 불리는 부분이다. 토큰의 실질적인 내용이 들어가는 부분이다.
JWT에 대한 내용(토큰 생성자(클라이언트)의 정보, 생성 일시 등)은 클라이언트와 서버 간 주고 받기로 한 값들로 구성된다.
또한 이렇게 페이로드(Payload)에 있는 각각의 속성들을 클레임, 그 모음을 클레임 셋(Claim Set)이라 부른다.
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
페이로드도 역시 Base64로 인코딩한다. 그럼 위의 페이로드의 경우, 이런 모습이 된다.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
서명이라고 불리는 부분이다. 토큰에서 암호화가 되어있는 부분이며, Base64로 디코딩해도 그 내용을 알 수 없는 부분이다.
JWT의 서명을 만드는 과정은 다음과 같다.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyfQ
HMAC SHA-256
와 비밀키로 암호화를 한다.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
암호화 알고리즘은 이미 만들어져있으므로 JWT자체는 누구나 쉽게 만들어볼 수 있다. 특히 공식 사이트에서 간단하게 만들어 볼 수 있고 설명도 잘 되어있으므로 JWT에 대해서 더 자세히 알아보고 싶다면 꼭 들려보자. https://jwt.io/
그렇다. 그게 바로 JWT의 한계이다. JWT를 이용한 인증 방식은 오직 토큰이 유효한지 아닌지만 판단하므로, 누가 보내든 토큰만 유효하면 OK다. 세션이 아니므로 만에 하나 토큰이 탈취당했을 경우 그 토큰을 무효화할 방법도 없다.
액세스 토큰의 유효기간을 짧게 하여 만에 하나 액세스토큰이 탈취당했더라도 피해를 최소화 할 수 있게 한다. 그러나 액세스 토큰의 유효기간이 너무 짧으면 유저가 매번 로그인해야하는 번거로움이 생긴다.
그래서 보통 백엔드에서는 최초 JWT 발급 시 하나만 주는게 아니라 액세스토큰, 리프레쉬 토큰 이렇게 두 개의 토큰을 준다. 액세스 토큰의 유효기간은 짧고, 리프레쉬 토큰의 유효기간은 긴 편이다. 인증, 인가를 요청할 때는 백엔드에 액세스 토큰만 넘겨주면 되고, 액세스 토큰이 만료되었다면 리프레쉬 토큰을 보내 새로운 액세스 토큰을 발급받는다.
유저를 강제로 로그아웃 시키려면 백엔드의 DB에서 해당 유저의 리프레쉬 토큰을 삭제시키면 된다.
프론트 자체에서 인증을 해야하는 경우가 아니라면 굳이 비밀키로 JWT의 서명을 확인할 필요가 없다. JWT의 수명이 유효한지 확인하고 싶으면 토큰을 Base64로 디코딩하고, 페이로드 값에 있는 클레임 정보만 쓰면 된다.
인증과 관련해서는 다음과 같은 로직을 구현할 필요가 있다.
토큰 보관의 경우,
이렇게 크게 두가지가 있다. 각각의 장단점이 있으므로 상황에 맞게 사용하면 된다.