Vue.js 3 & NodeJS/NodeJS

Node.js 레스트 API 인증처리(토큰 재발급) - Node.js REST API Authentication Refresh-Tokon

carrotweb 2021. 12. 12. 15:24
728x90
반응형

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"
}
728x90
반응형