JSON Web Token

Einere·2022년 8월 6일
0

원글 링크: https://kjwsx23.tistory.com/594

JWT

JSON Web Token은 IETF(Internet Engineering Task Force)의 RFC(Request for Comment) 7519로 제안된 기술입니다. (인터넷 표준은 특별한 RFC 또는 RFC의 집합을 의미합니다.) 해당 RFC는 현재 Proposed Standard 로써, 표준으로 채택된 듯 합니다.

해당 RFC를 보면, JWT에 대해 이렇게 설명하고 있습니다.

JWT은 두 당사자 간 전송될 클레임을 나타내는 간결하고 URL-안전한 방법이다.

소개

JWT은 HTTP 요청 헤더나 URI 쿼리 파라미터와 같이, 제한된 환경을 가진 공간에서 사용하기 위한 간결한 클레임 표현 방법입니다. 조금 더 쉽게 말하자면, 두 기기 간에 정보를 JSON 형태로 교환하는 간결하고 자가수용적인 방법입니다.

JWT은 클레임을 JSON Web Signature(JWS) 구조의 페이로드로서 사용되는 JSON 객체의 형태 혹은 JSON Web Encryption(JWE) 구조의 평문의 형태로 전송가능하게 인코딩합니다. 이 때, 클레임은 전자적으로 서명되거나 무결성이 Message Authentication Code(MAC)를 통해 암호화됩니다.
JWT은 항상 JWS 간결 직렬화 혹은 JWE 간결 직렬화로 표현됩니다. 조금 더 쉽게 말하자면, JWT은 시크릿(암구호) 혹은 공개/비밀 키를 이용해 서명됩니다.

JWT 자체를 암호화 하여 두 당사자 간 비밀을 공유할 수 있지만, 서명된 토큰에 더 중점을 둡니다. (서명된 토큰은 그 안에 담긴 클레임의 무결성을 확인할 수 있으며, 암호화된 토큰은 클레임 자체를 암호화합니다.) 비대칭 키를 이용해 서명한 토큰은 비공개 키를 보유한 당사자만이 서명을 한 사람임을 보장합니다.

참고로 발음은, jot 입니다. 😱

클레임

클레임(claim)은 주체에 저장된 정보 조각입니다. 클레임은 JSON 객체와 유사하게, 키와 값으로 구성되어 있습니다. 여기서 키는 claim name, 값은 claim value라고 합니다. 클레임 네임은 항상 string 형이며, 클레임 밸류는 어떠한 JSON 타입이 될 수 있습니다.

💡 클레임 네임은 간결함을 위해, 3글자로 구성되도록 정해져 있다. (아닌 것도 있다.)

활용

인증

기존의 세션 및 쿠키와 같은 인증 수단 중 하나로써 사용됩니다. 유저가 한번 로그인하면, 이후 인증이 필요한 요청에 JWT을 포함하므로써 인증을 처리할 수 있습니다.

토큰 기반 인증 방식은 오버헤드가 적고 여러 도메인에 걸쳐서 사용하기 용이합니다.

정보 교환

JWT은 서명되기 때문에, 두 파티 간 정보를 교환하는 데 사용하기도 좋습니다. 또한 헤더와 페이로드를 이용해 서명을 하기 때문에, 변조 탐지도 할 수 있습니다.

구조

JWT은 크게 header, payload, signature세가지 부분으로 나눌 수 있습니다.

헤더는 전형적으로 두가지 정보를 담습니다.

  • 토큰의 타입
  • 서명에 사용된 알고리즘 (HMAC, SHA256, RSA)
{
  "alg": "HS256",
  "typ": "JWT"
}

그리고 위 JSON은 base64를 통해 인코딩되어 JWT의 첫 부분에 들어가게 됩니다.

💡 base64는 8비트 이진 데이터(예를 들어 실행 파일이나, ZIP 파일 등)를 문자 코드에 영향을 받지 않는 공통 ASCII 영역의 문자들로만 이루어진 일련의 문자열로 바꾸는 인코딩 방식이다. 더 알아보고 싶다면 링크를 참고해주세요!

Payload

페이로드는 클레임을 담습니다. 클레임은 전형적으로 엔티티(보통 유저) 및 기타 정보들을 담는다.

