JWT
token 인증 방식
- 토큰을 사용하면 사용자의 인증 정보를 서버가 아닌 클라이언트 측에 저장할 수 있습니다. 이를 통해 서버 부하, 메모리 부족 문제를 해결할 수 있습니다.
- 토큰은 교통 승차권과 같이 무언가를 이용할 수 있는 권한이나 자격을 나타내는, 일종의 증표(지하철 승차권, 사무실 출입 카드)입니다. 웹 보안에서의 토큰은 인증(Authentication)과 권한(Authorization) 정보를 담고 있는 암호화된 문자열을 말합니다. 이를 이용해 특정 애플리케이션에 대한 사용자의 접근 권한을 부여합니다.
토큰 인증 방식의 흐름

-
사용자가 인증 정보를 담아 서버에 로그인 요청을 보냅니다.
-
서버는 데이터베이스에 저장된 사용자의 인증 정보를 확인합니다.
-
인증에 성공했다면 해당 사용자의 인증 및 권한 정보를 서버의 비밀 키와 함께 토큰으로 암호화합니다.
-
생성된 토큰을 클라이언트로 전달합니다. HTTP 상에서 인증 토큰을 보내기 위해 사용하는 헤더인 Authorization 헤더를 사용하거나, 쿠키로 전달하는 방법을 사용합니다.
-
클라이언트는 전달받은 토큰을 저장합니다. 저장하는 위치는 Local Storage, Session Storage, Cookie 등 다양합니다.
-
클라이언트가 서버로 리소스를 요청할 때 토큰을 함께 전달합니다. 토큰을 보낼 때에도 Authorization 헤더를 사용하거나 쿠키로 전달할 수 있습니다.
-
서버는 전달받은 토큰을 서버의 비밀 키를 통해 검증합니다. 이를 통해 토큰이 위조되었는지 혹은 토큰의 유효 기간이 지나지 않았는지 등을 확인할 수 있습니다.
-
토큰이 유효하다면 클라이언트의 요청에 대한 응답 데이터를 전송합니다.
토큰 인증 방식의 장점
- 무상태성 : 서버가 유저의 인증 상태를 관리하지 않습니다. 서버는 비밀 키를 통해 클라이언트에서 보낸 토큰의 유효성만 검증하면 되기 때문에 무상태적인 아키텍처를 구축할 수 있습니다.
- 확장성 : 다수의 서버가 공통된 세션 데이터를 가질 필요가 없다는 것도 토큰 기반 인증의 장점입니다. 이를 통해 서버를 확장하기 더 용이합니다.
- 어디서나 토큰 생성 가능 : 토큰의 생성과 검증이 하나의 서버에서 이루어지지 않아도 되기 때문에 토큰 생성만을 담당하는 서버를 구축할 수 있습니다. 이를 잘 활용하면 여러 서비스 간의 공통된 인증 서버를 구현할 수 있습니다.
- 권한 부여에 용이 : 토큰은 인증 상태, 접근 권한 등 다양한 정보를 담을 수 있기 때문에 사용자 권한 부여에 용이합니다. 이를 활용해 어드민 권한 부여 및 정보에 접근할 수 있는 범위도 설정
토큰 인증 방식의 단점
- 무상태성 : 인증 상태를 관리하는 주체가 서버가 아니므로, 토큰이 탈취되어도 해당 토큰을 강제로 만료시킬 수 없습니다. 따라서 토큰이 만료될 때까지 사용자로 가장해 계속해서 요청을 보낼 수 있습니다.
- 유효 기간 : 토큰이 탈취되는 상황을 대비해서 유효 기간을 짧게 설정하면, 사용자는 토큰이 만료될 때마다 다시 로그인을 진행해야 하기 때문에 좋지 않은 사용자 경험을 제공합니다. 그렇다고 유효 기간을 길게 설정하면 토큰이 탈취될 경우 더 치명적으로 작용할 수 있습니다.
- 토큰의 크기 : 토큰에 여러 정보를 담을 수 있는 만큼, 많은 데이터를 담으면 그만큼 암호화하는 과정도 길어지고 토큰의 크기도 커지기 때문에 네트워크 비용 문제가 생길 수 있습니다.
JWT(JSON WEB TOKEN)
토큰 기반 인증 구현 시 대표적으로 사용하는 기술로 JWT(JSON Web Token)가 있습니다. JWT는 JSON 객체에 정보를 담고 이를 토큰으로 암호화하여 전송할 수 있는 기술입니다. 클라이언트가 서버에 요청을 보낼 때, 인증정보를 암호화된 JWT 토큰으로 제공하고, 서버는 이 토큰을 검증하여 인증정보를 확인할 수 있습니다.
JWT의 구성
JWT는 다음 그림과 같이 **.
으로 나누어진 세 부분이 존재하며 각각을 Header, Payload, Signature라고 부릅니다.


