치춘짱베리굿나이스
jsonwebtoken 본문
jsonwebtoken
설치
$> npm i jsonwebtoken
$> npm i -D @types/jsonwebtoken // 당신이 타입스크립트 사용자라면
$> yarn add jsonwebtoken
$> yarn add -D @types/jsonwebtoken // 당신이 타입스크립트 사용자라면
npm 링크
https://www.npmjs.com/package/jsonwebtoken
yarn 링크
https://yarnpkg.com/package/jsonwebtoken
용례
JWT를 손쉽게 제작 및 검증할 수 있는 라이브러리이다
https://chichoon.tistory.com/761 JWT에 관한 이론 글을 적었었는데, 이번에는 실제로 JWT 토큰을 만들어 본다
jwt 모듈 불러오기
import jwt from 'jsonwebtoken';
jwt
모듈을 불러와 메서드를 사용하기 위해 점을 찍어보면 decode
, sign
, verify
그리고 에러 선언 3개 총 6개의 메서드와 프로퍼티가 정의되어 있는 것을 볼 수 있다
메서드 이름만 봐도 너무 직관적이다! 정말 사용하기 쉽고 간단한 라이브러리다
처음에는 jwt
모듈을 사용해서 토큰을 만들고 검증까지 해 보고, 뒤에는 jwt
모듈을 직접 만들어보는 것도 좋을 듯하다
jwt.sign
const token = jwt.sign(payload, secretKey);
페이로드 객체와 비밀 키 (검증할 때 사용할 secret
키) 를 인자로 넣어주면 토큰을 반환한다
헤더를 따로 넣어주지 않는 이유는, 헤더에 들어가는 정보는 토큰의 타입 (JWT로 고정) 과 서명을 암호화할 때 사용하는 알고리즘 (HS256
등) 밖에 없으므로 모듈이 알아서 다 넣어주기 떄문이다
비밀 키는 검증 시에 이 서버에서 만든 토큰이 맞는지? (조작된 토큰이 아닌지) 체크하는 용도로 으로, 보안을 위해서라면 파일을 읽어오거나 환경변수로 복잡한 문자열을 지정하여 사용하는 것이 좋다
jwt.sign (with options)
const token = jwt.sign(payload, secretKey, options);
세 번째 인자로 옵션 객체를 넣을 수 있다
옵션 객체는 아래의 key
-value
쌍을 가질 수 있다
algorithm
: 서명을 암호화할 알고리즘 (비워두면, 기본값으로HS256
이 적용된다)expiresIn
: 이 토큰이 만료되는 시점- 토큰 발행 시점부터 얼마만큼의 시간 동안 토큰이 유효한지 (얼마만큼의 시간이 흘러야 만료되는지) 지정하는 값이다
- 시간 단위를 함께 사용할 수도 있고, 단위를 붙이지 않는다면 기본값으로
ms
단위로 설정된다 2h
,60
(60ms
),10d
등- 등록된 클레임 (reserved claim) 중
exp
와 같은 값으로, 해당 옵션 설정과 페이로드의exp
설정을 동시에 할 수 없다
notBefore
: 이 토큰이 활성화되는 시점- 토큰 발행 시점부터 얼마만큼의 시간이 흘러야 토큰이 활성화되는지 지정하는 값이다
expiresIn
과 마찬가지로 시간 단위를 함께 붙여 사용한다- 등록된 클레임 중
nbf
와 같은 값으로, 해당 옵션 설정과 페이로드의nbf
설정을 동시에 할 수 없다
audience
: JWT가 의도하는 수신자- 등록된 클레임 중
aud
와 같은 값으로, 해당 옵션 설정과 페이로드의aud
설정을 동시에 할 수 없다
- 등록된 클레임 중
issuer
: JWT를 발급한 주체 (발신자)- 등록된 클레임 중
iss
와 같은 값으로, 해당 옵션 설정과 페이로드의iss
설정을 동시에 할 수 없다
- 등록된 클레임 중
subject
: 이 토큰의 주제- 등록된 클레임 중
sub
와 같은 값으로, 해당 옵션 설정과 페이로드의sub
설정을 동시에 할 수 없다
- 등록된 클레임 중
jwtid
: 이 토큰의 식별자- 등록된 클레임 중
jti
와 같은 값으로, 해당 옵션 설정과 페이로드의jti
설정을 동시에 할 수 없다
- 등록된 클레임 중
subject
: 이 토큰의 주제- 등록된 클레임 중
sub
와 같은 값으로, 해당 옵션 설정과 페이로드의sub
설정을 동시에 할 수 없다
- 등록된 클레임 중
noTimestamp
: issueAt (iat
) 자동 설정 여부- 기본값은
false
(issueAt 자동 설정) 으로,true
일 경우iat
값이 설정되지 않는다 - 이 값을
true
로 설정하면 페이로드에iat
을 직접 넣을 수 있다 - 실제 토큰 발행 시점이 아니라 지정한 시각을 발행 시점으로 사용할 수 있는 것
- 기본값은
header
: 헤더 커스터마이징- 기본 헤더 설정 대신 원하는 대로 헤더에 값을 수정 및 추가할 수 있다
keyid
mutatePayload
:true
로 설정될 경우,sign
메서드가 페이로드 객체를 직접 수정할 것이다- 등록된 클레임 중
iat
은 옵션 설정 없이도 자동으로 페이로드에 포함되므로,mutatedPayload
가 참일 경우sign
메서드의 첫 번째 인자로 받은 객체에iat
프로퍼티가 자동으로 추가된다 - 그 외에도, 옵션을 통해 추가한 클레임들이 자동으로 객체에 추가된다
- 토큰에 인코딩되기 직전 페이로드 값을 알고 싶다면
true
로 설정하라고 공식 문서에 적혀 있다
- 등록된 클레임 중
import jwt from 'jsonwebtoken';
const secretKey = '시크릿키'; // 가급적 환경변수 또는 별도 파일에 분리 추천
const obj = { id: 'chichoon', nickname: '치춘' };
const token = jwt.sign(obj, secretKey, {
issuer: 'chichoon',
jwtid: 'asd',
subject: 'asd'
});
console.log(token);
토큰 발급이 완료되었다
정말 사용하기 쉬운 라이브러리다… 직관성 100점
이번에는 사용법을 정리하기 위해 시크릿 키를 대충 파일 상단에 선언만 했지만, 말 그대로 ‘시크릿 키' 인 만큼 보안을 위해 파일이나 환경변수에 분리하는 것이 좋겠다 (깃허브에 저 값이 그대로 올라가면….)
필자는? 보통 dotenv
모듈을 이용하여 .env
파일에 분리하는 방식을 택하곤 한다
굳이 파일 열어서 내부 문자열 읽어들이고… 하는 복잡한 과정을 구현할 필요 없이 알아서 환경변수 (process.env) 에 등록해 주어서 편하다
https://jwt.io/ 의 디버거를 이용하면 base64를 다시 복호화해서 내용물을 열어볼 수 있다
위에 내가 설정한 값 (id
, nickname
, iss
, sub
, jti
) 과 동일하게 잘 들어간 것을 확인할 수 있다
.
단위로 헤더 - 페이로드 - 서명 연결까지 해주니 이렇게 편할 수가 없다
jwt.verify
const decoded = jwt.verify(jwtToken, secretKey);
토큰을 만들었으니, 이 토큰이 진실된 (?) 토큰인지 알아볼 차례이다
jwtToken
에는 토큰을 넣고, secretKey
에는 토큰 생성 시에 사용했던 시크릿 키를 똑같이 사용하면 된다
같은 secretKey
를 넣어줌으로써 이 토큰의 서명 부분이 해당 시크릿 키를 이용하여 인코딩되었는지 = 우리가 작성한 토큰이 맞는지, 조작되진 않았는지 알 수 있다
검증에 성공했을 경우 (값이 조작되지 않았고, 우리가 생성한 토큰이 맞을 경우) 페이로드 부분을 복호화하여 원본 객체로 반환해 주고, 실패했을 경우 예외를 throw 한다
import jwt from 'jsonwebtoken';
const secretKey = '시크릿키'; // 가급적 환경변수 또는 별도 파일에 분리 추천
...
try {
const decoded = jwt.verify(jwtToken, secretKey);
console.log(decoded);
} catch (e) {
console.log(e);
}
예외 때문에 try-catch
문을 통해 예외를 잡아줘야 한다
아까 맨 처음에 보았듯 검증 실패에 대한 각각의 예외가 별도로 지정되어 있으므로, 그에 맞는 에러 문구를 받아볼 것이다
- 아직 JWT가 활성화되지 않았을 경우 (
NotBeforeError
)
현재 시각이 JWT가 활성화되기로 한 시각 (nbf
) 이전일 때 발생하는 예외이다
토큰 자체가 활성화되어 있지 않으므로 검증에 실패한 것이다
- JWT 토큰이 만료되었을 경우 (
TokenExpiredError
)
위의 경우와 반대로, 현재 시각이 JWT가 만료되는 시각 (exp
) 이후일 때 발생하는 예외이다
토큰이 이미 만료되었으므로 검증에 실패하였다
- 어딘가 나사가 빠진 토큰 (
JsonWebTokenError: invalid token
)
토큰의 형식 (헤더.페이로드.서명
) 은 지키되, 토큰에 문자를 추가하거나 중간의 글자를 제거하는 식으로 재현해볼 수 있다
- 유효하지 않은 서명 (
JsonWebTokenError: invalid signature
)
시크릿 키가 다르거나, 페이로드가 조작되는 등의 이슈로 토큰의 서명 부분이 jwt.verify
에서 헤더와 페이로드를 가지고 직접 암호화한 서명 부분과 일치하지 않을 때 발생하는 예외이다
토큰이 조작되었다던가, 우리가 보낸 토큰이 아닐 때 등 보안 이슈로 걸러지는 경우이다
- 이건 토큰도 아닌데 (
JsonWebTokenError: jwt malformed
)
JWT 토큰 형식이 아닌 문자열이 들어왔을 경우 발생하는 예외이다
대표적으로 헤더와 페이로드, 서명이 .
을 기준으로 이어져 있지 않을 경우 (.
개수가 부족한 경우) 또는 헤더와 페이로드, 서명 중 하나라도 누락되었을 경우 등이 있다
- 허용되지 않은 알고리즘으로 암호화된 서명 (
JsonWebTokenError: invalid algorithm
)
verify
메서드의 세 번째 인자로 들어가는 옵션 객체의 algorithms
속성과 관련이 있다
서명 부분을 암호화하는 데 사용된 알고리즘이 algorithms
배열 내에 없는 다른 알고리즘으로 서명 부분을 암호화했을 경우 발생하는 예외이다
- 허용되지 않은 수신자 (
JsonWebTokenError: jwt audience invalid. expected: <예상한 수신자>
)
verify
메서드의 세 번째 인자로 들어가는 옵션 객체의 audience
속성과 관련이 있다
토큰의 수신자 (aud
) 가 audience
배열 내에 없을 경우, 또는 정규식과 일치하지 않을 경우 발생하는 예외이다
- 허용되지 않은 발신자 (
JsonWebTokenError: jwt issuer invalid. expected: <예상한 발신자>
)
verify
메서드의 세 번째 인자로 들어가는 옵션 객체의 issuer
속성과 관련이 있다
토큰의 발신자 (iss
) 가 issuer
와 다르거나 배열 내에 없을 경우 발생하는 예외이다
- 허용되지 않은 식별자 (
JsonWebTokenError: jwt jwtid invalid. expected: <예상한 식별자>
)
verify
메서드의 세 번째 인자로 들어가는 옵션 객체의 jwtid
속성과 관련이 있다
토큰의 식별자 (jti
) 가 jwtid
와 다를 경우 발생하는 예외이다
- 허용되지 않은 주제 (
JsonWebTokenError: jwt subject invalid. expected: <예상한 주제>
)
verify
메서드의 세 번째 인자로 들어가는 옵션 객체의 subject
속성과 관련이 있다
토큰의 주제 (sub
) 가 subject
와 다를 경우 발생하는 예외이다
그 외의 경우, 정상적으로 복호화한 페이로드 부분을 반환한다
페이로드 외의 부분 (헤더, 서명) 도 보고 싶다면 하단의 옵션을 지정해줘야 한다
jwt.verify (with options)
const decoded = jwt.verify(jwtToken, secretKey, options);
마찬가지로 세 번째 인자로 옵션 객체를 넣을 수 있다
아래와 같은 key-value 쌍을 가질 수 있다
algorithms
: 배열로 허용 알고리즘 종류를 지정할 수 있다algorithms: [”HS256”, “HS384”]
이런 식- 만약 토큰의 서명 부분이 지정한 알고리즘 종류와는 다른 알고리즘으로 암호화되었다면,
JsonWebTokenError: invalid algorithm
예외가 발생한다 - 지원하는 알고리즘 종류는 공식 문서에 표로 나와 있다
audience
: 문자열, 정규식 또는 문자열 배열, 정규식 배열로 토큰 수신 대상자 (aud
) 를 지정할 수 있다audience: [”chichoon”, “jiychoi”, /jiy.*/]
이런 식- 만약 토큰의
aud
가 배열 내에 없을 경우 (또는 정규식과 일치하지 않을 경우)JsonWebTokenError: jwt audience invalid. expected: <예상한 수신자>
예외가 발생한다
issuer
: 문자열 또는 문자열 배열로, 토큰을 생성한 사람 (iss
) 을 지정할 수 있다issuer: [”chichoon”, “jiychoi”]
이런 식- 만약 토큰의
iss
가 배열 내에 없을 경우JsonWebTokenError: jwt issuer invalid. expected: <예상한 발신자>
예외가 발생한다
jwtid
: 문자열로, 토큰의 식별자 (jti
) 를 지정할 수 있다jwtid: ‘hello’
이런 식- 만약 토큰의
jti
와jwtid
값이 일치하지 않을 경우JsonWebTokenError: jwt jwtid invalid. expected: <예상한 식별자>
예외가 발생한다
subject
: 문자열로, 토큰의 주제 (sub
) 를 지정할 수 있다subject: ‘token’
이런 식- 만약 토큰의
sub
와subject
값이 일치하지 않을 경우JsonWebTokenError: jwt subject invalid. expected: <예상한 주제>
예외가 발생한다
ignoreExpiration
:true
일 경우, 토큰의 만료 여부 (exp
) 는 검증하지 않는다ignoreNotBefore
:true
일 경우, 토큰의 비활성 여부 (nbf
) 는 검증하지 않는다clockTolerance
: 만료 여부 (exp) 와 비활성 여부 (nbf) 를 검증할 때, 서버간 시간차를 보정해주기 위한 옵션이다maxAge
: 토큰이 유효하다고 판단하기 위한 추가 시간을 지정할 수 있다clockTimestamp
: 현재 시간 대신 검증에 사용할 시간을 따로 지정할 수 있다nonce
:nonce
클레임을 검증할 수 있는 속성이라고 한다
complete
: 기존처럼 페이로드만 반환하는 대신,{ header, payload, signature }
형식의 객체를 반환한다- 위의 예시처럼 헤더와 페이로드는 복호화한 객체로 반환한다
- 서명 부분은 복호화할 수 없으므로 (단방향 암호화 하였으므로) 문자열 그대로 보여준다
jwt.verify (with callback)
const decoded = jwt.verify(jwtToken, secretKey, options, callback);
네 번째 인자로 콜백 함수를 지정할 수 있다
이 콜백 함수는 검증에 성공했을 때 (복호화된 값) 와 검증에 실패했을 때 (발생한 예외) 를 인자로 구분하여 서로 다른 행동을 취할 수 있도록 설정할 수 있다
function verifyCallback(error, decoded) {
if (error) {
// 예외 발생 (검증 실패)
}
if (decoded) {
// 검증 성공
}
}
콜백 함수는 위와 같은 형태로 인자 두 개를 받는다
따라서 내부에서 if
문을 통해 검증 성공 여부를 분기로 나눠 처리할 수 있는 것이다
만약 4번째 인자로 콜백 함수가 지정된다면, verify
메서드는 콜백 함수의 반환값을 반환한다 (반대로 말하면, 콜백 함수의 반환값이 없을 경우 verify
메서드는 undefined
를 반환한다)
또한 try-catch
대신 콜백 함수 내에서 예외를 처리하므로, 콜백 함수 내에서 throw
를 하지 않는 이상 try-catch
문이 필요가 없어진다
로직을 다른 모듈로 분리하고 싶다면 콜백 함수를 분리해서 인자로 넣어주고, 간단하게 예외처리만 하고 싶다면 try-catch
를 사용하면 되겠다.. 취향껏
jwt.decode
const decoded = jwt.decode(token);
검증 과정 일절 없이 복호화만 해 주는 함수이다
만약 유효하지 않은 토큰일 경우 (헤더 - 페이로드 - 서명 구조가 아니거나, 잘못된 base64 문자열이라 복호화가 불가능한 등) 예외 발생 없이 null
을 반환한다
검증을 하지 않으므로 서명이 깨져도 신경쓰지 않는다
두 번째 인자로 옵션을 지정해 줄 수도 있는데,
complete
:verify
메서드의complete
옵션과 같다- 페이로드 뿐만 아닌 헤더와 서명도 같이 반환한다
json
:boolean
값을 받으며, 시험해 봤는데 별 차이가 없는 듯 하다… json 형식으로 반환할지 여부를 묻는건지…?
공식 문서에는 decode
메서드 관련 내용이 없는데, verify
메서드 내에서 내부적으로 사용되는 메서드가 아닐지… 생각이 들기도
여담
구성이 매우 간단해서 직접 구현해 보면서 JWT 형식을 이해하기에도 좋은 라이브러리인 것 같다 (시간이 된다면…)
깃허브 스타 15800개 가량으로 엄청난 인기를 구가하고 있는데 그 이유를 알 것 같다 (심플 이즈 베스트)