Node.js & Express에서 JWT(JSON Web Token) 사용하기 -2-

전 포스트에서 JWT에 관한 설명을 조금 했는데 이번엔 내가 어떻게 썻는지에 대해서 중점적으로 남겨보려한다.(나중에 까먹을까봐)

이 방법이 Best Practice인지는 잘 모르겠지만…

혹시 이것보다 더 좋은 방법이 있다면 댓글로 알려주세요!

프로젝트 생성

Express 프로젝트를 생성하고 npm install을 해주자.

1
2
$ express --ejs express-jwt
$ cd express-jwt && npm install

일단 껍데기만 있는 Auth API를 만들어 두자.

app.js에 다음과 같이 추가하자.

1
2
3
4
5
// app.js

const auth = require('./routes/auth');

app.use('/auth', auth);

routes/auth.js 파일을 생성하자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// routes/auth.js

const express = require('express');
const router = express.Router();

/* Sign Up API
* - parameter email
* - parameter password
* - parameter username
*/
router.post('/signup', (req, res, next) => {
res.send('ok');
});

/* Sign In API
* - parameter email
* - parameter password
*/
router.post('/signin', (req, res, next) => {
res.send('ok');
});

module.exports = router;

JWT 미들 웨어

그리고 jsonwebtoken이라는 모듈을 설치하자

1
$ npm i --save jsonwebtoken

그리고 Express에서 미들웨어처럼 사용하기 위해 utils/tokenHelper.js라는 파일을 만들었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// utils/tokenHelper.js

const jwt = require('jsonwebtoken');

const TOKEN_SECRET = 'secret';

const tokenGenerator = (data, callback) => {
const token = jwt.sign(data, TOKEN_SECRET, {
algorithm: 'HS256',
expiresIn: 60 * 60 * 24 * 7
})
callback(token)
}

const isValid = (token, callback) => {
jwt.verify(token, TOKEN_SECRET, (err, decode) => {
if (err) {
// console.log("=========Token Helper: Can't decode token")
callback({isValid: false})
} else {
const exp = new Date(decode.exp * 1000)
const now = Date.now()
const day = (60 * 60 * 24 * 1000)
if (exp < now) {
// console.log("=========Token Helper: Expired Token")
callback({isValid: false})
} else if (exp < now + (5 * day)) {
// console.log("=========Token Helper: Generate New Token")
const newToken = module.exports.generateToken(decode.user.id)
callback({isValid: true, token: newToken, userInfo:decode})
} else {
// console.log("=========Token Helper: Token is valid")
callback({isValid: true, token: token, userInfo:decode})

}
}
})
}

const tokenHandler = (req, res, next) => {
const { token } = req.query

if(token) {
module.exports.isValid(token, (result) => {
req.userInfo = result;
next()
})
} else {
req.userInfo = {isValid: false}
next()
}
}

export default {
tokenGenerator,
isValid,
tokenHandler
}

Node.js & Express에서 JWT(JSON Web Token) 사용하기 -1-

두달만을 개발할 때 학습한 것들을 정리해서 올리기로 결심했다.

지금 생각나는 후보로는 다음과 같다.

  • JWT
  • Sequelize
  • Socket.io

더 있지만 일단 이 3가지의 기본 셋팅정도는 포스팅으로 올려놓으면 나중에 다시 보기에 좋을듯 싶다.

가장 처음으로 JWT를 다루기로 했다.

프로젝트 초기에는 Redis를 사용한 세션기반의 인증 방식을 사용하고 있었다.

하지만 뭔가 세련된 방법이 없을까 고민다하가 튜터님의 조언으로 JWT라는 것을 사용해 보기로 했다.

JSON Web Token

들어가기 전에 JSON Web Token의 개념을 알아두자.

JWT??

JSON Web Token은 공개된 업계 표준인 RFC 7519 방식으로 양 측의 클레임(Claims)을 안전하게 한다.
JWT.IO를 사용하여 JWT를 디코드, 검증 및 생성할 수 있다.

Claim이라는 용어가 처음에 낯설었지만 서버와 클라이언트가 주고받는 정보나 메세지 정도로 생각하면 된다.

JSON 객체로된 클레임은 디지털로 서명되었기 때문에 검증되고 신뢰할 수 있다고 한다.

또한 다음과 같은 개념을 가진다.

  • Compact: JWT는 크기가 작기 때문에 URL, POST 파라미터 또는 HTTP Header에 포함될 수 있다. 크기가 작아 전송 속도 또한 빠르다고 할 수 있다.

  • Self-contained: JWT는 Payload에 필요한 데이터를 포함한다. 따라서 토큰을 생성할 때 데이터베이스에서 정보를 가져오면 그 다음부터는 데이터베이스에 질의를 하지 않아도 된다.