- Header
Header에는 마치 HTTP의 헤더처럼 해당 토큰 자체를 설명하는 데이터가 담겨 있습니다. 토큰의 종류, 그리고 시그니처를 만들 때 사용할 알고리즘을 JSON 형태로 작성합니다.
{
"alg": "HS256",
"typ": "JWT"
}
참고로, base64 방식은 원한다면 얼마든지 디코딩할 수 있는 인코딩 방식입니다. 따라서 비밀번호와 같이 노출되어서는 안 되는 민감한 정보를 담지 않도록 해야 합니다. 다음 링크에서 직접 사용해 볼 수도 있습니다. 링크
- Payload
Payload는 HTTP의 페이로드와 마찬가지로 전달하려는 내용물을 담고 있는 부분입니다. 어떤 정보에 접근 가능한지에 대한 권한, 유저의 이름과 같은 개인정보, 토큰의 발급 시간 및 만료 시간 등의 정보들을 JSON 형태로 담습니다.
{
"sub": "someInformation",
"name": "phillip",
"iat": 151623391
}
- Signature
Signature는 토큰의 무결성을 확인할 수 있는 부분입니다. Header와 Payload가 완성되었다면, Signature는 이를 서버의 비밀 키(암호화에 추가할 salt)와 Header에서 지정한 알고리즘을 사용하여 해싱합니다.
예를 들어, 만약 HMAC SHA256 알고리즘을 사용한다면 Signature는 아래와 같은 방식으로 생성됩니다.
HMACSHA256(base64UrlEncode(header) + '.' + base64UrlEncode(payload), secret);
액세스 토큰(Access Token)과 리프레시 토큰(Refresh Token)
토큰 인증의 한계를 극복하기 위해 다양한 방법들이 고안되었지만 이 중 대표적인 구현 방법은 액세스 토큰과 리프레시 토큰을 함께 사용하는 것입니다.
- 액세스 토큰(Access Token)
액세스 토큰은 말 그대로 서버에 접근하기 위한 토큰으로 앞서 다룬 토큰과 비슷한 역할을 합니다. 따라서 보안을 위해 보통 24시간 정도의 짧은 유효기간이 설정되어 있습니다.
- 리프레시 토큰(Refresh Token)
리프레시 토큰은 서버 접근을 위한 토큰이 아닌 액세스 토큰이 만료되었을 때 새로운 액세스 토큰을 발급받기 위해 사용되는 토큰입니다. 따라서 리프레시 토큰은 액세스 토큰보다 긴 유효기간을 설정합니다.


