다음은 책 리액트를 다루는 기술을 읽고 공부한 내용을 바탕으로 작성된 글입니다.
서버에 회원 인증 시스템을 구현하였다.
시스템을 구현하기 위해서 JWT라는 기술을 사용하였는데 이는 JSON Web Token의 약자로, 데이터가 JSON으로 이루어져 있는 토큰을 의미한다.
토큰? 그렇다면 토큰이 무엇일까 ?
사용자의 로그인 상태를 서버에서 처리하는 데 사용하는 방식에는 대표적으로 두 가지 방식이 있다.
하나는 세션 기반 인증 시스템, 하나는 토큰 기반 인증 시스템이다.
1. 세션 기반 인증 시스템
한 마디로, 서버가 사용자가 로그인 중임을 항상 기억하고 있다는 뜻이다.
사용자가 로그인을 하면, 서버는 세션 저장소에 사용자 정보를 조회하고, 세션 id를 발급한다. 사용자의 브라우저의 쿠키에 이 세션 id가 주로 저장되며, 이후 사용자가 서버 측에 요청을 보낼 때마다 이 세션 id를 함께 보낸다.
서버는 세션 id를 저장소로부터 확인하여 로그인 여부를 결정, 확인하고 작업을 처리 및 응답한다.
서버가 세션을 저장해야 하므로 서버 확장이 번거롭고 과부하의 위험성이 있다는 단점이 있다.
2. 토큰 기반 인증 시스템
토큰은 로그인시 서버가 만들어주는 문자열이다.
첫 로그인 시, 서버가 사용자에게 토큰을 발급한다. 이 토큰에는 사용자의 로그인 정보와 함께 해당 서버가 서버에서 발급됨을 증명하는 서명 데이터가 존재한다. 이 서명으로 인해 토큰은 정보가 변경되거나 위조되지 않았음을 의미하는 무결성이 보장된다. 추후에 사용자는 요청과 함께 토큰을 함께 전송하고, 서버는 해당 토큰의 유효성 검사를 통해 로그인 여부를 확인, 작업을 처리/응답한다.
서버가 사용자의 로그인 정보를 유지할 필요가 없으므로 서버의 확장성이 매우 높다는 장점이 있다.
이 블로그 만들기 프로젝트에서 선택한 방법 역시 토큰 기반 인증 시스템이다.
3. User 스키마 / 모델 만들기
사용자의 정보를 담을 User 스키마와 모델을 만든다.
사용자 스키마에는 사용자 계정명과 비밀번호가 있다.
비밀번호를 데이베이스에 저장할 때 아무런 가공도 하지 않고 그냥 텍스트로서 저장한다면, 보안상 매우 위험하다.
따라서 단방향 해싱 함수를 지원해주는 bcypt 라이브러리를 사용했다.
- 터미널
$> yarn add bcrypt
- src/models/user.js
import mongoose, { Schema } from 'mongoose';
import bcrypt from 'bcrypt';
const UserSchema = new Schema({
username: String,
hashedPassword: String,
});
const User = mongoose.model('User', UserSchema);
export default User;
4. 모델 메서드 만들기
모델 메서드는 모델이 사용할 수 있는 함수를 의미하며, 두 가지 종류가 있다.
4-1. 인스턴스 메서드
첫 번째는 인스턴스 메서드이다. 이는 모델을 통해 만든 인스턴스를 통해 사용할 수 있는 함수를 의미한다.
ex) const user = new User( { username: 'subbni' } );
user.setPassword('mypassword');
4-2. 스태틱 (static) 메서드
두 번째는 스태틱 메서드이다. java의 class method의 개념과 비슷하다고 보면 될 듯하다.
인스턴스가 존재하지 않아도 모델에서 바로 사용할 수 있는 함수를 의미한다.
ex) const user = User.findByUsername('subbni');
인스턴스 메서드와 스태틱 메서드 모두를 사용하여 함수를 작성하였다.
- src/models/user.js
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
};
UserSchema.statics.findByUsername = function (username) {
return this.findOne({ username });
};
const User = mongoose.model('User', UserSchema);
export default User;
여기서 주의할 점은, 위의 인스턴스 메서드를 작성할 때는 화살표 함수가 아닌 function 키워드를 통해 구현해야 한다는 것이다. 이는 함수 내부에서 this에 접근해야 하기 때문인데, 여기서 this는 문서 인스턴스를 가리킨다.
화살표 함수를 사용하면 this는 문서 인스턴스가 아닌 함수 호출 당시의 상위 스코프의 this를 가리키게 된다.
따라서 반드시 this를 사용하는 인스턴스 메서드에서는 화살표 함수가 아닌 function 키워드를 통해 구현해야 한다.
스태틱 함수에서의 this는 모델을 가리킨다. 위의 코드에서는 User를 가리킨다.
5. 회원 인증 API 만들기
회원 인증 API를 위해 새로운 라우트를 정의한다.
- src/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) => {
// 로그아웃
};
총 4개의 API의 틀을 먼저 잡아주었다.
- src/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.get('/check', authCtrl.check);
auth.post('/logout', authCtrl.logout);
export default auth;
다음, auth 라우터를 생성하였다.
- src/api/index.js
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());
// 라우터를 내보낸다.
//module.exports = api;
export default api;
auth 라우터를 api 라우터에 적용하였다.
5-1. 회원가입 구현
-src/api/auth/auth/ctrl.js
import Joi from '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 {
//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(); // 데이터베이스에 저장
// 응답할 데이터에서 hashedPassword 필드 제거
ctx.body = user.serialize();
} catch (e) {
ctx.throw(500, e);
}
};
export const login = async (ctx) => {
// 로그인
};
export const check = async (ctx) => {
// 로그인 상태 확인
};
export const logout = async (ctx) => {
// 로그아웃
};
유효성 검사를 먼저 한 뒤, 해당 계정명의 사용자가 이미 존재하는지를 확인한다.
-> 스태틱 메서드 findByUsername 사용
이후 해당 계정명을 가진 모델 인스턴스를 하나 만든 뒤, 비밀번호를 설정한다.
-> 인스턴스 메서드 setPassword 사용
이후 클라이언트에 응답할 데이터에는 hashedPassword 필드를 제거해야 마땅한데, 이 비슷한 작업을 자주 하게 될 것으로 serialize라는 인스턴스 함수를 만들어 사용했다.
함수 코드는 다음과 같다.
- src/models/user.js
UserSchema.methods.serialize = function () {
// 회원가입 성공 후 hashedPassword 필드가 응답되지 않도록 데이터를 JSON으로 변환 후 delete를 통해 해당 필드를 지워준다.
const data = this.toJSON();
delete data.hashedPassword;
return data;
};
데이터를 JSON으로 변환한 후 delete를 통해 해당 필드를 지운다.
- Postman
- MongoDB
테스팅 결과, 성공적으로 username과 해싱함수 처리 된 password가 저장됐음을 확인하였다.
- Postman
또한 같은 계정명으로 다시 회원가입 요청을 보냈을 경우, conflict 오류가 전달되는 것 또한 확인하였다.
'Front-End > React' 카테고리의 다른 글
[React] 블로그 만들기 8 - 토큰 발급 및 검증 (0) | 2022.03.15 |
---|---|
[React] 블로그 만들기 7 - 로그인 구현 (0) | 2022.03.15 |
[React] 블로그 만들기 5 - 페이지네이션 구현 (0) | 2022.03.14 |
[React] 블로그 만들기 4 - Request Body 검증 (0) | 2022.03.14 |
[React] 블로그 만들기 3 - 요청 검증 (id 검증) (0) | 2022.03.14 |