언제 JWT를 사용할까?

  • Authentication: JWT를 사용하는 가장 일반적인 시나리오다. 유저가 처음 로그인을 하면, 유저 정보를 포함한 JWT를 생성해 발급해준다. 그 후 유저는 각 요청에 발급받은 토큰을 포함해 보내는데, 서버에서 토큰을 기반으로 유저의 정보, 허용되는 경로 등의 정보를 데이터베이스의 접근없이 알 수 있다. 또한 세션을 유지하지 않아도 되므로 서버의 비용이 줄어들 수 있다.

  • Information Exchange: JWT는 공개 키나 비밀 키로 서명할 수 있기 때문에 안전하게 정보를 교환할 수 있다. 게다가 서명이 헤더와 페이로드를 사용하여 계산되므로 내용이 변경되었는지도 판별할 수 있다.

JWT의 구조

JWT는 점(.)으로 구분되는 세가지 요소를 가진다.

  • Header
  • Payload
  • Signature

그래서 결과적으로 xxxxx.yyyyy.zzzzz 형태를 가지게 된다.

JWT의 첫 번째 부분은 Header 로, 헤더는 2가지 속성 값을 가진다.

  • alg: 해싱 알고리즘. 보통 HMAC SHA256 혹은 RSA가 사용된다.
  • typ: 토큰의 타입 = JWT

다음과 같이 Header를 생성할 수 있다.

1
2
3
4
{
"alg": "HS256",
"typ": "JWT"
}

Payload

두 번째 부분은 처음에 클레임을 포함하는 Payload가 들어가게 된다.

클레임은 3가지로 구분할 수 있는데, 다음과 같다.

  • Reserved claims: 이것은 필수는 아니지만 JWT를 사용함에 있어 권장되는 미리 정의된 클레임 세트이다. iss(발행자), exp(만료 시간), sub(주제), aud(고객) 등을 포함한다.

  • Public claims: 이것은 JWT사용자끼리 충돌을 방지하기 위한 값을 포함한다. 보통 충돌을 방지하기 위한 네임스페이스를 사용한다.

  • Private claims: 정보를 담는 클레임.

다음과 같이 Payload를 생성할 수 있다.

1
2
3
4
5
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}

Signature

서명을 만들기 위해서는 인코딩된 헤더, 인코딩된 페이로드, 시크릿 키, 헤더에 지정된 알고리즘을 가진다.

예를 들면 다음과 같이 서명을 만들 수 있다.

1
2
3
4
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)

이렇게 만든 서명으로 메세지가 변경되지 않았음(무결성)을 확인하는데 사용할 수 있다.

확인해보자

JWT Debugger에서 JWT를 디코딩해보거나, 검증하거나 생성해볼 수 있다.

JWT를 어떻게 쓰는…?

클라이언트는 서버에 인증 요청한다.

인증이 성공된다면 서버에서 사용할 클레임을 포함한 JWT를 생성해 클라이언트에게 넘겨준다.
일반적으로 로컬 저장소에 저장되지만 쿠키를 사용할 수도 있다.

그리고 클라이언트에서 인증이 필요한 어떠한 요청을 할 경우 요청 헤더에 다음과 같이 추가해준다.(Bearer는 토큰 타입을 결정하는 자리인데 OAuth2의 토큰 인증 방식이다.)

1
Authorization: Bearer <token>

서버는 요청 헤더에서 토큰 값을 디코딩해 필요한 정보를 가져올 수 있으며 그에 따른 요청 응답을 클라이언트에게 보내준다.

그래서 JWT를 사용하면

서버에서 토큰이 유효하다고 판단하면 클레임들을 디코딩해 정보를 가져올 수 있다.
서버의 메모리와 같이 다른 곳에 따로 관리를 하지 않아도 된다는 뜻이다.

따라서 Stateless한 서버를 만들 수 있게 된다.
그러므로 서버의 확장이 용이하고 요청 도메인에 관해 문제가 되지 않아 CORS문제를 생각하지 않아도 된다.

하지만 클레임셋이 증가하면 자연스레 토큰의 길이가 증가하게 되는데 요청 헤더에 토큰을 삽입하는 방식이므로 과한 오버헤드가 발생할 수 있다.

다음 포스팅

원래 프로젝트에서 사용한 예제도 포함하려 했는데

생각보다 JWT에 대한 설명이 길어져 다음 포스팅으로 넘기려한다.

간단한 예제긴 한데 나름 고민해서(?) 만들었고 나중에도 써먹기 위해 기록해둘 생각이다.

Refer

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×