😄 JWT
- JSON Web Token
- 사용자의 로그인 상태를 서버에서 처리하는 데 사용할 수 있는 두 가지 인증 방식
- 세션 기반
- 서버가 사용자가 로그인 중임을 기억하고 있다는 뜻
- 토큰 기반
- 토큰은 로그인 이후 서버가 만들어주는 문자열
- 해당 문자열 안에는 사용자의 로그인 정보와 서명이 들어가있다.
- 서명은 해싱 알고리즘으로 만든다.
- 토큰을 기반으로 하면 사용자 로그인 정보를 기억하기 위한 리소스가 적게 든다.
- 또 서버의 인스턴스가 여러 개로 늘어나도 상관이 없다.
- 서버끼리 사용자의 로그인 상태를 공유할 필요가 없기 때문이다.
😎 실제 코드
...
import jwtMiddleware from './lib/jwtMiddleware';
...
app.use(bodyParser());
app.use(jwtMiddleware);
...
import mongoose, { Schema } from 'mongoose';
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
const UserSchema = new Schema({
username: String,
hashedPassword: String,
});
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;
};
UserSchema.methods.serialize = function () {
const data = this.toJSON();
delete data.hashedPassword;
return data;
};
UserSchema.methods.generateToken = function () {
const token = jwt.sign(
{
_id: this.id,
username: this.username,
},
process.env.JWT_SECRET,
{
expiresIn: '7d',
},
);
return token;
};
UserSchema.statics.findByUsername = function (username) {
return this.findOne({ username });
};
const User = mongoose.model('User', UserSchema);
export default User;
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,
};
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,
});
}
return next();
} catch (e) {
return next();
}
};
export default jwtMiddleware;
import Router from 'koa-router';
import posts from './posts';
import auth from './auth';
const api = new Router();
api.use('/posts', posts.routes());
api.use('/auth', auth.routes());
export default api;
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.get('/check', authCtrl.check);
auth.post('/logout', authCtrl.logout);
export default auth;
import Joi from '@hapi/joi';
import User from '../../models/user';
export const register = async (ctx) => {
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 {
const exists = await User.findByUsername(username);
if (exists) {
ctx.status = 409;
return;
}
const user = new User({
username,
});
await user.setPassword(password);
await user.save();
ctx.body = user.serialize();
const token = user.generateToken();
ctx.cookies.set('access_token', token, {
maxAge: 1000 * 60 * 60 * 24 * 7,
httpOnly: true,
});
} catch (e) {
ctx.throw(500, e);
}
};
export const login = async (ctx) => {
const { username, password } = ctx.request.body;
if (!username || !password) {
ctx.status = 401;
return;
}
try {
const user = await User.findByUsername(username);
if (!user) {
ctx.status = 401;
return;
}
const valid = await user.checkPassword(password);
if (!valid) {
ctx.status = 401;
return;
}
ctx.body = user.serialize();
const token = user.generateToken();
ctx.cookies.set('access_token', token, {
maxAge: 1000 * 60 * 60 * 24 * 7,
httpOnly: true,
});
} catch (e) {
ctx.throw(500, e);
}
};
export const check = async (ctx) => {
const { user } = ctx.state;
if (!user) {
ctx.status = 401;
return;
}
ctx.body = user;
};
export const logout = async (ctx) => {
ctx.cookies.set('access_token');
ctx.status = 204;
};
⭐️ 팁
- openssl rand -hex 64: 랜덤 키를 만들어준다.