1. Access Token과 Refresh Token의 개념에 대해 알아봅니다.
2. Access Token과 Refresh Token을 실제로 구현해봅니다.
01. Access Token, Refresh Token
1) Access Token이 무엇인가요?
Access Token은 사용자의 인증(ex: 로그인)이 완료된 후 해당 사용자를 인증하는 용도로 발급하는 토큰입니다.
이전에 저희가 구현하였던 쿠키(Cookie)에 jwt를 설정하고, 지정된 만료 시간이 지나면 인증이 만료되는 구조 또한 Access Token이라고 부를 수 있습니다.
인증 요청시 Access Token을 사용하면, 토큰을 생성할 때 사용한 비밀키(Secret Key)로 인증을 처리하게 됩니다. 이 방식은 복잡한 설계나 여러 분기 처리 없이 코드를 구현할 수 있다는 장점을 가지고 있습니다.
Access Token은 Stateless(무상태) 즉, Node.js 서버가 재시작되더라도 동일하게 작동하게됩니다. 이는 jwt를 이용해 사용자의 인증 여부는 확인할 수 있지만, 처음 토큰을 발급한 사용자가 정말 그 사용자인지는 확인할 수 없습니다.
Access Token은 그 자체로도 사용자 인증에 필요한 모든 정보를 가지고 있습니다. 그렇기 때문에, 토큰을 가지고 있는 시간이 늘어날 수록, 탈취되었을 때 피해 규모는 더욱 커지게 됩니다.
토큰이 탈취되었다 하더라도, 서버에서는 해당 토큰이 탈취된 토큰인지 알 수 없으며, 강제로 토큰을 만료시킬 수도 없습니다. 따라서 서버는 언제나 토큰이 탈취될 수 있다는 가정 하에, 피해를 최소화할 수 있는 방향으로 개발을 진행해야 합니다. 😊
2) Refresh Token이 무엇인가요?
Refresh Token은 사용자의 모든 인증 정보를 담고 있는 Access Token과는 달리, 특정 사용자가 Access Token을 발급받기 위한 목적으로만 사용됩니다.
Refresh Token은 Access Token을 재발급하기 위해 사용되며, 서버에서 관리됩니다. 이는 토큰 탈취 시 피해를 최소화하기 위한 것으로, Access Token의 유효 기간을 짧게 설정하고 주기적으로 갱신함으로써 보안을 강화합니다.
3) Refresh Token Project의 템플릿을 만들어봅니다!
Refresh Token은 사용자가 서버와 최초 인증시에 발급을 받게되는데요. 이번 프로젝트에서는 하나의 파일에서 Refesh Token과 Access Token이 어떤식으로 동작하는지 확인해볼 예정입니다.
저희는 이번 프로젝트에서 아래와 같은 API를 만들거에요!
📚 Refresh Token Project의 API 명세서
기능 | Method | URL | Reqest Header | Reqest | Respond | Respond Header |
(로그인) Refresh Token, Access Token 발급 API |
POST | localhost:3019/tokens | { "id": "64c47134ad123e335d76fff5" } | { "message": "Token이 정상적으로 발급되었습니다." } | { ”accessToken”: “eyJhbGciOiJIUzI….”, ”refreshToken”: “eyJhbGciOiJIUzI…” } | |
(사용자인증미들웨어) AccessToken 검증 API |
GET | localhost:3019/tokens/validate | { ”accessToken”: “eyJhbGciOiJIUzI….” } | {} | { "message": "64c47134ad123e335d76fff5의 Payload를 가진 Token이 성공적으로 인증되었습니다." } | |
Refresh Token을 이용해 엑세스 토큰 재발급 API | POST | localhost:3019/tokens/refresh | { ”refreshToken”: “eyJhbGciOiJIUzI…” } | {} | { "message": "Access Token을 새롭게 발급하였습니다." } | { ”accessToken”: “eyJhbGciOiJIUzI….” } |
프로젝트 구현을 시작해보겠습니다
초기화
yarn Package 설치
# yarn을 이용해 프로젝트를 초기화합니다.
yarn init -y
# express, jsonwebtoken, cookie-parser 패키지를 설치합니다.
yarn add express jsonwebtoken cookie-parser
// package.json 추가
"type": "module",
"scripts": {"dev" : "nodemon app.js"},
Refresh Token Project - app.js
// app.js
import express from 'express';
import jwt from 'jsonwebtoken';
import cookieParser from 'cookie-parser';
const app = express();
const PORT = 3019;
// 비밀 키는 외부에 노출되면 안되겠죠? 그렇기 때문에, .env 파일을 이용해 비밀 키를 관리해야합니다.
const ACCESS_TOKEN_SECRET_KEY = `HangHae99`; // Access Token의 비밀 키를 정의합니다.
const REFRESH_TOKEN_SECRET_KEY = `Sparta`; // Refresh Token의 비밀 키를 정의합니다.
app.use(express.json());
app.use(cookieParser());
app.get('/', (req, res) => {
return res.status(200).send('Hello Token!');
});
app.listen(PORT, () => {
console.log(PORT, '포트로 서버가 열렸어요!');
});
test
4) Refresh Token과 Access Token을 발급하는 API를 만들어봅니다!
👉
Express 서버가 잘 동작하는것을 확인했다면, 이번에는 사용자가 API를 호출할 때, Refresh Token과 Access Token을 발급하는 API를 만들어 보겠습니다.
app.js - Refresh Token과 Access Token을 발급 받는 API 구성
// app.js
import express from "express";
import jwt from "jsonwebtoken";
import cookieParser from "cookie-parser";
const app = express();
const PORT = 3019;
app.use(express.json());
app.use(cookieParser());
app.get("/", (req, res) => {
return res.status(200).send("Hello Token!");
});
app.listen(PORT, () => {
console.log(PORT, "포트로 서버가 열렸어요!");
});
// 비밀 키는 외부에 노출되면 안되겠죠? 그렇기 때문에, .env 파일을 이용해 비밀 키를 관리해야합니다.
// ACCESS_TOKEN, REFRESH_TOKEN의 SECRET_KEY 를 평문으로 작성
/**
* 비밀키를 두 가지로 설정한 이유는,
* 액세스 토큰이 탈취되더라도 해당 정보를 악의적인 해커가 해석해
* 리프레시 토큰에 사용하는 비밀키를 알아내는 상황을 방지하기 위함입니다.
* 이러한 가능성을 완전히 차단하기 위해 ACCESS_TOKEN과 REFRESH_TOKEN의 비밀키를 서로 다르게 설정하였습니다.
*/
const ACCESS_TOKEN_SECRET_KEY = `HangHae99`; // Access Token의 비밀 키를 정의합니다.
const REFRESH_TOKEN_SECRET_KEY = `Sparta`; // Refresh Token의 비밀 키를 정의합니다.
/**
* 원래 비밀키는 평문이 아니라 암호화 되어서 관리되어야 함
* dotenv라이브러리를 사용해 비밀키 정보들을 외부에서 관리할 수 있도록 구현해야 합니다.
*/
// 1. 클라이언트가 전달한 id에 해당하는 요청 정보를 가지고 2가지의 토큰을 만듭니다.
// 2. refreshToken을 저장하는 객체에 해당하는 사용자의 전달된 정보를 바탕으로 특정 사용자 정보를 저장합니다.
// 3. 해당 쿠키를 만든 후 클라이언트에게 전달합니다.
// tokenStorage방식으로 해당하는 객체를 한나 만듭니다.
let tokenStorage = {}; // Refresh Token을 저장할 객체
/** Access Token, Refresh Token 발급 API **/
app.post("/tokens", async (req, res) => {
// id 전달
const { id } = req.body;
// 엑세스 토큰과 리프레스 토큰을 발급
// jwt.sign()을 사용하면 JWT를 발급한다고 인지할 수 있습니다.
// 1. id 기반으로 만듭니다.
// 2. 어떤 암호화키(=비밀키)를 이용해서 해당하는 jwt를 만들 것 인가
// - accessToken에 필요한 비밀번호는 ACCESS_TOKEN_SECRET_KEY
const accessToken = jwt.sign({ id: id }, ACCESS_TOKEN_SECRET_KEY, { expiresIn: "10s" }); // 10초
// - refreshToken에 필요한 비밀번호는 REFRESH_TOKEN_SECRET_KEY
const refreshToken = jwt.sign({ id: id }, REFRESH_TOKEN_SECRET_KEY, { expiresIn: "1d" }); // 하루
//const accessToken = createAccessToken(id);
//const refreshToken = createRefreshToken(id);
///////////////////////////
// 발급받은 토큰을 서버에서 사용하기 위해
// Refresh Token을 이용해 해당하는 키를 정의합니다.( 유저의 정보를 서버에 저장합니다.)
// refreshToken이 주어지면 서버에서는 이 토큰이 가지고 있는 여러 정보를 저장합니다.
// tokenStorage의 refreshToken키에 사용자의 여러 정보를 저장합니다.
// 발급받은 Refresh Token을 이용해 서버에서 사용할 키를 정의하고, 해당 유저의 정보를 저장합니다.
// 서버는 Refresh Token에 포함된 정보를 분석하여 `tokenStorage`의 `refreshToken` 키에 저장합니다.
/////////////////////////
tokenStorage[refreshToken] = {
id: id, // 사용자에게 전달받은 ID를 저장합니다.
ip: req.ip, // 사용자의 IP 정보를 저장합니다.
userAgent: req.headers["user-agent"], // 사용자의 User Agent 정보를 저장합니다.
};
console.log(tokenStorage);
// 클라이언트에게 토큰 할당
res.cookie("accessToken", accessToken); // Access Token을 Cookie에 전달한다.
res.cookie("refreshToken", refreshToken); // Refresh Token을 Cookie에 전달한다.
return res.status(200).json({ message: "Token이 정상적으로 발급되었습니다." });
});
createAccessToken, createRefreshToken 정상 발급
값은 Bearer토큰의 형식이 아니라 바로 JWT를 할당하도록 구현했음(다른 때는 암호화 돌리세요)
토큰을 발급한 뒤에 tokenStorages에 있는 정보 조회
JWT가 키로 쓰이고 그 안에 value로 클라이언트가 저장한 정보들이 들어있습니다.
id는 Request Body에 넣은 그대로 나옵니다.
ip는 이번에 코드를 localhost로 구현했기에 왼쪽에 ipv6이, 오른쪽에는 클라이언트의 ipv4주소가 적용되었습니다.
userAgent는 어떤 사용자가 어떤 방식으로 서버에 요청을 보냈는지 확인하는 것 입니다.
'insomnia/10.2.0'를 보면 insomnia를 통해서 요청을 보낸것을 확인할 수 있습니다.
createAccessToken, createRefreshToken 코드 설명
💡 createAccessToken 함수
- 역할: Access Token 생성
- 작동 원리: set-token API 호출 시 전달받은 id를 JWT의 payload에 포함하여 Access Token을 생성.
- 용도: 인증 과정에서 사용자의 id를 확인하고 인증 진행.
💡 createRefreshToken 함수
- 역할: Refresh Token 생성 및 저장
- 작동 원리: JWT에 전달받은 id를 포함하여 Refresh Token을 생성. 생성된 Refresh Token과 관련된 사용자 정보를 tokenStorage 변수에 저장.
- 특징: 서버가 Refresh Token 정보를 보유하지 못하면 인증 실패 가능성 존재.
❓ Refresh Token의 정보는 어디서 관리해야하나요?
인 메모리(In-Memory) 방식을 사용하기 때문에 서버가 재시작 또는 종료될 경우 모든 정보가 사라지게 됩니다.
이번 예시에서 Refresh Token은 tokenStorage라는 변수에서 관리하였지만, 이 방식은 실제 프로덕션 환경에서는 사용해선 안됩니다.
실제 서비스에서는 별도의 테이블에서 Refresh Token을 저장하고 관리한답니다. 이렇게 할 경우, Refresh Token 검증 작업을 MySQL과 같은 데이터베이스를 조회함과 동시에 함께 처리할 수 있게 됩니다.
Refresh Token 테이블의 예시
Name | 타입(type) | Null | default | 비고 |
tolenId(PK) | INTEGER | NOT NULL | AUTO_INCREMENT | 토큰의 기본 키 |
userId(FK) | INTEGER | NOT NULL | 사용자의 기본 키 | |
token | STRING | NOT NULL | 리프레시 토큰 | |
expiresAT | DATETIME | NOT NULL | 만료 날짜 | |
createdAT | DATETIME | NOT NULL | 현재시 | 생성 날짜 |
이 외에도 ip 또는 user-Agent와 같은 다양한 정보를 추가할 수 있습니다
사용자가 POST /tokens API를 호출하면, 이 API는 Access Token과 Refresh Token을 각각 하나씩 발급하도록 구현하였습니다. 이부분을 로그인 기능으로 생각할 수 있습니다.
서버에서는 Refresh Token을 Key로 사용하여 입력 받은 id 와 함께 토큰을 발급한 사용자 정보를 찾을 수 있도록 구현하였습니다. 실제 프로젝트에서는, 이 id 대신에 사용자 Id를 사용한다면, 동일한 방식으로 구현할 수 있겠죠?
5) Access Token을 검증하는 API를 만들어봅니다!
서버에서 발급받은 Access Token을 검증하는 GET /tokens/validate API를 만들어보겠습니다!
GET /tokens/validate - app.js
// app. js
...
/** 엑세스 토큰 검증 API **/
app.get("/tokens/validate", (req, res) => {
// cookieParser()를 사용했기 때문에 req.cookies를 이용해 쿠키 형식을 해당하는 객체 형식으로 받을 수 있습니다.
// req.cookies의 정보를 객체 수조분해 할당을 통해 가져왔습니다.
const { accessToken } = req.cookies;
// const accessToken = req.cookies.accessToken;
// 엑세스 토큰이 존제하는지 확인
if (!accessToken) {
return res.status(400).json({ errorMessage: "Access Token이 존재하지 않습니다." });
}
// 페이로드 확인
const payload = validateToken(accessToken, ACCESS_TOKEN_SECRET_KEY);
if (!payload) {
return res.status(401).json({ errorMessage: "Access Token이 유효하지 않습니다." });
}
const { id } = payload;
return res.status(200).json({
message: `${id}의 Payload를 가진 Token이 성공적으로 인증되었습니다.`,
});
});
// Token을 검증하고 Payload를 반환(조회)합니다.
// (가져올 토큰, 비밀키)
function validateToken(token, secretKey) {
try {
const payload = jwt.verify(token, secretKey);
return payload;
} catch (error) {
return null;
}
}
쿠키 발급 후에
시간이 지나면 쿠키 이용시간 만료됨
쿠키를 사용하기 위해 발급받고 바로 인증하거나 엑세스 쿠키의 이용시간을 늘려주세요
const accessToken = jwt.sign({ id: id }, ACCESS_TOKEN_SECRET_KEY, { expiresIn: "10m" }); // 10분
✅ ValidateToken 함수 추가 설명
ValidateToken 함수는 토큰의 유효성을 검증하는 핵심 역할을 합니다:
- 비밀 키 설정: secretKey를 전달받아 서버에서 사용할 검증용 비밀 키를 설정합니다.
- 토큰 출처 확인: Access Token 또는 Refresh Token이 서버에서 발급한 것인지 검증합니다.
- 만료 여부 확인: 토큰이 만료되지 않았는지 확인합니다.
✅ GET /tokens/validate API 에서 발생할 수 있는 에러
- Access Token 누락 에러
사용자가 Cookie를 전달했으나 Access Token이 없다면 아래와 같은 에러가 반환됩니다: - { "message": "Access Token이 존재하지 않습니다." }
- Access Token 유효성 에러
사용자가 전달한 Access Token이 유효하지 않을 경우: - { "message": "Access Token이 유효하지 않습니다." }
- Access Token 인증 성공
Access Token이 정상적으로 인증되면 응답에서 페이로드 값을 확인할 수 있습니다.
6) Refresh Token으로 Access Token을 재발급하는 API를 만들어봅니다!
서버에서 발급받은 Access Token을 검증하는 GET /tokens/validate API를 만들어보겠습니다!
[코드스니펫] POST /tokens/refresh - app.js
// app. js
...
/** 리프레시 토큰 검증 API **/
app.post('/tokens/refresh', (req, res) => {
const refreshToken = req.cookies.refreshToken;
if (!refreshToken)
return res
.status(400)
.json({ errorMessage: 'Refresh Token이 존재하지 않습니다.' });
const payload = validateToken(refreshToken, REFRESH_TOKEN_SECRET_KEY);
if (!payload) {
return res
.status(401)
.json({ errorMessage: 'Refresh Token이 유효하지 않습니다.' });
}
const userInfo = tokenStorage[refreshToken];
if (!userInfo)
return res.status(419).json({
errorMessage: 'Refresh Token의 정보가 서버에 존재하지 않습니다.',
});
const newAccessToken = createAccessToken(userInfo.id);
res.cookie('accessToken', newAccessToken);
return res.json({ message: 'Access Token을 새롭게 발급하였습니다.' });
});
// Token을 검증하고 Payload를 반환합니다.
function validateToken(token, secretKey) {
try {
const payload = jwt.verify(token, secretKey);
return payload;
} catch (error) {
return null;
}
}
newAccessToken에서 새롭게 accessToken을 발급하는 부분과 위에서 accessToken을 가장 처음 발급하는 부분에 만료기간을 설정하고 accessToken을 새롭게 발급하는 같은 기능을 하는 코드가 있습니다. 함수로 따로 빼어내어 엑세스 토큰을 발급도록 수정합시다.
// - accessToken에 필요한 비밀번호는 ACCESS_TOKEN_SECRET_KEY
const accessToken = jwt.sign({ id: id }, ACCESS_TOKEN_SECRET_KEY, { expiresIn: "10m" }); // 10분
// - refreshToken에 필요한 비밀번호는 REFRESH_TOKEN_SECRET_KEY
const refreshToken = jwt.sign({ id: id }, REFRESH_TOKEN_SECRET_KEY, { expiresIn: "1d" }); // 1일
...
/** 리프레시 토큰 검증 API **/
app.post("/tokens/refresh", (req, res) => {
// ACCESS_TOKEN_SECRET_KEY으로 jwt 만들기
const newAccessToken = jwt.sign(userInfo.id, ACCESS_TOKEN_SECRET_KEY)
app.post("/tokens", async (req, res) => {
const accessToken = createAccessToken(id);
...
function creatAccessToken(id) {
return jwt.sign({ id }, ACCESS_TOKEN_SECRET_KEY, { expiresIn: "10m" }); // 10분
}
app.post("/tokens/refresh", (req, res) => {
...
// ACCESS_TOKEN_SECRET_KEY으로 jwt 만들기
const newAccessToken = createAccessToken(userInfo.id);
✅ GET /tokens/validate API 에서는 어떤 에러들이 발생하나요 ?
사용자가 Cookie를 전달할 때, Refresh Token이 없다면 에러가 발생합니다.
{ "message": "Refresh Token이 존재하지 않습니다." }
사용자가 전달한 Refresh Token이 유효하지 않을 경우 에러가 발생합니다.
{ "message": "Refresh Token이 유효하지 않습니다." }
Refresh Token이 유효하지만, 서버에 해당 토큰 정보가 없을 경우 에러가 발생합니다.
{ "message": "Refresh Token의 정보가 서버에 존재하지 않습니다." }
사용자가 Refresh Token을 이용하여 API를 호출하면, 해당 토큰의 상태에 따라 적절한 응답(Response)이 반환되는 것을 확인할 수 있습니다.
만약, Refresh Token이 정상적으로 인증되면, 사용자에게 Access Token을 담은 Cookie를 전달하는 것을 확인할 수 있게 되었습니다!
7) Insonnia로 API 테스트하기
1) Insomnia 에서 Http Request를 생성하고, POST /tokens API를 호출
POST /tokens API에서 Access Token, Refresh Token을 발급받아 봅시다!
64c47134ad123e335d76fff5의 id를 가지도록 토큰을 발행합니다.
정상적으로 토큰이 발급되었다는 API Response를 받았습니다.
accessToken, refreshToken의 Key를 가지는 Cookie를 전달 받았습니다.
POST /tokens API를 호출했을 때, “Token이 정상적으로 발급되었습니다.”는 응답을 확인할 수 있습니다.
Cookies 탭에서 accessToken, refreshToken의 Key를 가지는 쿠키를 전달 받은것을 확인할 수 있습니다.
2) Insomnia에서 Http Request를 생성하고, GET /tokens/validate API를 호출
POST /tokens API에서 전달받은 Access Token을 이용해서 GET /tokens/validate API를 호출해보도록 하겠습니다!
GET /tokens/validate를 호출합니다.
토큰에 상태에 따라 변경되는 API Response를 확인할 수 있습니다.
Access Token이 재발행되었다는 Response와 함께, accessToken 쿠키를 전달받았습니다.
처음으로 GET /tokens/validate API를 호출하였을때, “Access Token이 유효하지 않습니다.”라는 메시지가 출력된 것을 확인할 수 있습니다.
이상하게도, 저희는 POST /tokens API에서 Access Token을 발급받았는데, 왜 유효하지 않다는 에러 메시지가 출력된 걸까요?
그 이유는 저희가 Access Token을 생성할 때, 10초의 만료기간을 설정했기 때문입니다. 토큰을 확인하는 과정에서 10초이상의 시간이 소요되어 Access Token이 만료되었고, 그 결과 에러 메시지가 출력된 것입니다.
따라서, Access Token의 만료 기간을 더 길게 설정하거나, 만료 기간이 되기 전에 GET /tokens/validate API를 호출하면, Access Token에 저장된 페이로드를 전달받을 수 있을 것입니다.
만료 기간이 도달하기전, GET /tokens/validate API를 호출하면, 인증에 성공합니다.
3) Insomnia에서 Http Request를 생성하고, POST /tokens/refresh API를 호출
POST /tokens API에서 전달받은 Refresh Token을 이용해서 POST /tokens/refresh API를 호출해보도록 하겠습니다!
POST /tokens/refresh를 호출합니다.
토큰에 상태에 따라 변경되는 API Response를 확인할 수 있습니다.
Access Token이 재발행되었다는 Response와 함께, accessToken 쿠키를 전달받았습니다.
Refresh Token을 서버에 전달하게 된다면, Access Token을 정상적으로 전달받을 수 있습니다.
만약, 만료 기간이 지난 Refresh Token이면, 에러 메시지를 전달 받겠죠?
8) 그래서 우리는 어떤 인증 방식을 사용해야하나요?
개발을 진행하다보면, 어떤 기술이든 모든 상황에서 강점을 가지는 기술은 없다는 것을 알게됩니다. 이런 이유로 새로운 기술을 도입할 때는 현재 상황에 가장 알맞는 최적의 선택을 하는것이 중요합니다.
Access Token과 Refresh Token을 비교해보면, 만약 프로젝트를 신속하게 구현해야 하거나 사용자의 요청에 대한 인증을 최소화하려는 목표가 있다면 Access Token만 사용할 것입니다. 반면에 보안성을 중요하게 여기고 서버를 더욱 견고하게 구성해야한다면 Refresh Token의 사용을 고려할 것입니다.
'내일배움 강의 > 강의- Node.js 입문, 숙련' 카테고리의 다른 글
Node.js 숙련주차 4.1 인증(Authentication), 인가(Authorization) - 나만의 게시판 사이트 (1) | 2024.12.02 |
---|---|
Node.js 숙련주차 3.7 나만의 게시판 사이트 설계 - DB 테이블 만들기 (0) | 2024.12.02 |
Node.js 숙련주차 3.6 JWT(Json Web Token) (1) | 2024.12.01 |
Node.js 숙련주차 3.5 쿠키(Cookie)와 세션(Session) (1) | 2024.12.01 |
Node.js 숙련주차 3.3 SQL과 제약 조건(SQL 강의와 겹침) (2) | 2024.11.27 |