JWT가 뭔데?

JWT는 Json Web Token의 약자로 URL에 사용할 수 있는 문자들로만 구성된(URL-Safe) 전자서명된 JSON 데이터 덩어리이다

인증에 필요한 정보들을 JSON으로 표현한 데이터 토큰으로, 토큰 자체가 서명되어 있고 인증에 필요한 정보를 지니고 있어서 빠르게 기존의 인증 방식들을 대체하기 시작했다

특히 토큰을 공개키, 개인키를 이용해서 비대칭 암호화 알고리즘으로 서명을 할 경우 굉장히 강력한 보안성능을 자랑한다

비대칭 암호화 알고리즘으로 서명한 JWT는 보통 개인키를 이용해서 암호화 하고 이를 복호화할 수 있는 공개키를 함께 전달한다. 그럼 "누구나 토큰의 내용을 볼 수 있는게 아닌가?" 라는 의문이 들 것이다

일반적으로 JWT를 개인키로 서명하는 이유는 애당초 토큰 자체에 누구나 봐도 딱히 상관없는 정보만 실어보내는 것도 있지만, 개인키로 암호화 해서 이를 복호화할 수 있는 공개키와 함께 전달하는 것은 해당 토큰이 서버에서 생성되어 전달된 것이라는, 즉 토큰 제공자의 신원이 보장되기 때문이다

JWT의 구조

JWT는 다음과 같은 3가지 구조로 이루어져 있다.

  • Header
  • Payload
  • Signature

헤더에는 보통 토큰의 타입이나, 서명에 사용된 알고리즘에 대한 정보가 담겨있다.

위의 이미지를 보면 해당 토큰은 JWT이며, RSA512 방식으로 서명된 토큰이다

Payload

페이로드에는 해당 토큰을 통해 제공되는 데이터들이 key-value 형태로 들어가있다

위의 이미지를 보면 해당 토큰은 Hello 라는 내용을 담은 message 프로퍼티가 들어가있고, 또 뭔가 더 들어가있다.

iat와 exp라는 처음보는 프로퍼티가 들어가있는데 이 프로퍼티는 표준 스펙에 해당하는 요소인데 다음과 같은 것들이 정의되어 있다

  1. iat(issued at): 해당 토큰이 발급된 시간
  2. exp(Expiration Time): 해당 토큰의 만료 시간
  3. aud(Audience): 해당 토큰을 발급받을 대상
  4. sub(Subject): 토큰 제목
  5. nbf(Not Before): 해당 토큰의 활성화 날짜. 이 시간 이전에는 해당 토큰을 사용할 수 없음을 보장한다
  6. iss(issuer): 토큰 발급자
  7. jti(JWT id): 토큰의 식별자. 여러 issuer가 토큰을 발급할 경우 이를 구분하기 위한 값이다

물론 해당 프로퍼티들이 표준 스펙이긴 하지만 iat와 exp 정도를 제외하면 반드시 포함시켜야 하는 것은 아니다. (exp도 사실 포함시키지 않아도 되지만 그럼 절대로 만료되지 않는 토큰이 완성되기 때문에 보안상 좋지 않으니 그러지 말 것을 권장한다)

그리고 임의로 프로퍼티를 생성해서 써도 되는데 예를 들어서 위의 이미지 처럼 message라는 임의의 프로퍼티를 페이로드에 포함시켜서 사용해도 문제는 없다

이유를 바로 아래에서 말해주겠지만 페이로드엔 무조건 남에게 보여져도 상관없는 데이터만 담는 것이 좋다

Signature


시그니쳐는 서명에 대한 정보로 헤더와 페이로드를 인코딩한 값을 합치고 특정 알고리즘과 특정 키로 암호화되어있는 값이다. 암호화 되어 있기 때문에 복호화를 하지 않으면 볼 수 없다

이 시그니쳐를 추가하는 이유는 헤더와 페이로드는 사실 암호화된 값이 아니라 단순히 Base64로 인코딩 되어있는 값이다. 당연히 이 상태로는 보안성을 담보할 수 없기 때문에 시그니쳐를 추가하여 위변조를 판별하는 것이다

위의 사진에 따라 이 정보를 읽어보면 말 그대로 Base64로 인코딩된 헤더와 페이로드 값을 합치고, 이를 'MySecretKey'라는 값의 키로, SHA256 이라는 알고리즘으로 암호화했다는 것을 알 수 있다

만일 누군가 토큰을 위조하거나, 페이로드 또는 헤더 값을 변조하더라도 시그니쳐 정보와 다르거나 이를 검증하는 측에서 토큰 복호화가 불가능하니 위변조된 토큰임을 쉽게 알 수 있다

JWT의 장단점

