JWT(JSON Web Token)
: ๋ฐ์ดํฐ๊ฐ JSON์ผ๋ก ์ด๋ฃจ์ด์ ธ ์๋ ํ ํฐ์ ์๋ฏธํฉ๋๋ค.
๋ ๊ฐ์ฒด๊ฐ ์๋ก ์์ ํ๊ฒ ์ ๋ณด๋ฅผ ์ฃผ๊ณ ๋ฐ์ ์ ์๋๋ก ์น ํ์ค์ผ๋ก ์ ์๋์๋ค.
์ฌ์ฉ์์ ๋ก๊ทธ์ธ ์ํ๋ฅผ ์๋ฒ์์ ์ฒ๋ฆฌํ๋ ๋ํ์ ์ธ ๋ ๊ฐ์ง ์ธ์ฆ๋ฐฉ์
1. ์ธ์ ๊ธฐ๋ฐ ์ธ์ฆ ์์คํ
2. ํ ํฐ ๊ธฐ๋ฐ ์ธ์ฆ ์์คํ
1. ์ธ์ ๊ธฐ๋ฐ ์ธ์ฆ ์์คํ
: ์๋ฒ๊ฐ ์ฌ์ฉ์๊ฐ ๋ก๊ทธ์ธ ์ค์์ ๊ธฐ์ตํ๊ณ ์๋ค.
์ธ์ ๊ธฐ๋ฐ ์ธ์ฆ ์์คํ ์์ ์ฌ์ฉ์๊ฐ ๋ก๊ทธ์ธ์,
์๋ฒ๋ ์ธ์ ์ ์ฅ์์์ ์ฌ์ฉ์์ ์ ๋ณด๋ฅผ ์กฐํํ๊ณ ์ธ์ id๋ฅผ ๋ฐ๊ธํ๋ค.
๋ฐ๊ธ๋ id๋ ์ฃผ๋ก ๋ธ๋ผ์ฐ์ ์ ์ฟ ํค์ ์ ์ฅํ๋ค.
๊ทธ ๋ค์์ ์ฌ์ฉ์๊ฐ ๋ค๋ฅธ ์์ฒญ์ ๋ณด๋ผ ๋๋ง๋ค ์๋ฒ๋ ์ธ์ ์ ์ฅ์์์ ์ธ์ ์ ์กฐํํ ํ
๋ก๊ทธ์ธ ์ฌ๋ถ๋ฅผ ๊ฒฐ์ ํ์ฌ ์์ ์ ์ฒ๋ฆฌํ๊ณ ์๋ตํ๋ค.
์ธ์ ์ ์ฅ์๋ ์ฃผ๋ก ๋ฉ๋ชจ๋ฆฌ, ๋์คํฌ, ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ฑ์ ์ฌ์ฉํ๋ค.
๋จ์ : ์๋ฒ ํ์ฅ์ด ์ด๋ ต๋ค.
๋ง์ฝ ์๋ฒ์ ์ธ์คํด์ค๊ฐ ์ฌ๋ฌ๊ฐ๊ฐ ๋๋ค๋ฉด, ๋ชจ๋ ์๋ฒ๋ผ๋ฆฌ ๊ฐ์ ์ธ์ ์ ๊ณต์ ํด์ผํ๋ฏ๋ก, ์ธ์ ์ ์ฉ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ๋ง๋ค์ด์ผ ํ ๋ฟ ์๋๋ผ ์ ๊ฒฝ ์จ์ผ ํ ๊ฒ๋ ๋ง๋ค.
2. ํ ํฐ ๊ธฐ๋ฐ ์ธ์ฆ ์์คํ
ํ ํฐ : ๋ก๊ทธ์ธ ์ดํ ์๋ฒ๊ฐ ๋ง๋ค์ด ์ฃผ๋ ๋ฌธ์์ด๋ก,
ํด๋น ๋ฌธ์์ด ์์๋ ์ฌ์ฉ์์ ๋ก๊ทธ์ธ ์ ๋ณด์ ํด๋น ์ ๋ณด๊ฐ ์๋ฒ์์ ๋ฐ๊ธ๋์์์ ์ฆ๋ช ํ๋ ์๋ช ์ด ๋ค์ด ์์ต๋๋ค.
์๋ช ๋ฐ์ดํฐ๋ ํด์ฑ ์๊ณ ๋ฆฌ์ฆ์ ํตํด ๋ง๋ค์ด์ง๋๋ค, (์ฃผ๋ก HMAC SHA256, RSA SHA256 ์๊ณ ๋ฆฌ์ฆ์ด ์ฌ์ฉ๋๋ค.)
ํ ํฐ์ ์๋ช ์ด ์๊ธฐ ๋๋ฌธ์ ๋ฌด๊ฒฐ์ฑ์ด ๋ณด์ฅ๋๋ค.
๋ฌด๊ฒฐ์ฑ : ์ ๋ณด๊ฐ ๋ณ๊ฒฝ๋๊ฑฐ๋ ์์กฐ๋์ง ์์์์ ์๋งํ๋ ์ฑ์ง
์ฌ์ฉ์๊ฐ ๋ก๊ทธ์ธ์ ํ๋ฉด,
์๋ฒ์์ ์ฌ์ฉ์์๊ฒ ํด๋น ์ฌ์ฉ์์ ์ ๋ณด๋ฅผ ์ง๋๊ณ ์๋ ํ ํฐ์ ๋ฐ๊ธํด์ฃผ๊ณ ,
์ถํ ์ฌ์ฉ์๊ฐ ๋ค๋ฅธ API๋ฅผ ์์ฒญํ ๋, ๋ฐ๊ธ๋ฐ์ ํ ํฐ๊ณผ ํจ๊ป ์์ฒญํ๋ค.
๊ทธ๋ฌ๋ฉด ์๋ฒ๋ ํด๋น ํ ํฐ์ด ์ ํจํ์ง ๊ฒ์ฌํ๊ณ , ๊ฒฐ๊ณผ์ ๋ฐ๋ผ ์์ ์ ์ฒ๋ฆฌํ๊ณ ์๋ตํ๋ค.
์ฅ์
- ์๋ฒ์์ ์ฌ์ฉ์ ๋ก๊ทธ์ธ ์ ๋ณด๋ฅผ ๊ธฐ์ตํ๊ธฐ ์ํด ์ฌ์ฉ๋๋ ๋ฆฌ์์ค๊ฐ ์ ๋ค.
- ์ฌ์ฉ์ ์ชฝ์์ ๋ก๊ทธ์ธ ์ํ๋ฅผ ์ง๋ ํ ํฐ์ ๊ฐ์ง๊ณ ์์ผ๋ฏ๋ก, ์๋ฒ๋ผ๋ฆฌ ์ฌ์ฉ์์ ๋ก๊ทธ์ธ ์ํ๋ฅผ ๊ณต์ ํ ํ์๊ฐ ์์ด, ์๋ฒ์ ํ์ฅ์ฑ์ด ๋งค์ฐ ๋๋ค.
- ์ธ์ฆ ์์คํ ์ ๊ตฌํํ๊ธฐ ๊ฐํธํ๊ณ ์ฌ์ฉ์๋ค์ ์ธ์ฆ ์ํ๋ฅผ ๊ด๋ฆฌํ๊ธฐ๋ ํจ์ฌ ์ฝ๋ค.
User ์คํค๋ง/๋ชจ๋ธ ๋ง๋ค๊ธฐ
๋น๋ฐ๋ฒํธ๋ฅผ DB์ ์ ์ฅํ ๋, ๋จ๋ฐฉํฅ ํด์ฑ ํจ์๋ฅผ ์ง์ํด ์ฃผ๋ 'bcrypt' ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํด ๋น๋ฐ๋ฒํธ๋ฅผ ์์ ํ๊ฒ ์ ์ฅํ๊ธฐ
yarn add bcrypt
๋ชจ๋ธ ๋ฉ์๋ ๋ง๋ค๊ธฐ
๋ชจ๋ธ ๋ฉ์๋ : ๋ชจ๋ธ์์ ์ฌ์ฉํ ์ ์๋ ํจ์๋ก ๋ ๊ฐ์ง ์ข ๋ฅ๊ฐ ์๋ค.
1. instance method : ๋ชจ๋ธ์ ํตํด ๋ง๋ ๋ฌธ์ ์ธ์คํด์ค์์ ์ฌ์ฉ / 2. static method : ๋ชจ๋ธ์์ ๋ฐ๋ก ์ฌ์ฉ
import mongoose, { Schema } from 'mongoose';
import bcrypt from 'bcrypt';
const UserSchema = new Schema({
username: String,
hashedPassword: String,
});
// ๋น๋ฐ๋ฒํธ๋ฅผ ํ๋ผ๋ฏธํฐ๋ก ๋ฐ์ ๊ณ์ ์ hashedPassword ๊ฐ์ ์ค์
UserSchema.methods.setPassword = async function (password) {
const hash = await bcrypt.hash(password, 10);
this.hashedPassword = hash;
};
// ํ๋ผ๋ฏธํฐ๋ก ๋ฐ์ ๋น๋ฐ๋ฒํธ๊ฐ ํด๋น ๊ณ์ ์ ๋น๋ฐ๋ฒํธ์ ์ผ์นํ๋์ง ๊ฒ์ฆ
UserSchema.methods.checkPassword = async function (password) {
const result = await bcrypt.compare(password, this.hashedPassword);
return result;
};
// username์ผ๋ก ๋ฐ์ดํฐ ์ฐพ๊ธฐ, static ํจ์์์์ this๋ ๋ชจ๋ธ(User)๋ฅผ ๊ฐ๋ฆฌํด.
UserSchema.statics.findByUsername = function (username) {
return this.findOne({ username });
};
const User = mongoose.model('User', UserSchema);
export default User;
setPassword(password), checkPassword(password)๋ instance method
โ ์ด๋ ํ์ดํ ํจ์๊ฐ ์๋, function ํค์๋๋ฅผ ๊ผญ ์ฌ์ฉ
→ ํจ์ ๋ด๋ถ์์ this์ ์ ๊ทผํด์ผ ํ๊ธฐ ๋๋ฌธ, ์ฌ๊ธฐ์ this๋ ๋ฌธ์ ์ธ์คํด์ค๋ฅผ ๊ฐ๋ฆฌํจ๋ค.
findByUsername(username) ๋ static method
์ฌ๊ธฐ์ this๋ ๋ชจ๋ธ์ ๊ฐ๋ฆฌํต๋๋ค. (User๋ฅผ ๊ฐ๋ฆฌํด)
2. ํ์ ์ธ์ฆ API ๋ง๋ค๊ธฐ
2-1. ๋จผ์ , ์๋ก์ด ๋ผ์ฐํธ๋ฅผ ์ ์ํ๊ณ ,
// api/auth/auth.ctrl.js
export const register = async (ctx) => {};
export const login = async (ctx) => {};
export const check = async (ctx) => {};
export const logout = async (ctx) => {};
2-2. auth ๋ผ์ฐํฐ๋ฅผ ์์ฑํ๊ธฐ
// api/auth/index.js
import Router from 'koa-router';
import * as authCtrl from './auth.ctrl';
const auth = new Router();
auth.post('/register', authCtrl.register);
auth.post('/login', authCtrl.login);
auth.post('/check', authCtrl.check);
auth.post('/logout', authCtrl.logout);
export default auth;
2-3. ๊ทธ ๋ค์ auth ๋ผ์ฐํฐ๋ฅผ api ๋ผ์ฐํฐ์ ์ ์ฉํ๊ธฐ
// api/index.js
import Router from 'koa-router';
import auth from './auth';
import posts from './posts';
const api = new Router();
api.use('/posts', posts.routes()); // posts ๋ผ์ฐํธ ์ ์ฉ
api.use('/auth', auth.routes());
export default api;
3. ํ์ ๊ฐ์ ๊ตฌํํ๊ธฐ
// api/auth/auth.ctrl.js
import Joi from '../../../node_modules/joi/lib/index';
import User from '../../models/user';
/*
POST /api/auth/register
{
username:'velopert',
password : 'mypass123'
}
*/
export const register = async (ctx) => {
// Request Body ๊ฒ์ฆํ๊ธฐ
const schema = Joi.object().keys({
username: Joi.string().alphanum().min(3).max(20).required(),
password: Joi.string().required(),
});
const result = schema.validate(ctx.request.body);
if (result.error) {
ctx.status = 400;
ctx.body = result.error;
return;
}
const { username, password } = ctx.request.body;
try {
// username์ด ์ค๋ณต์ธ์ง ํ์ธ
const exists = await User.findByUsername(username);
if (exists) {
ctx.status = 409; // Conflict
return;
}
const user = new User({
username,
});
await user.setPassword(password); // ๋น๋ฐ๋ฒํธ ์ค์
await user.save(); // DB์ ์ ์ฅ
// ์๋ตํ ๋ฐ์ดํฐ์์ hashedPassword ํ๋ ์ ๊ฑฐ
ctx.body = user.serialize();
} catch (error) {
ctx.throw(500, error);
}
};
export const login = async (ctx) => {};
export const check = async (ctx) => {};
export const logout = async (ctx) => {};
// models/user.js
// + ์ถ๊ฐ
UserSchema.methods.serialize = function () {
const data = this.toJSON();
delete data.hashedPassword;
return data;
};
4. ๋ก๊ทธ์ธ ๊ตฌํํ๊ธฐ
// api/auth/auth.ctrl.js
import Joi from '../../../node_modules/joi/lib/index';
import User from '../../models/user';
/*
POST /api/auth/register
{
username:'velopert',
password : 'mypass123'
}
*/
export const register = async (ctx) => { ... };
export const login = async (ctx) => {
const { username, password } = ctx.request.body;
if (!username || !password) {
ctx.status = 401; // Unauthorized
return;
}
try {
// ์ฌ์ฉ์ ๋ฐ์ดํฐ ์ฐพ๊ธฐ
const user = await User.findByUsername(username);
if (!user) {
ctx.status = 400;
return;
}
const valid = await user.checkPassword(password);
if (!valid) {
ctx.status = 400;
return;
}
ctx.body = user.serialize();
} catch (error) {
ctx.throw(500, error);
}
};
export const check = async (ctx) => {};
export const logout = async (ctx) => {};
5. ํ ํฐ ๋ฐ๊ธ ๋ฐ ๊ฒ์ฆํ๊ธฐ
ํด๋ผ์ด์ธํธ์์ ์ฌ์ฉ์ ๋ก๊ทธ์ธ ์ ๋ณด๋ฅผ ์ง๋๊ณ ์์ ์ ์๋๋ก ์๋ฒ์์ ํ ํฐ์ ๋ฐ๊ธํ๊ธฐ
jsonwebtoken : JWT ํ ํฐ์ ๋ง๋ค๊ธฐ ์ํด์ ํด๋น ๋ชจ๋์ ์ค์นํด์ผํ๋ค.
yarn add jsonwebtoken
5-1. ๋น๋ฐํค ์ค์ ํ๊ธฐ
.env ํ์ผ์ JWTํ ํฐ์ ๋ง๋ค๋ ์ฌ์ฉํ ๋น๋ฐํค๋ฅผ ๋ง๋ ๋ค. (๋ฌธ์์ด ์๋ฌด๊ฑฐ๋ ๊ฐ๋ฅ)
์ด ๋น๋ฐํค๋ ๋์ค์ JWT ํ ํฐ์ ์๋ช ์ ๋ง๋๋ ๊ณผ์ ์์ ์ฌ์ฉ๋๋ค.
๋น๋ฐํค๊ฐ ๊ณต๊ฐ๋๋ ์๊ฐ, ๋๊ตฌ๋ ์ง ๋ง์๋๋ก JWT ํ ํฐ์ ๋ฐ๊ธํ ์ ์์ผ๋ฏ๋ก ์ ๋ ์ธ๋ถ์ ๊ณต๊ฐ๋์ ์๋ฉ๋๋ค.
// .env
JWT_SECRET=//////////////////////////////////////
5-2. ํ ํฐ ๋ฐ๊ธํ๊ธฐ
// models/user.js
// + ์ถ๊ฐ
import jwt from 'jsonwebtoken';
// ํ ํฐ ๋ฐ๊ธํ๊ธฐ
UserSchema.methods.generateToken = function () {
// jwt.sing(ํ ํฐ ์์ ์ง์ด ๋ฃ๊ณ ์ถ์ ๋ฐ์ดํฐ, JWT ์ํธ)
const token = jwt.sign(
{
_id: this.id,
username: this.username,
},
process.env.JWT_SECRET,
{
expiresIn: '7d', // 7์ผ๋์ ์ ํจ
},
);
return token;
};
์ด์ ํ์๊ฐ์ ๊ณผ ๋ก๊ทธ์ธ์ ์ฑ๊ณตํ์ ๋ ํ ํฐ์ ์ฌ์ฉ์์๊ฒ ์ ๋ฌํด์ฃผ๊ฒ ๋ค.
์ฌ์ฉ์๊ฐ ๋ธ๋ผ์ฐ์ ์์ ํ ํฐ์ ์ฌ์ฉํ ๋๋ ์ฃผ๋ก ๋ ๊ฐ์ง ๋ฐฉ๋ฒ์ ์ฌ์ฉํ๋ค.
1. ๋ธ๋ผ์ฐ์ ์ localStorage ํน์ sessionStorage์ ๋ด์์ ์ฌ์ฉํ๊ธฐ
- ๋งค์ฐ ํธ๋ฆฌํ๊ณ ๊ตฌํํ๊ธฐ๋ ์ฝ๋ค.
ํ์ง๋ง *XSS(Cross Site Scripting)๊ณต๊ฒฉ์ ๋ฐ์ ์ ์๋ค.
* XSS : ๋๊ตฐ๊ฐ๊ฐ ํ์ด์ง์ ์ ์ฑ ์คํฌ๋ฆฝํธ๋ฅผ ์ฝ์ ํ๋ค๋ฉด ์ฝ๊ฒ ํ ํฐ์ ํ์ทจํ ์ ์์.
2. ๋ธ๋ผ์ฐ์ ์ cookie์ ๋ด์์ ์ฌ์ฉํ๊ธฐ
์ฟ ํค๋ XSS ๊ณต๊ฒฉ์ ๋ฐ์ ์ ์์ง๋ง, httpOnly๋ผ๋ ์์ฑ์ ํ์ฑํํ๋ฉด ์๋ฐ์คํฌ๋ฆฝํธ๋ฅผ ํตํด ์ฟ ํค๋ฅผ ์กฐํํ ์ ์์ด
์ ์ฑ ์คํฌ๋ฆฝํธ๋ก๋ถํฐ ์์ ํฉ๋๋ค.
๊ทธ ๋์ CSRF(Cross Stie Request Forgery) ๊ณต๊ฒฉ์ ์ทจ์ฝํด์ง ์ ์์ต๋๋ค.
ํ์ง๋ง, CSRF ํ ํฐ ์ฌ์ฉ ๋ฐ Referer ๊ฒ์ฆ ๋ฑ์ ๋ฐฉ์์ผ๋ก ์ด ๊ณต๊ฒฉ์ ๋ง์ ์ ์์ต๋๋ค.
* CSRF : ํ ํฐ์ ์ฟ ํค์ ๋ด์ผ๋ฉด ์ฌ์ฉ์๊ฐ ์๋ฒ๋ก ์์ฒญ์ ํ ๋๋ง๋ค ๋ฌด์กฐ๊ฑด ํ ํฐ์ด ํจ๊ป ์ ๋ฌ๋๋ ์ ์ ์ด์ฉํด ์ฌ์ฉ์๊ฐ ๋ชจ๋ฅด๊ฒ ์ํ์ง ์์ API ์์ฒญ์ ํ๊ฒ ๋ง๋ ๋ค. (์ฌ์ฉ์๋ ๋ชจ๋ฅด๋ ์ํฉ์์ ๊ธ์ ์์ฑ, ์ญ์ , ํํดํ๊ฒ ๋ง๋ฌ)
// api/auth/auth.ctrl.js
export const register = async (ctx) => {
...
// ํ์๊ฐ์
์ ์ฑ๊ณตํ์๋ ํ ํฐ์ ์ฌ์ฉ์์๊ฒ ์ ๋ฌํ๊ธฐ
const token = user.generateToken();
ctx.cookies.set('access_token', token, {
maxAge: 1000 * 60 * 60 * 24 * 7, // 7d
httpOnly: true,
});
} catch (error) {
ctx.throw(500, error);
}
};
export const login = async (ctx) => {
...
// ๋ก๊ทธ์ธ์ ์ฑ๊ณตํ์๋ ํ ํฐ์ ์ฌ์ฉ์์๊ฒ ์ ๋ฌํ๊ธฐ
const token = user.generateToken();
ctx.cookies.set('access_token', token, {
maxAge: 1000 * 60 * 60 * 24 * 7, // 7d
httpOnly: true,
});
} catch (error) {
ctx.throw(500, error);
}
};
5-3. ํ ํฐ ๊ฒ์ฆํ๊ธฐ
์ฌ์ฉ์์ ํ ํฐ์ ํ์ธํ ํ ๊ฒ์ฆํ๋ ์์ ์ ๋ฏธ๋ค์จ์ด๋ฅผ ํตํด ์ฒ๋ฆฌํ๋ค.
// lib/jwtMiddleware.js
// ์ฌ์ฉ์์ ํ ํฐ์ ํ์ธํ ํ ๊ฒ์ฆํ๋ ์์
import jwt from 'jsonwebtoken';
import User from '../models/user';
const jwtMiddleware = async (ctx, next) => {
const token = ctx.cookies.get('access_token');
if (!token) return next();
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
ctx.state.user = {
_id: decoded._id,
username: decoded.username,
};
// ํ ํฐ ๋จ์ ์ ํจ๊ธฐ๊ฐ์ด 3.5์ผ ๋ฏธ๋ง์ด๋ฉด ์ฌ๋ฐ๊ธ
const now = Math.floor(Date.now() / 1000);
if (decoded.exp - now < 60 * 60 ** 24 * 3.5) {
const user = await User.findById(decoded._id);
const token = user.generateToken();
ctx.cookies.set('access_token', token, {
maxAge: 1000 * 60 * 60 * 24 * 7,
httpOnly: true,
});
}
//console.log(decoded);
return next();
} catch (error) {
// ํ ํฐ ๊ฒ์ฆ ์คํจ
return next();
}
};
export default jwtMiddleware;
๋ฏธ๋ค์จ์ด ์ ์ฉํ๊ธฐ
// main.js
...
const app = new Koa();
const router = new Router();
import api from './api';
import jwtMiddleware from './lib/jwtMiddleware';
// api ๋ผ์ฐํธ ์ ์ฉ
router.use('/api', api.routes()); // => /api/test
// ๋ผ์ฐํฐ ์ ์ฉ ์ ์ bodyparser ์ฌ์ฉํ๊ธฐ
app.use(bodyParser());
// ๋ผ์ฐํฐ ์ ์ฉ ์ ์ jwMiddleware๋ฅผ ์ ์ฉํ๊ธฐ
app.use(jwtMiddleware);
// app ์ธ์คํด์ค์ ๋ผ์ฐํฐ ์ ์ฉ
app.use(router.routes()).use(router.allowedMethods());
check ํจ์(๋ก๊ทธ์ธ ์ํ ํ์ธ๊ธฐ๋ฅ) ๊ตฌํ
// api/auth/auth.ctrl.js
// ๋ก๊ทธ์ธ ์ํ ํ์ธ
export const check = async (ctx) => {
const { user } = ctx.state;
if (!user) {
// ๋ก๊ทธ์ธ ์ค ์๋
ctx.state = 401; // Unauthorized
return;
}
ctx.body = user;
};
5-4. ๋ก๊ทธ์์ ๊ธฐ๋ฅ ๊ตฌํํ๊ธฐ
์ฟ ํค๋ฅผ ์ง์ ์ฃผ๊ธฐ๋ง ํ๋ฉด ๋!
// api/auth/auth.ctrl.js
export const logout = async (ctx) => {
ctx.cookies.set('access_token');
ctx.status = 204; //No Content
};