안녕하세요.
오늘은 웹 서비스에서 토큰 방식의 로그인을 구현할 때 인증 수단으로 가장 많이 활용되는 JWT (JSON Web Token)에 대해 쉽게 정리해 보려고 해요!!
JWT는 이름에서도 알 수 있듯이 JSON 형태의 인증 수단인데요. 크게 다음과 같은 세 구성요소로 나누어져 있어요!
- Header
- Payload
- Signature
JWT만 대충 간단히 보려고 들어왔더니 공부할 게 3가지나 된다는 점에서 머리가 아프지만 아주 간단하니까 조금만 봐주세요!
우선 넘어가기 전에 알아 두어야 할 사실 몇 가지만 설명드릴게요.
첫째로, 위에서 Header와 Payload만 JSON 구조이고 Signature는 검증만 하는 애라고 생각해 주세요! 다시 말해 Header와 Payload에만 쓸모 있는 정보가 들어있다! 이 말이죠!
둘째로, JWT는 유저가 로그인을 성공했을 때, from 인증을 담당하는 위치 (백엔드 서버 어딘가)에서 to 클라이언트에게 발급해 주는 거예요! 로그인 과정과는 상관이 없고 로그인 이후부터 클라이언트가 검증된 유저인지를 계속 확인하는 용도로 사용돼요!
Payload
Payload부터 먼저 설명할게요! 중요한 정보는 얘가 다 포함하고 있거든요. JWT가 로그인한 유저가 진짜 검증됐는지를 확인할 수 있는 인증 수단이라고 했죠? 따라서 어딘가에는 유저에 대한 식별 가능한 정보가 포함되어야 하는데 그 정보가 바로 Payload에 포함되어 있다고 보시면 돼요. 또한 토큰이라는 것은 로그인이 성공했을 때 발급되고 “토큰이 유효함” == “로그인이 됐음”이 성립되는 거거든요? 다시 말해 토큰이 영원히 유효했다간 영원히 로그인이 돼버리겠죠? 그래서 Payload에는 토큰의 만료시간도 함께 적어놓아요.
Payload는 이것 말고도 개발자가 원하는 대로 커스터마이징이 가능해요.
예를들어 많은 개발자들이 이런 것들을 포함해요.
{
"iss": "Issuer, 토큰을 발행한 발행자.",
"sub": "Subject, 토큰의 대상 또는 주제.",
"aud": "Audience, 토큰의 수신자.",
"exp": "Expiration Time, 토큰의 만료 시간, 일반적으로 Epoch time으로 제공됩니다.",
"nbf": "Not Before, 이 시간 이전에는 토큰을 처리해서는 안 됩니다.",
"iat": "Issued At, 토큰이 발행된 시간.",
"jti": "JWT ID, JWT의 고유 식별자, 중복 공격을 방지하기 위해 사용될 수 있습니다."
}
위에는 Registered claims라고 하는 값 들인데요. 위의 정보들을 사용할 때에는 가급적 'iss', 'sub'와 같이 위에 적혀진 이름대로 property 명을 사용하는 것이 권장되어 있어요. 물론 사용하지 않아도 무방해요.
아래는 말 그대로 이런 정보들을 넣는다 하는 느낌으로 알아두시면 되는 예시입니다!
{
"user_id": "사용자의 고유 식별자.",
"email": "사용자의 이메일 주소.",
"roles": "사용자의 역할 (예: ['admin', 'user']).",
"permissions": "사용자의 특정 권한."
}
단!! 뒤에 Header를 설명할 때에도 마찬가지지만, header와 payload는 암호화되지 않아요!! 그러니 절대 패스워드나 공개되어서는 안되는 민감한 정보를 포함하면 안 돼요!!
위에서 전달드린 말 그대로 Payload는 개발자가 원하는 모든 정보를 포함할 수 있고 프론트엔드에서도 JWT에 포함된 내용을 활용하여 특정 정보를 제공하는 것도 가능해요. 하지만 JWT는 권한이 필요한 정보에 대한 요청을 할 때마다 계속 전송 되어야 할 수 있으므로 크기가 너무 크게 되지 않도록 주의해야 돼요!
Header
이제 설명이 거의 다 끝났다고 할 수 있을 정도로 Header는 정말 간단해요.
Header에는 이 토큰의 타입이 무엇인지.. (당연히 “JWT”죠..) 그리고 바로 뒤에 나올 Signature를 생성할 때 어떤 암호화 알고리즘을 사용했는지.. 이게 끝이에요.
따라서 Header는 보통 Payload와 다르게 표준화된 구조를 띄고 있어요.
{
"alg": "HS256",
"typ": "JWT"
}
이게 끝입니다. 참 쉽죠?
Signature
Signature는 '서명'이라는 말 그대로 앞에 payload랑 header가 변조되었는지 체크하려고 만들어놓은 거예요.
바로 방금 전에 header에 “alg”라는 값이 있었죠? 그 알고리즘으로 Signature를 만들었다고 생각하면 되는데요. 암호화 방식은 뒤에서 설명드릴게요!
Signature 생김새
분명 JSON Web Token이라고 했지만!! 실제로 까보면 이런 모양이에요..
eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY0ZjczYzliZWI0ODE1Nzg5ZWMyMjEyZiIsImVtYWlsIjoiZXhhbXBsZUBzdG9sZW5jaGVlc2UuY29tIiwiaXNFbWFpbFZlcmlmaWNhdGlvbiI6ZmFsc2UsImlhdCI6MTY5MzkyNDUyNywiZXhwIjoxNjkzOTI4MTI3fQ.DLWkW11J6TSRlgS2rgrZVT65ChkZ78M5QNou2wdHiCMRY-IoBWj_0s9ADcX6gZpHMAki_vd7BoZ7nsqSGlil1g
뭔가 갑자기 알 수 없는 문자열들이 튀어나와 머리가 복잡해지면서 배신감이 느껴지지만 사실 별거 없는 놈이에요! 잘 보시면 문자열 사이에 쩜(.)이 두 개가 있을 거예요.
eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY0ZjczYzliZWI0ODE1Nzg5ZWMyMjEyZiIsImVtYWlsIjoiZXhhbXBsZUBzdG9sZW5jaGVlc2UuY29tIiwiaXNFbWFpbFZlcmlmaWNhdGlvbiI6ZmFsc2UsImlhdCI6MTY5MzkyNDUyNywiZXhwIjoxNjkzOTI4MTI3fQ.DLWkW11J6TSRlgS2rgrZVT65ChkZ78M5QNou2wdHiCMRY-IoBWj_0s9ADcX6gZpHMAki_vd7BoZ7nsqSGlil1g
저 쩜을 기준으로 문자열을 세 개로 쪼갤 수 있을 텐데요.
첫 번째가 Header, 두 번째가 Payload, 세 번째가 Signature입니다!
아니 그런데 왜 저런 모양이냐고요?
그냥 통신을 위해서는 JSON은 너무 길고 크니깐 Base64Url이라는 걸로 encoding 해서 저러는 거예요. 아 참고로 인코딩은 암호화가 아니에요!! 다시 말해 어느 누구나 Base64Url로 저걸 되돌리면 (decoding 하면) 원본의 JSON 데이터가 나온다는 거죠!
제가 직접 보여드릴게요!
const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV\_adQssw5c";
// Base64 디코딩을 위한 함수
function base64UrlDecode(str) {
// Base64Url 포맷을 일반 Base64 포맷으로 변환
str = str.replace(/-/g, '+').replace(/\_/g, '/');
// Base64 패딩 추가
while (str.length % 4) {
str += '=';
}
return atob(str); // Base64 디코딩
}
const parts = token.split('.'); // 토큰 분할
const header = JSON.parse(base64UrlDecode(parts\[0\]));
const payload = JSON.parse(base64UrlDecode(parts\[1\]));
console.log("Header:", header);
console.log("Payload:", payload);
위 코드의 결과는 아래와 같습니다!
위 코드는 JWT 토큰에서 쩜 (.)을 기준으로 문자열을 Header, Payload, Signature로 쪼갠 다음 Header와 Payload는 다시 JSON 형태로 되돌려 놓은 거예요.
당연히 Signature는 Encoding이 아니라 암호화이기 때문에 해독할 수가 없어요!
다시 한번 말씀드리지만 JWT 토큰에서 Header와 Payload는 위와 같이 누구나 열어볼 수 있기 때문에 절대 절대 Password나 민감한 정보들이 포함되어서는 안돼요!
일단 여기까지가 JWT에 대한 기본 개념이고 아래부터는 좀 더 자세히 설명을 드리려고 하니 더 읽고 싶으신 분만 읽어주세요! (자세하지만 아주 쉬워요!)
Signature 생성 방법 (암호화 방법)
Signature는 Header와 Payload를 암호화해서 생성한다고 위에서 말씀을 드렸죠? 그럼 어떻게 Header와 Payload라는 JSON Object를 암호화 시킬 수 있을까라며 궁금해하실듯한데요. 사실 예상했던 것보다 아주 단순한 방식입니다!!!
JWT 토큰이 Header와 Payload를 Base64Url로 encoding 하고 그 둘을 쩜 (.)으로 연결 (concatenation) 한다고 말씀드렸는데요…
그냥 그 상태로 암호화를 해버립니다.
말 그대로 그 둘을 쩜으로 연결하고 그 문자열을 통째로요!
위 토큰에서
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
이 부분을 (쩜을 포함해서..) HS256이든, ES256이든 Header “alg”에 표기된 암호화 알고리즘으로 암호화한 것이 바로 Signature입니다.
정말 단순하지 않나요?
토큰이 유효한지 검증하는 방법
다음과 같이 한 가지 질문이 떠오를 수 있습니다.
JWT 토큰의 Header와 Payload는 Base64Url로 Decode가 가능해서 누구나 열어볼 수 있다고 하는데, 그럼 Payload를 연 다음 ‘exp’ (토큰 만료 시간)을 내 마음대로 늘려버린 다음 다시 Base64Url로 Encode를 해서 사용하면 인증 서버 (Authorisation Server)는 어떻게 그 토큰이 변조되지 않았는지 알 수 있는 거지?
여기서 잠깐, JWT는 세션 방식이 아닌 토큰 방식의 인증이기 때문에 서버에서는 JWT 토큰에 대한 정보를 데이터베이스든, 서버 상의 메모리든 저장하지 않는 것이 원칙입니다. (특정 상황에서 예외는 있음)
다시 말해, 검증된 토큰을 메모리다 데이터베이스에 가지고 있다가 값을 비교하는 방식이 아니라는 거죠. 대신 클라이언트가 인증 서버에 전달한 JWT 하나로 유효한지를 검증해야 합니다!
암호화 방식을 이해하면 쉬운데요. 암호화를 설명하는 글은 아니기 때문에 간단하게 설명해 볼게요.
HS256이라는 암호화 알고리즘이 있는데 이는 미리 정해놓은 Secret Key라는 걸로 특정 데이터를 알아볼 수 없게 바꿔버려요. 암호화된 결과는 특정 데이터와 Secret Key의 조합으로 만들어진 것이기에 Secret Key가 없다면 절대 그 암호화된 데이터를 복호화 할 수 없는 거죠.
(HS256의 Secret Key는 정말 그냥 random 한 문자열을 개인적으로 만들어서 사용하셔도 돼요! 잃어버리거나 어딘가에 노출되지만 않으면 돼요!)
마찬가지로 JWT의 Signature가 Header와 Payload, 그리고 Secret Key의 조합으로 생성되었다면 Payload의 값이 변조되었다면 같은 Signature 값을 얻을 수 없겠죠? (클라이언트는 Secret Key를 절대 몰라요. Secret Key는 서버만 가지고 있어야 하니까요!)
다시 말해, 인증서버에서는 단순히 클라이언트로부터 JWT를 전달 받고 나서 JWT를 쩜(.)으로 쪼개고 앞에 두 부분 (Header와 Payload)만 원래 가지고 있던 Secret Key로 암호화해보면 돼요! 그 암호화 결과가 클라이언트가 준 JWT의 Signature 값과 같다면 앞에 두 부분 역시 변조되지 않았다는 걸 얘기해 주는 것 이거든요!
지금까지 JWT가 뭔지, 각 파트는 어떻게 구성되어 있는지, 그리고 검증이 어떻게 이루어지는지 설명을 드렸어요.
쉽게 설명해 보려고 노력해 봤지만 혹시나 여전히 어려운 부분이 있다면 댓글로 질문해 주세요!
감사합니다!
'웹 개발 > 웹 기초' 카테고리의 다른 글
[늅늅 시리즈] 정말 정말 쉬운 왕초보 웹 개발 - 1 (0) | 2023.07.04 |
---|