토큰 기반 인증 구현
hash를 만드는 함수와 has 검증을 하는 함수
$ yarn add bcrypt
모델 메소드 만들기
모델에서 사용할 수 있는 함수
인스턴스 메서드 모델을 통해 만든 문서 인스턴스에서 사용할 수 있는 함수
static 메소드로 모델에서 바로 사용할 수 있는 함수
instance method
import mongoose, { Schema } from 'mongoose';
import bcrypt from 'bcrypt';
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; // true or false
};
const User = mongoose.model('User', UserSchema);
export default User;
static method
UserSchema.statics.findByUsername = function(username) {
return this.findOne({ username });
};
회원 인증 API 만들기
src/api/auth/auth.ctrl.js
import Joi from 'joi';
import User from '../../models/user';
/*
POST /api/auth/register
{
username: 'zzz',
password: 'zzz'
}
*/
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 = Joi.validate(ctx.request.body, schema);
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; // conflict
return;
}
const user = new User({
username,
});
await user.setPassword(password);
await user.save();
ctx.body = user.serialize(); // delete hashedPassword
} catch (e) {
ctx.throw(500, e);
}
};
/*
POST /api/auth/login
{
username: 'zzz'
password: 'zzz'
}
*/
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 = 401;
return;
}
const valid = await user.checkPassword(password);
if (!valid) {
ctx.status = 401;
return;
}
ctx.body = user.serialize();
} catch (e) {
ctx.throw(500, e);
}
};
export const check = async ctx => {};
export const logout = async ctx => {};
토큰 발급 및 검증
$ yarn add jsonwebtoken
$ openssl rand -hex 64
root의 .env
에 JWT_SECRET
값으로 설정
# .env
JWT_SECRET=dflkjdlkfdjflkdjflkjd
위 키는 JWT
토큰의 서명을 만드는 과정에 사용되기 때문에 외부에 절대 공개되면 안됨
user
모델 파일에서 generateToken
이라는 인스턴스 메서드 생성
UserSchema.methods.generateToken = function() {
const token = jwt.sign(
// 토큰안에 넣고 싶은 데이터
{
_id: this.id,
username: this.username,
},
// JWT 암호
process.env.JWT_SECRET,
{
expiresIn: '7d', // 7일동안 토큰 유효
},
);
return token;
};
회원가입과 로그인에 성공했을때 토큰을 사용자에게 전달
사용자가 브라우저에서 토큰을 사용
localStorage, sessionStorage
매우 편리하고 구현이 쉽지만, 쉽게 해킹이 가능 (XSS(Cross Site Scripting)
브라우저 쿠키에 담아서 사용
httpOnly 속성을 활성화하면 자바스크립트등을 통해 쿠키를 조회할 수 없음
CSRF (Cross Site Request Forgery) 공격에 취약
토큰을 쿠키에 담으면 사용자가 서버로 요청할때 무조건 토큰이 함께 전달
내가 모르는 상황에서 글이 작성, 삭제, 탈퇴가 가능...
CSRF 토큰 사용 및 Referer 검증 등의 방식으로 제대로 막을 수 있는 반면
XSS는 보안장치를 적용해도 개발자가 놓칠 수 있는 다양한 취약점으로 공격 가능
const token = user.generateToken();
ctx.cookies.set('access_token', token, {
maxAge: 1000 * 60 * 60 * 24 * 7, // 7days
httpOnly: true,
});
login, register에 다음과 같이 추가하면 access_token이 Header에서 확인
토큰 검증
사용자의 토큰을 확인한 후 검증 작업
src/lib
아래 다음과 같은 파일을 생성
import jwt from 'jsonwebtoken';
const jwtMiddleware = (ctx, next) => {
const token = ctx.cookie.get('access_token');
if (!token) return next(); // token is not exists
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
console.log(decoded);
return next();
} catch (e) {
// fail token validation
return next();
}
};
export default jwtMiddleware;
토큰 재발급받기
토큰의 만료 기간을 7일로 했는데 만료되면 재발급
generateToken
에서 토큰 유효기간을 3일로 변경하고 아래 함수가 실행되는지 확인
// 토큰의 남은 유효 기간이 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, // 7days
httpOnly: true,
});
}
로그아웃
로그아웃은 access_token
을 지워주면 된다.
export const logout = async ctx => {
ctx.cookies.set('access_token');
ctx.status = 240; // No Content
};