장점

  1. JWT 자체가 이미 인증된 정보이기 때문에 이를 저장하기 위한 저장소를 따로 필요로 하지 않는다. 때문에 인증서버가 터져서 다른 서비스를 모두 못쓰게 되는 등의 일을 걱정하지 않아도 된다
  2. 세션과 같이 서버에서 클라이언트 상태를 저장해 둘 필요가 없다
  3. 암호화한 시그니쳐를 통해 위변조를 쉽게 알 수 있기 때문에 보안성이 좋다
  4. JSON이라는 형태로 존재하기 때문에 범용성이 좋다

단점

  1. 토큰을 강제로 만료시킬 방법이 없다. 때문에 키를 같이 탈취당하거나 암호화가 풀리면 이를 수습하는 과정이 번거로운 편이다
  2. 헤더와 페이로드 부분은 별다른 암호화 없이 그냥 Base64 방식으로 인코딩 되어있어서 페이로드에 민감한 정보를 담기가 어렵다

결론

JWT는 위와 같은 장점과 단점들이 있는데, 위와 같은 단점에도 불구하고 현재 가장 널리 쓰이는 인증수단이다

물론 토큰을 강제로 만료시킬 수단이 존재하지 않는 점은 분명히 문제가 되기 때문에 리프레시 토큰과 액세스 토큰을 같이 발급하는 방식 등의 여러 보완책이 나와있는 상태이니 자신이 개발하는 서비스의 상태에 맞는 방식을 골라서 쓰면 된다

Node.js로 JWT 발급하기

Node.js 버전은 16.15.0 LTS 버전이다

토큰 발급해보기

Node.js는 자체적으로 JWT를 발행하는 기능이 없다. 그러니 아래의 명령어를 터미널에 입력해 JWT를 발행하는 라이브러리를 추가해주자
둘 중 자신이 쓰는 패키지 관리자에 맞는 명령어만 쓰면 된다

npm install jsonwebtoken
또는
yarn add jsonwebtoken

그럼 이제 코드를 한 번 짜보도록 하자
아래 코드는 JWT를 발행하는 아주 간단한 예제이다

import jwt from 'jsonwebtoken'
const { sign } = jwt

const token = sign({ message: 'hello' }, 'mySecretKey', { expireIn: '10s' })
console.log(token)

sign 함수의 첫번째 인자에는 그냥 페이로드에다 담을 아무 값이나 주면 된다
두번째 인자에는 해당 토큰을 암호화하는데 쓰일 키를 주면 된다. 아무 값이나 키로 줘보자
세번째 인자에는 각종 표준 스펙들을 지정하는 객체를 인자로 주면 된다. issuer나 토큰의 만료시간, 알고리즘의 종류 등을 여기서 지정해주면 된다

발급받은 토큰 확인해보기(jwt.io)

이 코드를 터미널에서 실행시켜보면 뭔가 암호화 된 것 같은 문자열들이 나타날텐데 이걸 복사해서 아래의 사이트에 가서 입력해보자
https://jwt.io/

시그니쳐 부분에 비어있는 박스가 있을 텐데 해당 토큰을 복호화하는데 쓰이는 키를 입력하면 된다. 자신이 토큰을 발행할 때 썼던 값을 입력해주면 아래에 Signature Verified 라는 글이 뜰 것이다

발급받은 토큰 확인해보기(코드)

이제 우리가 발급했던 토큰을 사이트가 아니라 코드상에서 확인해보고 싶을 것이다. 그러니 코드상에서 확인해보자

우선 위에서 작성한 코드를 아래와 같이 변경하고 토큰을 검증하는 verify 함수를 추가해주자

import jwt from 'jsonwebtoken'
const { sign, verify } = jwt

const token = sign({ message: 'hello' }, 'mySecretKey', { expireIn: '10s' })
const data = verify(token, 'mySecretKey')
console.log(data)

verify 함수의 첫번째 인자에는 검증을 진행할 토큰을 주면 된다
두번째 인자에는 해당 토큰을 복호화하기 위한 키 값을 주면 된다

만일 키 값이 다르면 바로 에러를 내뱉으며 꺼지게 되는데 이게 그냥 단순히 이상한 값을 리턴하는게 아니라 예외를 던지기 때문에 예외를 핸들하는 코드가 없다면 바로 프로그램이 꺼지게 된다

그리고 키 값이 다른 것 이외에도 여러가지 이유로(만료된 토큰의 검증 시도, 토큰이 아닌 이상한 값의 검증 시도, 위변조된 토큰의 검증 시도 등등) 토큰의 검증에 실패하게 된다면 그 즉시 예외를 던지게 되므로 실제 서비스 개발에 사용할 생각이면 적당한 예외 핸들링 코드를 작성해주는 것이 좋다

각설은 이쯤하고 이 코드를 실행시켰을 때 아래와 같은 내용이 터미널에 뜨게 된다면 성공이다

페이로드의 내용만 뜨는 게 정상이니 걱정하지 않아도 된다

그런데 말이에요

위에서 쓰인 코드는 안타깝게도 비대칭 암호화 방식이 아니다

