기존 ID Token 생성 부분을 함수로 분리
ID Token 생성
1. src\api\v1\member.js 파일 수정 – ID Token 생성 함수로 분리합니다.
// ID Token을 생성합니다.
async function generateIDToken(memberID, memberName, memberRoles) {
const result = {
result: 'success',
message: '',
token: ''
};
// 토큰 발급 시간
const issuedAt = new Date().toLocaleString();
const iat = Date.parse(issuedAt) / 1000;
// ID Token - JWT
const uuid = uuidv4().toString();
const jwtid = uuid.replace(/-/gi, '');
try {
const idToken = await new Promise((resolve, reject) => {
jwt.sign({
name : memberName,
roles : memberRoles,
typ : "ID",
iat : iat
},
secret,
{
algorithm: "HS256",
issuer : "https://apiserver.example.com",
subject : memberID,
audience : "lunchmenuApp",
expiresIn : "1d",
jwtid : jwtid
},
(err, token) => {
if (err) {
reject(err);
} else {
resolve(token);
}
});
});
result.token = idToken;
} catch(err) {
console.log(err);
result.result = "fail";
result.message = "ID Token 생성이 실패하였습니다.";
}
return result;
}
type으로 추가하여 Token을 구분하였습니다.
2. src\api\v1\member.js 파일 수정 – login 라우트를 수정합니다.
:
// JWT - 암호
const secret = "005c9780fe7c11eb89b4e39719de58a5";
// 사용자 ID로 검색하여 사용자를 인증하고 토큰을 리턴합니다.
router.post('/login', async function(req, res, next) {
console.log("REST API Post Method - Member Login.");
const result = {
result: 'fail',
code: 401,
message: '',
id_token: ''
};
if (req.body.id == undefined || req.body.id == "") {
result.code = 400;
result.message = "id가 없습니다.";
res.status(result.code).json(result);
return;
} else if (req.body.password == undefined || req.body.password == "") {
result.code = 400;
result.message = "password가 없습니다.";
res.status(result.code).json(result);
return;
}
const params = {
id : req.body.id,
password : req.body.password
};
//const query = "SELECT * FROM tb_member WHERE mbr_id = ?";
const query = mybatisMapper.getStatement('apiserver.db.mappers.mariadb.lunchmenu.member',
'selectMember', params, format);
let conn;
try {
conn = await getDBConnection();
//data = await conn.query(query, [req.body.id]);
data = await conn.query(query);
if (data.length == 0) {
result.message = "사용자가 없습니다.";
} else {
if (data[0].mbr_pw != params.password) {
result.message = "비밀번호가 동일하지 않습니다."
} else {
result.message = "사용자가 인증되었습니다.";
// ID Token을 생성합니다.
const resultIDToken = await generateIDToken(data[0].mbr_id, data[0].mbr_name, data[0].mbr_roles);
if (resultIDToken.result == "success") {
result.id_token = resultIDToken.token;
}
}
if (result.id_token != "") {
result.result = "success";
result.code = 200;
} else {
result.code = 500;
result.id_token = "";
result.message = "토큰 생성이 실패하였습니다.";
}
}
} catch (error) {
result.code = 500;
if (error.code == "ER_NO_SUCH_TABLE") {
result.message = "테이블이 없습니다.";
} else {
result.message = "서버 오류가 발생하였습니다.";
}
} finally {
if (conn) {
conn.release();
}
}
res.status(result.code).json(result);
});
동일한 암호키로 토큰을 생성하기 위해 JWT 암호를 위로 이동시킵니다.
기존 idtoken를 id_token으로 변경하였습니다.
사용자 API 테스트 (POSTMAN에서 확인)
사용자 로그인

JWT 확인

ID Token의 문제점
ID Token은 사용자를 인증하고 인증된 사용자에 대한 정보를 취득할 수 있지만 토큰의 유효 기간이 길게 설정되기 때문에 토큰이 탈취 당할 경우 보안이 취약해집니다.

Access Token을 사용해야 하는 이유?
토큰의 유효 기간을 짧게 설정하여 토큰이 탈취되어도 토큰의 유효 기간이 짧기 때문에 보안 문제를 최소화 할 수 있습니다.

