Access-Token이 만료되면 로그인과 같은 인증을 하지 않고 재발급이 되도록 하기 위해서는 Refresh-Token를 생성해야 합니다.
Refresh-Token 생성하기
1. memberapi.js 파일을 오픈하여 사용자 객체를 가지고 있는 배열에 refreshToken를 추가합니다. refreshToken는 사용자가 Access-Token를 재발급하려고 할 때 인증 확인과 검증하기 위해서 사용됩니다.
let memberList = [
{id:"testid1", password:"testpwd1", name:"홍길동", refreshToken:""},
{id:"testid2", password:"testpwd2", name:"김철수", refreshToken:""},
{id:"testid3", password:"testpwd3", name:"이영희", refreshToken:""}];
2. login 라우터에서 Access-Token 부분을 다음처럼 수정합니다. 토큰 만료 시간을 1일(1d)에서 10분(10m)으로 수정합니다.
let accessToken = "";
let errorMessageAT = "";
// Access-Token
try {
accessToken = await new Promise((resolve, reject) => {
jwt.sign({
memberId : memberItem.id,
memberName : memberItem.name
},
config.secret,
{
expiresIn : '10m'
},
(err, token) => {
if (err) {
reject(err);
} else {
resolve(token);
}
});
});
} catch(err) {
errorMessageAT = err;
}
console.log("Access-Token : " + accessToken);
console.log("Access-Token Error : " + errorMessageAT);
3. Refresh-Token를 생성하는 부분을 추가합니다. payload에는 사용자 아이디만 저장합니다. 토큰 만료 시간은 1일로 합니다.
let refreshToken = "";
let errorMessageRT = "";
// Refresh-Token
try {
refreshToken = await new Promise((resolve, reject) => {
jwt.sign({
memberId : memberItem.id
},
config.secret,
{
expiresIn : '1d'
},
(err, token) => {
if (err) {
reject(err);
} else {
resolve(token);
}
});
});
} catch(err) {
errorMessageRT = err;
}
console.log("Refresh-Token : " + refreshToken);
console.log("Refresh-Token Error : " + errorMessageRT);
4. 이전에 try ~ catch로 된 전송 부분을 다음처럼 수정합니다.
생성된 refreshToken를 사용자 객체에 저장하고 전송할 JSON에 refreshToken를 추가합니다.
if (errorMessageAT == "" && errorMessageRT == "") {
memberItem.refreshToken = refreshToken;
res.json({success:true, accessToken:accessToken, refreshToken:refreshToken});
} else {
res.status(401).json({success:false, errormessage:'token sign fail'});
}
수정된 login 라우터 전체 소스입니다.
router.post('/login', async function(req, res, next) {
console.log("REST API Post Method - Member Login And JWT Sign");
const memberId = req.body.id;
const memberPassword = req.body.password;
var memberItem = memberList.find(object => object.id == memberId);
if (memberItem != null) {
if (memberItem.password == memberPassword) {
let accessToken = "";
let errorMessageAT = "";
// Access-Token
try {
accessToken = await new Promise((resolve, reject) => {
jwt.sign({
memberId : memberItem.id,
memberName : memberItem.name
},
config.secret,
{
expiresIn : '10m'
},
(err, token) => {
if (err) {
reject(err);
} else {
resolve(token);
}
});
});
} catch(err) {
errorMessageAT = err;
}
console.log("Access-Token : " + accessToken);
console.log("Access-Token Error : " + errorMessageAT);
let refreshToken = "";
let errorMessageRT = "";
// Refresh-Token
try {
refreshToken = await new Promise((resolve, reject) => {
jwt.sign({
memberId : memberItem.id
},
config.secret,
{
expiresIn : '1d'
},
(err, token) => {
if (err) {
reject(err);
} else {
resolve(token);
}
});
});
} catch(err) {
errorMessageRT = err;
}
console.log("Refresh-Token : " + refreshToken);
console.log("Refresh-Token Error : " + errorMessageRT);
if (errorMessageAT == "" && errorMessageRT == "") {
memberItem.refreshToken = refreshToken;
res.json({success:true, accessToken:accessToken, refreshToken:refreshToken});
} else {
res.status(401).json({success:false, errormessage:'token sign fail'});
}
} else {
res.status(401).json({success:false, errormessage:'id and password is not identical'});
}
} else {
res.status(401).json({success:false, errormessage:'id and password is not identical'});
}
});
Access-Token 재발급하기
1. HTTP POST 메서드로 "/refresh" 요청(Request)이 들어오면 사용자 배열에서 아이디(id)로 검색합니다. 사용자가 검색되지 않으면 상태 코드를 401로 리턴합니다.
router.post('/refresh', async function(req, res, next) {
console.log("REST API Post Method - Member JWT Refresh");
const memberId = req.body.id;
const accessToken = req.body.accessToken;
const refreshToken = req.body.refreshToken;
var memberItem = memberList.find(object => object.id == memberId);
if (memberItem != null) {
// 아래 코드들이 들어 옵니다.
} else {
res.status(401).json({success:false, errormessage:'id is not identical'});
}
});
2. refreshToken이 유효한지 검증합니다.
let refreshPayload = "";
let errorMessageRT = "";
// Refresh-Token Verify
try {
refreshPayload = await new Promise((resolve, reject) => {
jwt.verify(refreshToken, config.secret,
(err, decoded) => {
if (err) {
reject(err);
} else {
resolve(decoded);
}
});
});
} catch(err) {
errorMessageRT = err;
}
console.log("Refresh-Token Payload : ");
console.log(refreshPayload);
console.log("Refresh-Token Verify : " + errorMessageRT);
3. 토큰 검증을 위해 만료된 Access-Token의 payload를 가져옵니다. ignoreExpiration를 true로 설정하면 만료를 무시하고 payload를 디코딩하여 리턴됩니다.
let accessPayload = "";
let errorMessageAT = "";
// Access-Token Verify
try {
accessPayload = await new Promise((resolve, reject) => {
jwt.verify(accessToken, config.secret, {ignoreExpiration: true},
(err, decoded) => {
if (err) {
reject(err);
} else {
resolve(decoded);
}
});
});
} catch(err) {
errorMessageAT = err;
}
console.log("Access-Token Payload : ");
console.log(accessPayload);
console.log("Access-Token Verify : " + errorMessageAT);
4. 사용자 아이디와 Access-Token의 payload에 저장된 사용자 아이디, Refresh-Token의 payload에 저장된 사용자 아이디가 동일한지 검증하고 사용자 객체에 저장된 refreshToken과 동일한지 검증한 후 Access-Token를 재발급합니다. Refresh-Token과 Access-Token이 검증 단계에서 에러가 있으면 상태 코드를 401로 리턴합니다.
if (errorMessageRT == "" && errorMessageAT == "") {
if (memberId == accessPayload.memberId && memberId == refreshPayload.memberId && memberItem.refreshToken == refreshToken) {
let accessToken = "";
errorMessageAT = "";
// Access-Token
try {
accessToken = await new Promise((resolve, reject) => {
jwt.sign({
memberId : memberItem.id,
memberName : memberItem.name
},
config.secret,
{
expiresIn : '10m'
},
(err, token) => {
if (err) {
reject(err);
} else {
resolve(token);
}
});
});
} catch(err) {
errorMessageAT = err;
}
console.log("Access-Token : " + accessToken);
console.log("Access-Token Error : " + errorMessageAT);
if (errorMessageAT == "") {
res.json({success:true, accessToken:accessToken});
} else {
res.status(401).json({success:false, errormessage:'token sign fail'});
}
} else {
res.status(401).json({success:false, errormessage:'Token is not identical'});
}
} else if (errorMessageRT != "") {
res.status(401).json({success:false, errormessage:'Refresh-Token has expired or invalid signature'});
} else if (errorMessageAT != "") {
res.status(401).json({success:false, errormessage:'Access-Token is invalid signature'});
}
npm run 명령어로 실행합니다.
npm run start
Postman(포스트맨)를 실행하여 /members/login과 /member/refresh를 테스트합니다.
HTTP POST Method
http://localhost:9000/members/login
Request Body
{
"id" : "testid1",
"password" : "testpwd1"
}
아이디와 패스워드로 로그인 처리되어 리턴 값에 success가 true이고 accesToken과 refreshToken이 생성되어 리턴됩니다.
{
"success": true,
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZW1iZXJJZCI6InRlc3RpZDEiLCJtZW1iZXJOYW1lIjoi7ZmN6ri464-ZIiwiaWF0IjoxNjM5Mjg2NTg3LCJleHAiOjE2MzkyODcxODd9.p_aoQSNEk5qMrfE_r8bdK7PGBShn5RHsx37wMzkeHpg",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZW1iZXJJZCI6InRlc3RpZDEiLCJpYXQiOjE2MzkyODY1ODcsImV4cCI6MTYzOTM3Mjk4N30.9W2qLVA5Dv2PDJcygVcZgEANC7hRK7ihOlhyJuN8D54"
}
10분 후 /member/refresh를 테스트합니다.
HTTP POST Method
http://localhost:9000/members/refresh
Request Body
{
"id" : "testid1",
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZW1iZXJJZCI6InRlc3RpZDEiLCJtZW1iZXJOYW1lIjoi7ZmN6ri464-ZIiwiaWF0IjoxNjM5Mjg2NTg3LCJleHAiOjE2MzkyODcxODd9.p_aoQSNEk5qMrfE_r8bdK7PGBShn5RHsx37wMzkeHpg",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZW1iZXJJZCI6InRlc3RpZDEiLCJpYXQiOjE2MzkyODY1ODcsImV4cCI6MTYzOTM3Mjk4N30.9W2qLVA5Dv2PDJcygVcZgEANC7hRK7ihOlhyJuN8D54"
}
토큰이 검증되어 리턴 값에 success가 true이고 accesToken이 재발급되어 리턴됩니다.
{
"success": true,
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZW1iZXJJZCI6InRlc3RpZDEiLCJtZW1iZXJOYW1lIjoi7ZmN6ri464-ZIiwiaWF0IjoxNjM5Mjg3NTY5LCJleHAiOjE2MzkyODgxNjl9.Ktud6G77ofuwsgnGPBgiMoTorRbovm5pKGfG-Gpkr48"
}