sign 함수는 algorithm을 통해 서명에 쓰일 암호화 알고리즘을 따로 지정해주지 않는다면 기본적으로는 SHA256이란 알고리즘을 통해 서명되는데, 이 알고리즘은 암호화와 복호화에 쓰이는 키가 동일한 대칭 암호화 알고리즘이다

즉 알고리즘을 따로 지정해주지 않았는데 만일 토큰 암호화에 쓰인 키를 탈취당한다면 그냥 보안이 뚫린 것이다

이를 방지하기 위해 비대칭 암호화 알고리즘을 사용해서 토큰을 발급해보자

비대칭 암호화 알고리즘을 이용해 서명한 JWT 발급하기

우선 코드를 아래와 같이 고쳐서 암호화 방식을 지정해주자

import jwt from 'jsonwebtoken'
const { sign, verify } = jwt

const token = sign({ message: 'hello' }, 'mySecretKey', { algorithm: 'ES256', expireIn: '10s' })
console.log(token)

jsonwebtoken 라이브러리의 sign 함수에서는 RSASSA-PKCS 방식의 암호화를 제공하는 RS256, RS384, RS512 알고리즘과, RSASSA-PSS 방식의 암호화를 제공하는 PS256, PS384, PS512 알고리즘, ECDSA 방식의 암호화를 제공하는 ES256, ES384, ES512 알고리즘을 선택할 수 있다
필자는 암호화폐에도 쓰이는 ECDSA 방식의 암호화를 제공하는 ES256을 이용하여 암호화를 해봤다

그리고 터미널에서 코드를 실행시켜보면 드디어!!

가 아니라 에러가 뜨는 모습을 볼 수 있다

비대칭 암호화 알고리즘은 반드시 개인키와 공개키 한 쌍이 필요한데, sign 함수는 어떤 것이 공개키이고, 어떤 것이 개인키인지 여부를 start line 플래그를 통해 확인한다

제공되는 라이브러리를 이용할 수도 있겠지만 간단하게 아래의 사이트를 들어가서 공개키와 개인키를 발급받아보자
RSA: https://travistidwell.com/jsencrypt/demo/
ECDSA: https://dinochiesa.github.io/jwt/

RSA키를 발급해주는 사이트에서는 왼쪽에서 키 사이즈를 설정할 수 있는데, 최근 권장되는 길이는 2048비트라고 한다
2048비트를 선택하고 키를 만드니 약 1초정도가 걸렸다

ECDSA키를 발급해주는 사이트에선 사이트 위쪽 중간에 저렇게 알고리즘을 선택하는 버튼이 있는데 저기서 ES256이나 ES384, ES512 중 하나를 선택하고

아래로 내려보면 오른쪽 아래에 저렇게 Private Key와 Public Key를 볼 수 있는데, 리프레시 버튼을 눌러 새로 키를 발급 받으면 ECDSA 키가 나타난다

이제 생성된 Private Key와 Public Key를 복사해서 코드에 넣어주고 아래와 같이 sign 함수 부분을 수정해주자

import jwt from 'jsonwebtoken'
const { sign, verify } = jwt

const privateKey = '사이트에서 복사해온 Private Key'
const publicKey = '사이트에서 복사해온 Public Key'

const token = sign({ message: 'hello' }, privateKey, { algorithm: 'ES256', expireIn: '10s' })
console.log(token)

이 코드를 실행시켰을 때 아래와 같이 토큰값이 출력되면 성공이다

이제 이 코드를 verify를 이용해 발행된 토큰을 검증해보자

import jwt from 'jsonwebtoken'
const { sign, verify } = jwt

const privateKey = '사이트에서 복사해온 Private Key'
const publicKey = '사이트에서 복사해온 Public Key'

const token = sign({ message: 'hello' }, privateKey, { algorithm: 'ES256', expireIn: '10s' })
const data = verify(token, publicKey, { algorithms: 'ES256' })
console.log(data)

이 코드를 실행시켰을 때 아래와 같이 페이로드 값이 출력되면 성공이다

이렇게 서명에 쓰이는 키와 검증에 쓰이는 키가 다른 비대칭 암호화 알고리즘을 이용한 JWT를 발급해서 사용해 보았다

2개의 댓글

comment-user-thumbnail
2023년 8월 7일

정말 잘 읽었습니다. 제 이해가 맞는지 궁금해서 여쭤봅니다!
해시 방식인 HS256방식은 signature 인코딩을 위한 secret key를 서버에 저장해두는 것으로 알고 있는데, 그럼 비대칭 키 방식도 결국에는 private key를 서버에 저장을 해두어야 하기 때문에 보안적으로는 둘 다 차이가 없는것이 맞을까요??
현업이나 실제에서는 어떤 방식이 더 많이 쓰이나요?

1개의 답글
Powered by GraphCDN, the GraphQL CDN