그러나 Access Token의 유효 기간이 짧기 때문에 매번 사용자를 다시 인증해야 하는 문제가 발생합니다.
Refresh Token을 사용해야 하는 이유?
Access Token이 만료되었을 때 사용자를 확인하는 인증 절차를 거치지 않고 Access Token을 재발급할 수 있게 처리해주는 토큰을 생성합니다.

Access Token, Refresh Token (액세스 토큰, 리프레시 토큰)
사용자가 로그인하면 서버에서 사용자를 확인하여 토큰(Access Token과 Refresh Token)을 생성한 후 클라이언트에게 토큰(Access Token과 Refresh Token)을 전달하는 방식입니다. 클라이언트는 요청을 보낼 때 Authorization Header에 토큰(Access Token)을 포함하여 전달하면 서버는 전달 받은 토큰을 검증하여 인증된 사용자인지 확인합니다.
토큰(Access Token, Refresh Token)은 JSON Web Token (JWT, JSON 웹 토큰)입니다.
Access Token은 인증된 사용자에 대한 정보(claim(클레임))과 서버 리소스에 접근할 수 있는 권한(scope(스코프))이 포함되어 있습니다.
Refresh Token은 Access Token을 재발급하는데 사용되는 토큰입니다.
장점
- 클라이언트에서 토큰을 관리하기 때문에 서버에 부하가 낮아집니다.
단점
- 토큰이 탈취 당할 수 있습니다. → 보안성이 취약함
- (HTTPS로 보안할 수 있습니다. 단, 클라이언트 해킹은 차단할 수 없습니다.)
- 모든 자원을 접근할 수 있습니다. → 별도의 접근 권한 처리 필요
Access Token, Refresh Token Process (Access / Refresh 토큰 생성 프로세스)

Access Token, Refresh Token Process (Access Token 만료 시 프로세스)

