- 토큰 기반 인증 구현
- 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는 보안장치를 적용해도 개발자가 놓칠 수 있는 다양한 취약점으로 공격 가능
- localStorage, sessionStorage
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
};
'Web 개발 > Node' 카테고리의 다른 글
[Node] Access to XMLHttpRequest from origin has been blocked by CORS policy (0) | 2020.03.01 |
---|---|
[Node] 회원 인증 시스템 도입 및 포스트 검색 (0) | 2020.02.16 |
[Node] 세션 기반 인증 vs 토큰 기반 인증 (로그인 상태를 어디서) (0) | 2020.02.16 |
[Node] 페이지네이션 (Pagination) 구현 & Body 내용 제한 소스코드 예제 (0) | 2020.02.16 |
[Node] MongoDB ObjectId 검증, Request Body 검증 (joi, mongoose) (0) | 2020.02.16 |