클레임에는 세가지 종류가 있습니다.

  • registered claims
    • 유용하고 상호 운용이 가능한 클레임 집합을 제공하기 위해, 강제는 아니지만 권장되는, 미리 정의된 클레임 집합입니다.
    • 여기엔 iss(발행자), iat(발행 시간), exp(만료 시간), sub(주체), aud(청중) 등이 포함됩니다.
    • 각 클레임에 대한 자세한 정보는 RFC 7519 4.1에 자세히 나와 있습니다.
  • public claims
    • JWT을 사용하는 사용자라면 누구나 마음대로 정의할 수 있습니다.

    • 단, 클레임 네임 충돌을 피하기 위해 IANA JSON Web Token Registry에 클레임을 등록하거나 충돌을 피할수 있는 네임스페이스를 가진 네임을 사용해야 합니다. 보통은 URI를 사용하는 듯...

      {
          "https://velopert.com/jwt_claims/is_admin": true
      }
  • private claims
    • 해당 토큰의 생성자와 소비자가 서로 동의한 클레임입니다.
    • 토큰 레지스트리에 등록되지 않고, 퍼블릭 클레임에 포함되지 않는 이름이어야 합니다.

🚨 서명된 토큰은 무결성은 보장하지만 누구나 읽을 수 있다. 암호화하지 않는 경우, 헤더 혹은 페이로드에 민감한 정보를 넣지 말아야 합니다.

{
    "iss": "einere.com", // 등록된 클레임 네임
    "exp": "1485270000000", // 등록된 클레임 네임
    "https://einere.com/jwt_claims/is_admin": true, // 공개된 클레임 네임
    "userId": "11028373727102", // 비공개 클레임 네임
    "username": "einere" // 비공개 클레임 네임
}

페이로드도 마찬가지로 base64를 통해 인코딩되어 JWT의 두번째 부분에 들어가게 됩니다.

Signature

서명을 만들기 위해서는 인코딩된 헤더와 인코딩된 페이로드, 시크릿(혹은 비밀 키), 헤더에 명시한 암호화 알고리즘이 필요합니다. 에를 들어, HMAC SHA256 알고리즘을 이용한다면 다음과 같을 것입니다.

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret
);

🚨 단, base64로 인코딩 하는 경우, padding을 의미하는 = 문자가 포함될 수 있는데, 해당 문자는 URL-unsafe 하므로 제거해줘야 합니다.

💡 = 문자는 제거해도 복호화에 문제가 없습니다. 😎

이 서명은 토큰의 내용이 전송 과정에서 변조되지 않았음을 보증합니다. 만약 비밀 키로 서명된 토큰이라면, JWT의 전송자가 발행자임을 보증할 수 있습니다.

위 세가지 부분을 합치면 위 그림과 같이 토큰이 완성되며, JWT 공식 사이트의 디버거를 통해 검증할 수 있습니다.

차이점

서버 기반 인증 방식

HTTP는 기본적으로 stateless하기 때문에, 유저 정보를 저장하고 로그인 이후에 유저를 인증하는 과정이 필요합니다. 대부분은 유저의 session id를 쿠키에 저장하는 방식을 사용합니다.

이러한 방식은 여러가지 단점을 가지고 있습니다.

  • Hard to scale : 서버는 유저 세션 정보를 메모리 혹은 DB에 유지하고 있어야 합니다. 분산 시스템의 경우, 각 서버간 세션을 저장하고 확인하고 동기화하는 작업이 복잡합니다.
  • CORS : 기본적으로 HTTP 프로토콜은 요청에 자격(Credential, Authorization 요청 헤더)을 포함하지 않기 때문에, 다른 출처에 자원을 요청할 경우 CORS 문제가 발생할 수 있습니다.
  • Coupling to web framework : 웹 프레임워크와 커플링이 생깁니다. 만약 다른 언어나 프레임워크를 사용한 서버 세션 데이터를 공유하는 것은 매우 어렵습니다.

🚨 cookie나 Authentication 등 자격증명을 포함한 요청은 credentialed request라고 합니다. 이러한 요청은 크로스 사이트 요청일 때 자격증명을 포함해서 보내지 않습니다.
만약 자격 증명을 포함한 통신을 원한다면 요청 측에서는 credentials와 관련된 처리를 해야 하며, 응답 측은 Access-Control-Allow-Credentialstrue 로 설정해야 합니다.
추가로 응답 측은 Access-Control-Allow-Origin 를 와일드카드(*)를 제외한 출처를 명시해야 합니다.

💡 자격증명에는 쿠키, Authorization 헤더, TLS 클라이언트 인증서가 있습니다.