Access / Refresh Token 생성
1. src\api\v1\member.js 파일 수정 - Access Token 생성 함수를 추가합니다.
// Access Token을 생성합니다.
async function generateAccessToken(memberID, memberName, memberRoles) {
const result = {
result: 'success',
message: '',
token: ''
};
// 토큰 발급 시간
const issuedAt = new Date().toLocaleString();
const iat = Date.parse(issuedAt) / 1000;
// Access Token - JWT
const uuid = uuidv4().toString();
const jwtid = uuid.replace(/-/gi, '');
try {
const accessToken = await new Promise((resolve, reject) => {
jwt.sign({
name : memberName,
roles : memberRoles,
typ : "Bearer",
scope: "profile lunchmenu.all",
iat : iat
},
secret,
{
algorithm: "HS256",
issuer : "apiserver.example.com",
subject : memberID,
audience : "lunchmenuApp",
expiresIn : "10m",
jwtid : jwtid
},
(err, token) => {
if (err) {
reject(err);
} else {
resolve(token);
}
});
});
result.token = accessToken;
} catch(err) {
console.log(err);
result.result = "fail";
result.message = "Refresh Token 생성이 실패하였습니다.";
}
return result;
}
Type으로 Token 구분 : Bearer
Scope로 서버 리소스에 접근할 수 있는 권한 설정 : profile lunchmenu.all
2. src\api\v1\member.js 파일 수정 - Refresh Token 생성 함수를 추가합니다.
// Refresh Token을 생성합니다.
async function generateRefreshToken(memberID) {
const result = {
result: 'success',
message: '',
token: ''
};
// 토큰 발급 시간
const issuedAt = new Date().toLocaleString();
const iat = Date.parse(issuedAt) / 1000;
// Refresh Token - JWT
const uuid = uuidv4().toString();
const jwtid = uuid.replace(/-/gi, '');
try {
const refreshToken = await new Promise((resolve, reject) => {
jwt.sign({
typ : "Refresh",
scope: "profile scopes",
iat : iat
},
secret,
{
algorithm: "HS256",
issuer : "apiserver.example.com",
subject : memberID,
audience : "lunchmenuApp",
expiresIn : "15m",
jwtid : jwtid
},
(err, token) => {
if (err) {
reject(err);
} else {
resolve(token);
}
});
});
result.token = refreshToken;
} catch(err) {
console.log(err);
result.result = "fail";
result.message = "Refresh Token 생성이 실패하였습니다.";
}
return result;
}
Type으로 Token 구분 : Refresh
Scope로 서버 리소스에 접근할 수 있는 권한 설정 : profile scopes
3. src\api\v1\member.js 파일 수정 - login 라우트를 수정합니다.
:
// 사용자 ID로 검색하여 사용자를 인증하고 토큰을 리턴합니다.
router.post('/login', async function(req, res, next) {
console.log("REST API Post Method - Member Login.");
const result = {
result: 'fail',
code: 401,
message: '',
access_token: '',
refresh_token: ''
};
if (req.body.id == undefined || req.body.id == "") {
result.code = 400;
result.message = "id가 없습니다.";
res.status(result.code).json(result);
return;
} else if (req.body.password == undefined || req.body.password == "") {
result.code = 400;
result.message = "password가 없습니다.";
res.status(result.code).json(result);
return;
}
const params = {
id : req.body.id,
password : req.body.password
};
//const query = "SELECT * FROM tb_member WHERE mbr_id = ?";
const query = mybatisMapper.getStatement('apiserver.db.mappers.mariadb.lunchmenu.member',
'selectMember', params, format);
let conn;
try {
conn = await getDBConnection();
//data = await conn.query(query, [req.body.id]);
data = await conn.query(query);
if (data.length == 0) {
result.message = "사용자가 없습니다.";
} else {
if (data[0].mbr_pw != params.password) {
result.message = "비밀번호가 동일하지 않습니다."
} else {
result.message = "사용자가 인증되었습니다.";
// Access Token을 생성합니다.
const resultAccessToken = await generateAccessToken(data[0].mbr_id, data[0].mbr_name, data[0].mbr_roles);
if (resultAccessToken.result == "success") {
result.access_token = resultAccessToken.token;
}
// Refresh Token을 생성합니다.
const resultRefreshToken = await generateRefreshToken(data[0].mbr_id);
if (resultRefreshToken.result == "success") {
result.refresh_token = resultRefreshToken.token;
}
}
if (result.access_token != "" && result.refresh_token != "") {
result.result = "success";
result.code = 200;
} else {
result.code = 500;
result.access_token = "";
result.refresh_token = "";
result.message = "토큰 생성이 실패하였습니다.";
}
}
} catch (error) {
result.code = 500;
if (error.code == "ER_NO_SUCH_TABLE") {
result.message = "테이블이 없습니다.";
} else {
result.message = "서버 오류가 발생하였습니다.";
}
} finally {
if (conn) {
conn.release();
}
}
res.status(result.code).json(result);
});
id_token를 지우고 access_token, refresh_token를 추가합니다.
기존 ID Token 생성을 주석 처리하거나 삭제합니다.
4. src\api\v1\authMiddleware.js 파일 수정
// Token 방식 (Access Token, Refresh Token)
const jwt = require('jsonwebtoken');
const authMiddleware = async (req, res, next) => {
let authorization = req.header('Authorization');
const result = {
result: 'fail',
code: 401,
message: '인증이 실패하였습니다.',
data: {}
};
if (authorization == undefined || authorization == "") {
result.code = 400;
res.status(result.code).json(result);
} else {
authorization = authorization.replace("Bearer ","");
try {
const secret = "005c9780fe7c11eb89b4e39719de58a5";
const tokenData = await new Promise((resolve, reject) => {
jwt.verify(authorization, secret,
(err, decoded) => {
if (err) {
reject(err);
} else {
resolve(decoded);
}
});
});
if (tokenData.typ == "Bearer") {
req.tokenData = tokenData;
next();
}
} catch(err) {
if (err instanceof jwt.TokenExpiredError) {
// 토큰 만료 - 401(Unauthorized), 498(Invalid token)
result.message = "토큰이 만료되었습니다.";
} else if (err instanceof jwt.JsonWebTokenError) {
// 토큰 에러 - Invalid token
result.message = "토큰이 유효하지 않습니다.";
} else {
result.message = "토큰이 유효하지 않습니다.";
}
res.status(result.code).json(result);
}
}
}
module.exports = authMiddleware;
Type이 Bearer일 때만 next()를 호출합니다.
사용자 API 테스트 (POSTMAN에서 확인)
사용자 로그인

Access Token 확인

Refresh Token 확인

