
๐ฏ JWT ๊ธฐ๋ฐ์ผ๋ก ์ธ์ฆ, ์ธ๊ฐํ๋ ๊ฐ๋ ์ ์ ๋ฆฌํ๊ณ , ์ฝ๋์ ๋ฐ์ํฉ๋๋ค.
์ฌ์ฉ์๊ฐ ์ฌ์ดํธ์ ๊ฐ์ ๋ ์ฌ์ฉ์์์ ์ฆ๋ช ํ๋ ๊ณผ์ ์ ๋๋ค.
์ธ์ฆ๋ ์ฌ์ฉ์๊ฐ ํน์ ํ์ด์ง๋ ๊ธฐ๋ฅ์ ์ ๊ทผํ ๊ถํ์ด ์๋์ง ํ์ธํ๋ ๊ณผ์ ์ ๋๋ค.
๋ธ๋ผ์ฐ์ ๊ฐ ์๋ฒ์์ ๋ฐ์ ์์ ๋ฐ์ดํฐ ์กฐ๊ฐ(key-value)์ ์ ์ฅํด๋๊ณ , ์ดํ ๊ฐ์ ์๋ฒ์ ์์ฒญํ ๋ ์๋์ผ๋ก ๋ค์ ๋ณด๋ด์ค๋๋ค.
๋ก๊ทธ์ธ โ ์๋ฒ๊ฐ ์ฟ ํค ์์ฑ (์: userId=123)
์๋ฒ๊ฐ ์ฟ ํค๋ฅผ ๋ธ๋ผ์ฐ์ ์ ์ ์ก (Set-Cookie)
๋ธ๋ผ์ฐ์ ๋ ์ฟ ํค๋ฅผ ์ ์ฅํ๊ณ ์ดํ ์์ฒญ๋ง๋ค ์ฟ ํค ์๋ ์ ์ก
์๋ฒ๋ ์ฟ ํค ๊ฐ์ ์ฝ์ด์ ์ ์ ์๋ณ or ์ค์ ๊ฐ ํ์ธ
๋ธ๋ผ์ฐ์ ์ ์ ์ฅ๋จ (Client-side)
๊ธฐ๋ณธ์ ์ผ๋ก ์์ฒญ๋ง๋ค ์๋ ์ ์ก๋จ โ ๋ณด์ ์ฃผ์ ํ์
์ฉ๋ ์ ํ ์์ (๋๋ฉ์ธ๋น 4KB๊น์ง)
์น ํ์ด์ง์ F12๋ฅผ ๋๋ฌ ๊ฐ๋ฐ์ ๋๊ตฌ ์ฐฝ > Application > Cookies ์์ ์ฟ ํค๋ฅผ ํ์ธํ ์ ์์ต๋๋ค.

Expires ํญ์์ Session์ด๋ผ๊ณ ์ ํ์๋ ์ฟ ํค๋ ์ธ์
์ฟ ํค๋ฅผ ๋ปํฉ๋๋ค.