토큰 기반 인증 방식

토큰 기반 인증 방식은 stateless하기 때문에 유저 인증 정보를 별도로 저장할 필요가 없습니다. 즉, 유저가 어느 서버에서 로그인에 성공했는지 신경쓰지 않아도 됩니다. 단순히 토큰 값을 재활용 하면 됩니다.

동작 방식

인증

유저가 로그인에 성공하면, access token이 반환됩니다. 이 토큰 또한 신원 증명서이기 때문에 유효기간(exp)을 최대한 짧게 설정해야 합니다. 토큰 값을 저장할 위치는 크게 세 가지가 있습니다.

  • local storage
  • session storage
  • cookie

Web Storage API는 개발자 도구에서 다 조회가 가능하기 때문에, 추천되지는 않습니다. 따라서 보통 httpOnly 설정이 된 쿠키에 토큰값을 저장하거나, Authorization 헤더를 사용하는 듯 합니다.

유저가 권한이 필요한 페이지나 자원에 접근할 때, 인증정보를 포함한 요청 방식은 두가지가 있다.

  • Authorization 헤더에 Bearer 인증 타입(스킴)과 토큰 값
  • 쿠키 (서버에서 쿠키에 access token을 설정한 경우)
Authorization: Bearer <token>

서버는 토큰 값을 확인해서 유효성 검증을 한다.

💡 인증 타입에 대해 더 알고 싶다면 Hypertext Transfer Protocol (HTTP) Authentication Scheme Registry를 참고하세요.

장점

  • Stateless & Scalable
    • JWT은 자가수용적이기 때문에, 어느 서버에 귀속될 필요 없이 어느 서버에 전달할 수 있습니다.
  • Reusability
    • 같은 토큰을 여러 서버에 재사용 할 수 있다. 따라서 다른 앱과 권한을 공유하는 앱을 만들기 쉽습니다.
  • JWT Security
    • 쿠키를 사용하지 않는다면 CRSF(cross-site request fergery) 공격을 따로 대비할 필요가 없습니다.
    • 만약 민감한 정보를 담아야 한다면 JWE로 암호화 할 수 있으며, HTTPS를 통해 토큰을 전송하여 중간 공격을 막을 수 있습니다.
    • 보안적 결함 없이 XML을 서명하는 것은 매우 어려운 반면, JSON을 서명하는 것은 훨씬 간편합니다.
  • Performance
    • JWT은 XML에 비해 덜 서술적이기 때문에 더 작고 가볍습니.
    • 매 요청마다 세션을 찾고 역직렬화 할 필요가 없기 때문에, 성능이 좋습니다.
  • Cross-platform
    • JWT은 인터넷에 규모에서 사용되기 때문에, 모바일과 같은 다른 플랫폼에서도 잘 작동합니다.

JWT 활용

sign

const jwt = require('jsonwebtoken');
// sign with RSA SHA256
const privateKey = fs.readFileSync('private.key');
const token = jwt.sign({ foo: 'bar' }, privateKey, { algorithm: 'RS256' });

verify

var jwt = require('express-jwt');

app.get('/protected',
  jwt({ secret: 'shhhhhhared-secret', algorithms: ['HS256'] }),
  function(req, res) {
    if (!req.user.admin) return res.sendStatus(401);
    res.sendStatus(200);
  });

jwt 미들웨어와 익명 함수 미들웨어를 체이닝할 수 있습니다. jwt 함수가 요청(req) 의 user 속성에 디코딩된 JWT 페이로드를 설정합니다. (물론 옵션을 통해 저장할 속성을 변경할 수 있습니다.)

express-jwt 은 기본적으로 토큰이 유효하지 않다면 에러를 던집니다.

app.use(function (err, req, res, next) {
  if (err.name === 'UnauthorizedError') {
    res.status(401).send('invalid token...');
  }
});

위와 같이 미들웨어를 통해 에러 핸들링을 커스터마이징할 수 있습니다.

참고

JWT 공식 홈페이지
RFC 7519
JWT에 대해 알아보자!
[JWT] JSON Web Token 소개 및 구조
JSON Web Token Tutorial: An Example in Laravel and AngularJS
교차 출처 리소스 공유 (CORS)
HTTP 인증

profile
지속가능한 웹 개발자를 지향합니다. 경험의 공유를 통해 타인에게 도움이 되는 것을 좋아합니다. 사용자에게 가치를 제공하는 것에 기쁨을 느낍니다.

0개의 댓글