이렇게 두 가지의 각기 다른 토큰을 사용하는 경우, 액세스 토큰이 만료되더라도 리프레시 토큰의 유효기간이 남아있다면 사용자는 다시 로그인을 할 필요 없이 지속해서 인증 상태를 유지할 수 있습니다.
물론 리프레시 토큰의 도입도 모든 문제를 해결해주진 않습니다. 리프레시 토큰은 긴 유효 기간을 가지고 있어 해당 토큰마저 탈취된다면 토큰의 긴 유효 기간 동안 악의적인 유저가 계속해서 액세스 토큰을 생성하고 사용자의 정보를 해킹할 수도 있기 때문입니다. 이를 대비하기 위해 리프레시 토큰을 세션처럼 서버에 저장하고 이에 대한 상태를 관리하기도 합니다. 따라서 보안과 사용자 경험 사이의 적절한 균형을 찾는 것이 중요하다.
token 실습(로그인 유지 구현 with jwt)
📦fe-sprint-auth-cookie
┃
┗ 📂server
┃ ┣ 📂controllers
┃ ┃ ┣ 📂helper
┃ ┃ ┃ ┣ 📜tokenFunctions.js #(token 생성과 token 유효성 검사)
┃ ┃ ┣ 📂users
┃ ┃ ┃ ┣ 📜login.js
┃ ┃ ┃ ┣ 📜logout.js
┃ ┃ ┃ ┗ 📜userInfo.js
┃ ┃ ┗ 📜index.js
┃ ┣ 📂db
┃ ┃ ┗ 📜data.js
┃ ┣ 📂__test__
┃ ┃ ┗ 📜server.test.js
┃ ┣ 📜.gitignore
┃ ┣ 📜index.js
┃ ┣ 📜multi-reporters.json
┃ ┣ 📜package-lock.json
┃ ┗ 📜package.json
┃ ┣ 📜.env
tokenFunctions.js
의 파일 구조는 다음과 같습니다.
require('dotenv').config();
const { sign, verify } = require('jsonwebtoken');
module.exports = {
generateToken: (user, checkedKeepLogin) => {
const payload = {
id: user.id,
email: user.email,
};
let result = {
accessToken: sign(payload, process.env.ACCESS_SECRET, {
expiresIn: '1d', // 1일간 유효한 토큰을 발행합니다.
}),
};
if (checkedKeepLogin) {
result.refreshToken = sign(payload, process.env.REFRESH_SECRET, {
expiresIn: '7d', // 일주일간 유효한 토큰을 발행합니다.
});
}
return result;
},
verifyToken: (type, token) => {
let secretKey, decoded;
switch (type) {
case 'access':
secretKey = process.env.ACCESS_SECRET;
break;
case 'refresh':
secretKey = process.env.REFRESH_SECRET;
break;
default:
return null;
}
try {
decoded = verify(token, secretKey);
} catch (err) {
console.log(`JWT Error: ${err.message}`);
return null;
}
return decoded;
},
};
-
Client에서 로그인 데이터 보내기
-
Server에서 Client에서 받은 데이터를 활용해서 token 및 쿠키 생성
- 서버에서 받아온 정보 (userId, userPassword, checkedKeepLogin) 데이터를 활용해서 토큰과 그에 따른 쿠키를 생성합니다. checkedKeepLogin의 값이 true일 경우 access token과 refresh token을 모두 생성하고 각각 쿠키로 브라우저에게 전달합니다. checkedKeepLogind이 false일 경우 access token 만을 생성합니다. 이를 redirect하여 /userinfo url로 전달합니다.
// server/controller/users/login.js
const { USER_DATA } = require('../../db/data');
// JWT는 generateToken으로 생성할 수 있습니다. 먼저 tokenFunctions에 작성된 여러 메서드들의 역할을 파악하세요.
const { generateToken } = require('../helper/tokenFunctions');
module.exports = async (req, res) => {
const { userId, password } = req.body.loginInfo;
const { checkedKeepLogin } = req.body;
// user 데이터 가지고 오기
const userInfo = {
...USER_DATA.filter(
user => user.userId === userId && user.password === password,
)[0],
};
//cookie 옵션 설정
const cookiesOption = {
domain: 'localhost',
path: '/',
secure: true,
httpOnly: true,
sameSite: 'strict',
};
// user 데이터를 받아오지 못했을 때
if (userInfo.id === undefined) {
return res.status(401).send('Not Authorized');
}
//checkedKeepLogin === true일때
else if (userInfo.id !== undefined && checkedKeepLogin === true) {
//accessToken, refreshToken 모두 발급
const { accessToken, refreshToken } = generateToken(
userInfo,
checkedKeepLogin,
);
// 쿠키 만들기
res.cookie('access_jwt', accessToken, {
...cookiesOption,
maxAge: 1000 * 60 * 60 * 24,
expires: new Date(Date.now() + 1000 * 60 * 60 * 24),
});
res.cookie('refresh_jwt', refreshToken, {
...cookiesOption,
maxAge: 1000 * 60 * 60 * 24 * 7,
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7),
});
}
//checkedKeepLogin === false일때
else {
//accessToken만 발급
const { accessToken } = generateToken(userInfo, checkedKeepLogin);
// 쿠키 만들기
res.cookie('access_jwt', accessToken, cookiesOption);
}
return res.redirect('/userinfo');
};
-
- userInfo에서 redirect 처리하기
-
/userInfo
에서 req.cookies를 확인하면/login
라우터에서 생성한 쿠키를 확인할 수 있습니다. 이제는 쿠키의 상황에 따라 분기 처리를 진행해야 합니다.- 쿠키의 token값을 decode화를 진행합니다. (access token) 이때 정상적으로 decode화가 되었다면, token의 데이터를 활용해서 유저 데이터를 출력합니다.
- 만약 access token이 만료되었다면, refresh token을 활용해서 access token 갱신 작업이 이루어져야 합니다.
- access token, refresh token이 모두 만료되었거나 없으면, login 페이지로 다시 전송해야 합니다.
// server/controller/users/userinfo.js
const { USER_DATA } = require('../../db/data');
const { verifyToken, generateToken } = require('../helper/tokenFunctions');
module.exports = async (req, res) => {
// Cookie 옵션
const cookiesOption = {
domain: 'localhost',
path: '/',
secure: true,
httpOnly: true,
sameSite: 'strict',
};
// login 라우터에서 발급한 cookie 모두 가지고 오기
const { access_jwt, refresh_jwt } = req.cookies;
// access token decode 진행
const data = verifyToken('access', access_jwt);
// access token이 만료되었거나, 존재하지 않는다면 --- 2번
if (data === null) {
// refresh 토큰 decode화 진행
const refresh = verifyToken('refresh', refresh_jwt);
console.log('refresh');
// refresh token 이 발급되지 않았거나 만료되었다면 ---- 3번
if (refresh === null) {
return res.status(401).send('Not Authorized');
}
// 2번에서 refrech token decode가 진행되었고 새로운 accessToken 발급 및 쿠키 최신화
const accessToken = generateToken({ id: refresh.id, email: refresh.email });
res.cookie('access_jwt', accessToken, cookiesOption);
// 유저 정보 찾아내기 --> refresh token에서 진행
const userInfo = {
...USER_DATA.filter(
user => user.id === refresh.id && user.email === refresh.email,
)[0],
};
delete userInfo.password;
//Client에 data 전달
return res.send(userInfo);
}
// access token이 만료되지 않았고 일치한다면 유저 데이터 추출 ---- 1번
const userInfo = {
...USER_DATA.filter(
user => user.id === data.id && user.email === data.email,
)[0],
};
// 비밀번호 삭제
delete userInfo.password;
return res.send(userInfo);
};
- logout 기능 구현
logout 기능은 access token key와 refresh token key를 가진 cookie를 모두 제거하는 것으로 해결할 수 있습니다.
// server/controller/users/logout.js
module.exports = (req, res) => {
const cookiesOption = {
domain: 'localhost',
path: '/',
secure: true,
httpOnly: true,
sameSite: 'strict',
};
res.clearCookie('access_jwt', cookiesOption);
res.clearCookie('refresh_jwt', cookiesOption);
return res.status(205).send('DONE');
};
참조
- token 인증 방식
- 토큰 인증 방식의 흐름
- - 토큰 인증 방식의 장점
- - 토큰 인증 방식의 단점
- JWT(JSON WEB TOKEN)
- - JWT의 구성
- - 액세스 토큰(Access Token)과 리프레시 토큰(Refresh Token)
- token 실습(로그인 유지 구현 with jwt)
- 참조