Node.js JSONWEBTOKEN 사용법 정리
2018-12-04 16:45:11

세션 데이터를 token으로 발급하는 형태.

장점은 서버가 부담을 안 받는다. 그러나 역시 단점은 서버에서 제어가 안됨

페이스북이나 기타 SNS의 access_token이 이런 형태.

테스트는 심플하게 해본다.

  1. 클라이언트에서 sdk로 페이스북 엑세스 토큰, 유저 ID를 받아서 서버에 보낸다.
  2. 서버에서는 받은 토큰, ID를 검증을 한다.
  3. 레디스에 있으면 jwt 토큰 + 해서 리턴, 없으면 유저 프로필을 저장 후 토큰 + 해서 리턴

JWT

jwt는 jsonwebtoken을 통해 쉽게 처리가 가능하다.

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

const jwt = require('jsonwebtoken');

const secret = 'nyancat (=^・ェ・^=))ノ彡☆';

/**
* TODO: jwt 토큰 생성
* @description 사용 알고리즘은 hmac sha256이라고 함
* @param {*} id 넣고싶은 해시 값, 지금은 걍 아디로.
*/
const makeToken = (id) => {
const payload = { id };
const options = {
issuer: 'delryn', // 발행자
expiresIn: '12h', // 날짜는 @d 시간은 @h, 분은 @m 그냥 숫자만 넣으면 ms단위
};
return jwt.sign(payload, secret, options);
};

/**
* 검증 미들웨어
* @param {*} req
* @param {*} res
* @param {*} next
*/
const verify = (req, res, next) => {
const checksum = new Promise((resolve, reject) => {
jwt.verify(req.body.token, secret, (err, decode) => {
if (err) return reject(err);
return resolve(decode);
});
});

return checksum.then((result) => {
req.decode = result;
return next();
}).catch((err) => {
console.log(err.message);
return res.status(401).send('jwt verify fail');
});
};

exports.makeToken = makeToken;
exports.verify = verify;

payload에는 쉽게 말해 세션에 담을 꺼 넣어주면 됨.

option에는 issuer, expiresIn 이외에도 넣을 수 잇는 규칙이 잇는데 이건 공홈에 ㄱ

sign을 통해 일종의 hash 값이 생긴다.

verify는 미들웨어로 작성을 한건데 기타 다른 API를 요청올 때 토큰을 읽어서 복호화 후

유저 세션을 보는 용도.

한 가지 알아두어야 할 점은 만료된 토큰의 경우도 에러로 받는다. 적절하게 예외 처리 요망.

페이스북

클라이언트는 귀찬아서 패스

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80

const axios = require('axios');
const redis = require('redis');
const jwt = require('./jwt');

const redisClient = redis.createClient();
const FACEBOOK_URL = 'https://graph.facebook.com/v3.2/';
const { FACEBOOK_APP_TOKEN } = process.env;

/**
* TODO: 엑세스 토큰 검증 체크
* @param {*} userId 페이스북에서 발급한 유저 ID
* @param {*} accessToken 페이스북에서 발급한 엑세스 토큰
*/
const accessTokenVerify = (userId, accessToken) => new Promise((resolve, reject) => {
const requestURL = `${FACEBOOK_URL}debug_token?input_token=${accessToken}&access_token=${FACEBOOK_APP_TOKEN}`;
axios.get(requestURL).then((response) => {
const body = response.data;
if (body.data.is_valid && body.data.user_id === userId) return resolve();
return reject(new Error('facebook verify fail'));
}).catch((err) => reject(err));
});

/**
* TODO: 페이스북 유저 프로필 가지고 오기.
* @param {*} accessToken 페이스북에서 발급한 엑세스 토큰
*/
const getFaceebookProfile = (accessToken) => new Promise((resolve, reject) => {
const requestUrl = `${FACEBOOK_URL}me?fields=id,name,picture&access_token=${accessToken}`;
axios.get(requestUrl).then((response) => resolve(response.data)).catch((err) => reject(err));
});

/**
* TODO: redis에 유저 데이터 저장
* @param {*} userId 페이스북에서 발급한 유저 ID
* @param {*} userData 유저 페이스북 프로필
*/
const setUser = (userId, userData) => new Promise((resolve, reject) => {
redisClient.hset('profile', userId, JSON.stringify(userData), (err) => {
if (err) return reject(err);
return resolve();
});
});

/**
* TODO: redis에 저장된 유저 프로필 가지고 오기.
* @param {*} userId 페이스북에서 발급한 유저 ID
*/
const getUser = (userId) => new Promise((resolve, reject) => {
redisClient.hget('profile', userId, (err, data) => {
if (err) return reject(err);
if (!data) return resolve();
return resolve(JSON.parse(data));
});
});

/**
* TODO: 로그인 처리 후 JWT 토큰 발급
* @param {*} userId 페이스북에서 발급한 유저 ID
* @param {*} accessToken 페이스북에서 발급한 엑세스 토큰
*/
const login = (userId, accessToken) => new Promise((resolve, reject) => {
accessTokenVerify(userId, accessToken).then(async () => {
try {
const userData = await getUser(userId);
if (typeof userData !== 'undefined') {
userData.token = jwt.makeToken(userId);
return resolve(userData);
}
const facebookProfile = await getFaceebookProfile(accessToken);
await setUser(userId, facebookProfile);
facebookProfile.token = jwt.makeToken(userId);
return resolve(facebookProfile);
} catch (err) {
return reject(err);
}
}).catch((err) => reject(err));
});

module.exports = login;

페이스북 API 응답

페이스북 API의 응답도 정리 해놓는다.

엑세스 토큰이 만료 된 경우

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{ data:
{ app_id: '내 페북 앱 아이디',
type: 'USER',
application: '내 페북 앱 이름',
data_access_expires_at: 1551621795,
error:
{ code: 190,
message: 'Error validating access token: Session has expired on Monday, 03-Dec-1808:00:00 PST. The current time is Tuesday, 04-Dec-18 06:17:26 PST.',
subcode: 463 },
expires_at: 1543852800,
is_valid: false,
scopes: [ 'email', 'public_profile' ],
user_id: '이 앱의 내 유저 ID' }
}

엑세스 토큰 정상인 경우

1
2
3
4
5
6
7
8
9
10
{ data:
{ app_id: '내 페북 앱 아이디',
type: 'USER',
application: '내 패북 앱 이름',
data_access_expires_at: 1551621795,
expires_at: 1543939200,
is_valid: true,
scopes: [ 'email', 'public_profile' ],
user_id: '이 앱의 내 유저 ID' }
}

프로필

1
2
3
4
5
6
7
8
9
10
{ id: '이 앱의 내 유저 ID',
name: '내 이름',
picture:
{ data:
{ height: 50,
is_silhouette: false,
url: '내 프로필 사진 url',
width: 50 }
}
}