Refresh Token Rotation
Access Token과 Refresh Token 둘 다 탈취가 될 경우 문제가 발생할 수 있습니다.
그래서 Access Token이 재발급될 때 Refresh Token도 같이 재발급되게 하여 보안 문제를 최소화 할 수 있습니다.

1. src\api\v1\member.js 파일 수정 - refresh 라우트를 추가합니다.
// Refresh Token으로 Access Token를 재발급(갱신)합니다.
// Refresh Token도 재발급합니다.
router.post('/refresh', async function(req, res, next) {
console.log("REST API Post Method - Token Refresh.");
const result = {
result: 'fail',
code: 401,
message: '',
access_token: '',
refresh_token: ''
};
if (req.body.refresh_token == undefined || req.body.refresh_token == "") {
result.code = 400;
result.message = "Refresh Token이 없습니다.";
res.status(result.code).json(result);
return;
}
const refreshToken = req.body.refresh_token;
// 토큰 검증
let tokenData = {};
try {
tokenData = await new Promise((resolve, reject) => {
jwt.verify(refreshToken, secret,
(err, decoded) => {
if (err) {
reject(err);
} else {
resolve(decoded);
}
});
});
} catch(err) {
if (err instanceof jwt.TokenExpiredError) {
// 토큰 만료 - 401(Unauthorized), 498(Invalid token)
result.message = "Refresh Token이 만료되어 Access Token을 재발급(갱신)할 수 없습니다.";
} else if (err instanceof jwt.JsonWebTokenError) {
// 토큰 에러 - Invalid token
result.message = "Refresh Token이 유효하지 않습니다.";
} else {
result.message = "Refresh Token이 유효하지 않습니다.";
}
res.status(result.code).json(result);
return;
}
const params = {
id : tokenData.sub
};
//const query = "SELECT * FROM tb_member WHERE mbr_id = ?";
const query = mybatisMapper.getStatement('apiserver.db.mappers.mariadb.lunchmenu.member',
'selectMember', params, format);
let conn;
try {
conn = await getDBConnection();
//data = await conn.query(query, [tokenData.sub]);
data = await conn.query(query);
if (data.length == 0) {
result.message = "사용자가 없어 Access Token을 재발급(갱신)할 수 없습니다.";
} else {
// Access Token을 생성합니다.
const resultAccessToken = await generateAccessToken(data[0].mbr_id, data[0].mbr_name, data[0].mbr_roles);
if (resultAccessToken.result == "success") {
result.access_token = resultAccessToken.token;
}
// Refresh Token을 생성합니다.
const resultRefreshToken = await generateRefreshToken(data[0].mbr_id);
if (resultRefreshToken.result == "success") {
result.refresh_token = resultRefreshToken.token;
}
if (result.access_token != "" && result.refresh_token != "") {
result.result = "success";
result.code = 200;
result.message = "Access Token을 재발급(갱신)하였습니다.";
} else {
result.code = 500;
result.access_token = "";
result.refresh_token = "";
result.message = "Access Token을 재발급(갱신)할 수 없습니다.";
}
}
} catch (error) {
result.code = 500;
if (error.code == "ER_NO_SUCH_TABLE") {
result.message = "테이블이 없어 Access Token을 재발급(갱신)할 수 없습니다.";
} else {
result.message = "서버 오류가 발생하여 Access Token을 재발급(갱신)할 수 없습니다.";
}
} finally {
if (conn) {
conn.release();
}
}
res.status(result.code).json(result);
});
사용자 API 테스트 (POSTMAN에서 확인)
전체 점심 메뉴 조회 - Access Token 만료

Access Token 재발급(갱신)

Access Token 확인

'Vue.js 3 & NodeJS > 사내교육 - 점심 메뉴' 카테고리의 다른 글
| [15] Frontend 개발 - Login 관리, Access Token, Refresh Token (0) | 2026.08.05 |
|---|---|
| [14] Backend 개발 - 사용자 API 개발, 라우트 별 인증 처리, 인가 처리 (0) | 2026.08.05 |
| [12] Frontend 개발 - 상태 관리, Login 관리, ID Token (0) | 2026.08.04 |
| [11] Frontend 개발 - 상태 관리, Store – Pinia (0) | 2026.08.04 |
| [10] Frontend 개발 - 상태 관리 (0) | 2026.08.04 |