์ฟ ํค์ ๋ณด์ ๋ฌธ์ ๋ฅผ ๋ณด์ํ๊ธฐ ์ํด ๋ฑ์ฅํ ๋ฐฉ์์ผ๋ก ์๋ฒ๊ฐ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ์๋ฒ ๋ฉ๋ชจ๋ฆฌ/DB์ ์ ์ฅํ๊ณ , ๋ธ๋ผ์ฐ์ ์๋ ๋จ์ํ ์ธ์ ID(Session ID)๋ง ์ฟ ํค๋ก ๋ฐ๊ธํ๋ ๋ฐฉ์์ ๋๋ค.
์ฌ์ฉ์๊ฐ ๋ก๊ทธ์ธํ๋ฉด ์๋ฒ๊ฐ ํน์ ์ ๋ณด๋ฅผ ์ ์ฅํ๋ ์ธ์ ์ ์์ฑํฉ๋๋ค.
์ธ์ ID๋ฅผ ์ฟ ํค๋ก ์ฌ์ฉ์์๊ฒ ์ ๋ฌํฉ๋๋ค.
์ดํ ์ฌ์ฉ์๋ ์์ฒญ ์ ์ธ์ ID๋ฅผ ํฌํจํ์ฌ ์๋ฒ์ ํต์ ํฉ๋๋ค.
๋น๊ต์ ๋ณด์์ด ๋ฐ์ด๋ฉ๋๋ค. (์ฟ ํค๋ณด๋ค ์์ )
์๋ฒ์์ ์ ์ ์ํ๋ฅผ ๊ธฐ์ต(Stateful) โ ์๋ฒ์ ์ ์ฅ์ ํ์
JWT๋ JSON ํ์์ ๋ฐ์ดํฐ๋ฅผ ์์ ํ๊ฒ ์ ์กํ๋ ํ ํฐ์ผ๋ก, ์ธ์ฆ๊ณผ ์ธ๊ฐ์ ์์ฃผ ์ฌ์ฉ๋ฉ๋๋ค.
ํด๋ผ์ด์ธํธ๊ฐ ์๋ฒ์ ๋ก๊ทธ์ธ ์์ฒญ์ ๋ณด๋ ๋๋ค.
์๋ฒ๊ฐ ์ฌ์ฉ์์ ๋ก๊ทธ์ธ ์ ๋ณด๋ฅผ ํ์ธํฉ๋๋ค.
์๋ฒ๊ฐ ๋ก๊ทธ์ธ ์ ๋ณด๊ฐ ์ ํจํ๋ฉด JWT๋ฅผ ์์ฑํ์ฌ ํด๋ผ์ด์ธํธ์ ์ ๋ฌํฉ๋๋ค.
ํด๋ผ์ด์ธํธ๊ฐ ๋ฐ์ JWT๋ฅผ ์ ์ฅํ ์ดํ ์๋ฒ์ ์์ฒญ์ ๋ณด๋ผ ๋ JWT๋ฅผ ํฌํจํ์ฌ ์ ์กํฉ๋๋ค.
์๋ฒ๋ ๋ฐ์ JWT์ ์ ํจ์ฑ์ ๊ฒ์ฆํ ํ ์์ฒญ์ ์ฒ๋ฆฌํ๊ณ ์๋ตํฉ๋๋ค.
[๐ฅ๏ธํด๋ผ์ด์ธํธ] [๐ข์๋ฒ]
| |
1๏ธโฃ ๋ก๊ทธ์ธ ์์ฒญ -------------------> 2๏ธโฃ ๋ก๊ทธ์ธ ์ ๋ณด ํ์ธ
| |
| <---------------------------- 3๏ธโฃ JWT ๋ฐ๊ธ ๋ฐ ์ ๋ฌ
| |
4๏ธโฃ ์์ฒญ ์ JWT ํฌํจ ---------------> 5๏ธโฃ ์๋ฒ์์ JWT ๊ฒ์ฆ ๋ฐ ์๋ต
|
(๐ ์ธ์ฆ๋ ์ํ ์ ์ง)
๋ณ์กฐ๋ฅผ ๊ฐ์งํ ์ ์์ด ๋ณด์์ฑ์ด ๋ฐ์ด๋ฉ๋๋ค.
์๋ฒ๊ฐ ๋ณ๋๋ก ์ ๋ณด๋ฅผ ์ ์ฅํ์ง ์์๋ ๋๋ฏ๋ก Statelessํ ๊ตฌ์กฐ์ ๋๋ค.
๋ณ๋์ ํ ํฐ ๋ฐ๊ธ ์๋ฒ๋ฅผ ์ด์ํ ์ ์์ต๋๋ค.
JWT๋ ํค๋(Header), ํ์ด๋ก๋(Payload), ์๋ช (Signature)์ผ๋ก ๊ตฌ์ฑ๋ฉ๋๋ค.
๊ฐ ๋ถ๋ถ์ .์ผ๋ก ๊ตฌ๋ถ๋๋ฉฐ, ๋ค์๊ณผ ๊ฐ์ ํ์์
๋๋ค.
์ธ์ฝ๋ฉ ๊ฐ
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30
๐ jwt.io ์์ JWT ๊ตฌ์กฐ๋ฅผ ํ์ธํ ์ ์์ต๋๋ค.
JWT์ ํ์ ๊ณผ ์ํธํ ์๊ณ ๋ฆฌ์ฆ ์ ๋ณด๋ฅผ ํฌํจํฉ๋๋ค.
{
"alg": "HS256",
"typ": "JWT"
}
alg : ์ฌ์ฉ๋ ํด์ฑ ์๊ณ ๋ฆฌ์ฆ
typ : ํ ํฐ ํ์
์ฌ์ฉ์ ์ ๋ณด ๋ฐ ํด๋ ์(๊ถํ ๋ฑ)์ด ํฌํจ๋ฉ๋๋ค.
{
"sub": "1234567890",
"name": "John Doe",
"admin": true,
"iat": 1516239022
}
sub : ์ฌ์ฉ์ ID
name : ์ฌ์ฉ์ ์ด๋ฆ
admin : ๊ด๋ฆฌ์ ์ฌ๋ถ
iat(Issued At) : ํ ํฐ ๋ฐํ ์๊ฐ์ ๋ํ๋
๋๋ค.
JWT ๋ณ์กฐ ๋ฐฉ์ง๋ฅผ ์ํ ์๋ช (Signature) ์ ๋ณด์ ๋๋ค.
a-string-secret-at-least-256-bits-long
$ npm install jsonwebtoken dotenv cookie-parser
const express = require('express');
const router = express.Router();
const conn = require('../mariadb');
const { body, validationResult } = require('express-validator');
const jwt = require('jsonwebtoken');
const dotenv = require('dotenv');
dotenv.config();
router.use(express.json());
const validate = (req, res, next) => {
const err = validationResult(req);
if (err.isEmpty()) {
return next();
} else {
return res.status(400).json(err.array());
}
};
router.post(
'/login',
[
body('email').notEmpty().isEmail().withMessage('์ด๋ฉ์ผ ์
๋ ฅ ํ์'),
body('password').notEmpty().isString().withMessage('๋น๋ฐ๋ฒํธ ์
๋ ฅ ํ์'),
validate,
],
(req, res) => {
const { email, password } = req.body;
const sql = 'SELECT * FROM users WHERE email =?';
conn.query(sql, email, (err, results) => {
if (err) {
return res.status(400).end();
}
let loginUser = results[0];
if (loginUser && loginUser.password === password) {
const token = jwt.sign(
{
email: loginUser.email,
name: loginUser.name,
},
process.env.PRIVATE_KEY,
{
expiresIn: '30m',
issuer: 'chickenmandu',
}
);
res.cookie('token', token, { httpOnly: true });
res.status(200).json({ message: `${loginUser.name}๋ ํ์ํฉ๋๋ค.` });
} else {
res.status(403).json({
message: '์ด๋ฉ์ผ ๋๋ ๋น๋ฐ๋ฒํธ๊ฐ ํ๋ ธ์ต๋๋ค.',
});
}
});
}
);
loginUser๊ฐ ์กด์ฌํ๊ณ ์
๋ ฅํ ๋น๋ฐ๋ฒํธ๊ฐ DB์ ๋น๋ฐ๋ฒํธ์ ์ผ์นํ๋ฉด ๋ก๊ทธ์ธ ์ฑ๊ณตํ๊ฒ ๋์ด jwt.sign()์ ์ฌ์ฉํด JWT ๋ฐ๊ธํฉ๋๋ค.
email, name : ํ ํฐ์ ํฌํจํ ์ฌ์ฉ์ ์ ๋ณด์
๋๋ค.
process.env.PRIVATE_KEY : .env ํ์ผ์ ์ ์ฅ๋ ๋น๋ฐํค๋ก ์๋ช
ํฉ๋๋ค.
expiresIn: '30m' : ํ ํฐ ๋ง๋ฃ ์๊ฐ์ 30๋ถ์ผ๋ก ์ค์ ํฉ๋๋ค.
issuer: 'chickenmandu' : ํ ํฐ ๋ฐ๊ธ์๋ฅผ ์ค์ ํฉ๋๋ค.
res.cookie('token', token, { httpOnly: true }) : HTTP ์ ์ฉ ์ฟ ํค๋ก JWT ์ ์ฅํฉ๋๋ค.
Name : ์๋ฒ์์ token์ด๋ผ๋ ์ด๋ฆ์ ์ฟ ํค๋ฅผ ์ค์
Value : JWT(ํ ํฐ) ๊ฐ์ด ์ค์ ๋์ด ์์
Domain : ๋ก์ปฌ ํ๊ฒฝ์์ ์ฟ ํค๊ฐ ์ค์ ๋จ
Path: ๋ชจ๋ ๊ฒฝ๋ก์์ ์ด ์ฟ ํค๋ฅผ ์ฌ์ฉํ ์ ์์
Expires : ๋ง๋ฃ๊ฐ ์ธ์
์ฟ ํค๋ก ์ค์ ๋จ (๋ธ๋ผ์ฐ์ ๋ฅผ ๋ซ์ผ๋ฉด ์ญ์ ๋จ)
HTTPOnly : ๋ธ๋ผ์ฐ์ ์ JavaScript๋ก ์ ๊ทผ ๋ถ๊ฐ๋ฅ โ XSS ๊ณต๊ฒฉ ๋ฐฉ์ง
Secure : HTTPS ํ๊ฒฝ์์๋ง ์ฟ ํค ์ ์ก ๊ฐ๋ฅ โ ๋คํธ์ํฌ ์ค๋ํ ๋ฐฉ์ง
๐ค์ JWT๋ฅผ ์ฟ ํค๐ช์ ๋ด๋ ๊ฑธ๊น?
JWT๋ฅผ ์ ์ฅํ ์ ์๋ ๋ฐฉ๋ฒ(
localStorage,sessionStorage,Cookie)์ ์ฌ๋ฌ๊ฐ์ง์ง๋ง, ์ฟ ํค๋ฅผ ์ฌ์ฉํ๋ ์ด์ ๋ ๋ณด์๊ณผ ํธ์์ฑ ๋๋ฌธ์ ๋๋ค.
- XSS ๊ณต๊ฒฉ ๋ฐฉ์ด (HttpOnly ์์ฑ)
- ์๋ ์ธ์ฆ ๊ด๋ฆฌ (์์ฒญ ์ ์๋์ผ๋ก ํฌํจ๋จ)
- ๋ณด์ ๊ฐํ ๊ฐ๋ฅ
- ๋ธ๋ผ์ฐ์ ๊ธฐ๋ณธ ๊ธฐ๋ฅ ํ์ฉ ๊ฐ๋ฅ
| ๊ตฌ๋ถ | ์ฟ ํค (Cookie) | ์ธ์ (Session) | JWT (Json Web Token) |
|---|---|---|---|
| ์ ์ฅ ์์น | ๋ธ๋ผ์ฐ์ (Client) | ์๋ฒ(์ธ์ ์ ์ฅ์) + ๋ธ๋ผ์ฐ์ (์ธ์ ID) | ๋ธ๋ผ์ฐ์ (LocalStorage, SessionStorage, Cookie ๋ฑ) |
| ์๋ฒ ์ํ ์ ์ง ์ฌ๋ถ | โ Stateless | โ Stateful | โ Stateless |
| ์ธ์ฆ ๋ฐฉ์ | ์ฟ ํค ์์ฒด์ ๋ฐ์ดํฐ ์ ์ฅ | ์ธ์ ID๋ง ์ฟ ํค์ ์ ์ฅ โ ์๋ฒ์์ ์ ์ ์ํ ์กฐํ | ํ ํฐ ์์ฒด์ ์ ์ ์ ๋ณด ํฌํจ โ ์๋ฒ๋ ๊ฒ์ฆ๋ง |
| ํ์ฅ์ฑ(Scale) | ๋์ | ๋ฎ์ (์ธ์ ํด๋ฌ์คํฐ๋ง ํ์) | ๋งค์ฐ ๋์ (๋ถ์ฐ ์๋ฒ ํ๊ฒฝ์ ์ ๋ฆฌ) |
| ๋ณด์ ์ํ | ์ฟ ํค ํ์ทจ(XSS) | ์ธ์ ID ํ์ทจ (CSRF) | ํ ํฐ ์ ์ถ ์ ์ํ (Payload ์์ ์ ๋ณด ๋ ธ์ถ ๊ฐ๋ฅ) |
| ๋ง๋ฃ ๊ด๋ฆฌ | ์ฟ ํค ๋ง๋ฃ์ผ ์ค์ | ์๋ฒ์์ ์ธ์ ๋ง๋ฃ ์ฒ๋ฆฌ | ํ ํฐ ๋ง๋ฃ ์๊ฐ(exp) ์ค์ + Refresh Token ์ฌ์ฉ |
| ์ฅ์ | ๊ฐ๋จํ๊ณ ๋น ๋ฆ | ์ ์ ์ํ ๊ด๋ฆฌ ์ฉ์ด | ์๋ฒ ํ์ฅ์ ์ ๋ฆฌ, ์ํ ์ ์ฅ ๋ถํ์ |
| ๋จ์ | ๋ณด์์ ์ทจ์ฝ | ์๋ฒ ์์ ์ฌ์ฉ ๋ง์ | ํ ํฐ ํ์ทจ ์ ์ํ, ํ ํฐ ๊ธธ์ด๊ฐ ๊ธธ์ด์ง |
์ธ์ฆ๊ณผ ์ธ๊ฐ์ ๋ํ ์ฐจ์ด๋ฅผ ์ ๋ฆฌํ๊ณ , JWT๋ฅผ ํ์ฉํ ๋ก๊ทธ์ธ ๊ตฌํ๊ณผ ๋ณด์ ์์๊น์ง ๊ณ ๋ คํ ์ฝ๋๋ฅผ ์ง๋ฉด์, ๋ณด์์ ๋ํด ๋ ๊ณต๋ถํด์ผ๊ฒ ๋ค๊ณ ์๊ฐํ์ต๋๋ค.
