1. http-status-codes 모듈 활용하기
이제까지는 상태코드를 하드코딩해서 보냈는데, 이렇게 보내는 것보다는 무슨 코드인지 명확하게 보여줄 수 있는 코드를 사용하는 것이 좋기 때문에 http-status-codes 를 활용해보려고 한다.
res.status(400).json('...');
http-status-codes 모듈 설치
npm i http-status-codes
// 모듈 불러오기
const {statusCodes} = require('http-status-codes');
모듈을 불러온 후, StatusCodes. 을 치면 상태코드들이 뜨는데, 원하는 상태코드 값을 골라서 사용하면 된다.
2. Node.js 패키지(파일) 구조
지금까지는 최상위 폴더 밑에 routes 폴더를 두고 그 안에 books.js, carts.js 등의 파일을 두고 그 파일 안에 모든 코드가 다 들어있었다.
라우터는 사실 경로 역할만 담당하는 것이기 때문에 라우터 안에서 로직까지 다 수행해버리면 여러 단점이 생긴다. 그렇기 때문에 코드를 분리해서 간결하게 만들어 가독성과 유지보수성을 높여주어야 한다.
라우터가 로직까지 수행하면 생기는 단점
1) 프로젝트 규모가 커질수록 코드가 엄청 복잡해짐
2) 가독성이 좋지 않음
3) 트러블 슈팅 X -> 유지보수하기 어려움
2.1. Controller
- 프로젝트에서 매니저 역할을 하는 파일 -> 직접 일을 하지는 않지만, 누군가에게 일을 어떻게 시켜야할지 알고 있음
- 누군가에게 시킨 일이 끝나고 결과를 받아서, 결과를 사용자에게 돌려줌
Controller 폴더를 생성하고 userController.js 파일을 만들었다. 기존 라우터 파일 안에 작성했던 로직 코드를 분리하고 userController.js 파일에 넣었다.
// users.js (라우터파일)
const express = require('express');
const conn = require('../mariadb');
const router = express.Router();
// const {body, param, validationResult} = require('express-validator');
const {
join,
login,
passwordResetRequest,
passwordReset
} = require('../controller/userController');
router.use(express.json());
// 회원가입
router.post('/join', join);
// 로그인
router.post('/login', login);
// 비밀번호 초기화 요청
router.post('/reset', passwordResetRequest);
// 비밀번호 초기화
router.put('/reset', passwordReset);
module.exports = router;
- userController.js
/ 회원가입
const join = (req, res) => {
const {email, password} = req.body;
let sql = `INSERT INTO users (email, password) VALUES (?, ?)`;
let values = [email, password];
conn.query(sql, values,
(err, results) => {
if (err) {
console.log(err);
return res.status(StatusCodes.BAD_REQUEST).end();
}
res.status(StatusCodes.CREATED).json({ message: `환영합니다.` });
}
)
}
FORBIDDEN(403) -> 클라이언트에게 접근권리가 없음 -> 서버가 그 사람이 누군지 알고 있음, 그래서 접근 권리가 없다는 것도 앎
UNAUTHORIZED(401) -> 서버가 클라이언트의 접근을 거부 -> 서버가 그 사람이 누군지 모름
// 로그인
const login = (req, res) => {
const { email, password } = req.body;
let sql = `SELECT * FROM users WHERE email = ?`;
conn.query(sql, email,
(err, results) => {
if (err) {
console.log(err);
return res.status(StatusCodes.BAD_REQUEST).end();
}
let loginUser = results[0];
if (login && loginUser.password === password) {
// 토큰 발행
const token = jwt.sign({
email: loginUser.email,
password: loginUser.password,
}, process.env.PRIVATE_KEY, {
expiresIn: '5m',
issuer: 'bomya'
});
res.cookie("token", token, {
httpOnly: true
}); // 토큰 쿠키에 담음
console.log(token);
res.status(StatusCodes.OK).json(results);
} else {
res.status(StatusCodes.UNAUTHORIZED).end();
}
}
)
};
비밀번호 변경 요청을 하면
리퀘스트로 받은 이메일이 존재하는 유저의 이메일인지 확인하면, 받은 이메일을 리스폰스로 돌려준다. (다음 페이지인 비밀번호 변경 페이지에서 사용하기 위해)
// 비밀번호 변경 요청
const passwordResetRequest = (req, res) => {
const {email} = req.body;
let sql = `SELECT * FROM users WHERE email = ?`;
conn.query(sql, email,
(err, results) => {
if (err) {
console.log(err);
return res.status(StatusCodes.BAD_REQUEST);
}
// 이메일로 유저가 있는지 찾음
const user = results[0];
if (user) {
return res.status(StatusCodes.OK).json({
email: email
});
} else {
return res.status(StatusCodes.UNAUTHORIZED).end();
}
}
)
};
const passwordReset = (req, res) => {
// 이전 페이지에서 입력했던 이메일
const {email, password} = req.body;
let sql = `UPDATE users SET password = ? WHERE email = ?`;
let values = [password, email]
conn.query(sql, values,
(err, results) => {
if (err) {
console.log(err);
return res.status(StatusCodes.BAD_REQUEST).end();
}
if (results.affectedRows)
return res.status(StatusCodes.OK).json(results);
else
return res.status(StatusCodes.BAD_REQUEST).end();
}
)
};
3. 회원가입 시 비밀번호 암호화
데이터베이스에 비밀번호를 그대로 노출해서 저장하는 것은 보안상 위험하기 때문에 사용자가 입력한 비밀번호를 암호화해서 암호화된 비밀번호를 데이터베이스에 저장해야 한다.
암호화를 위해서 crypto 모듈이 필요한데, crypto 는 node.js 에 기본적으로 내장된 내장 모듈로 여러 해시 함수를 통한 암호화 기능을 제공한다.
// 회원가입 시 비밀번호를 암호화해서 암호화된 비밀번호와 salt 값을 같이 DB에 저장
const salt = crypto.randomBytes(10).toString('base64');
const hashPassword = crypto.pbkdf2Sync(password, salt, 10000, 10, 'sha512').toString('base64');
salt
- crypto.randomBytes(n): n 바이트 길이의 랜덤한 바이트 배열을 생성 -> salt 값으로 사용되는 추가적인 랜덤 문자열
- toString('base64'): 바이트 데이터를 base64 인코딩 문자열로 변환 (base64 는 일반 텍스트처럼 사용할 수 있게 바이트 데이터를 텍스트로 변환하는 방식)
hashPassword
- crypto.pbkdf2Sync(password, salt, 10000, 10, 'sha512')
- password: 해싱할 비밀번호
- salt: 해싱에 사용될 salt 값
- 10000: 해시 반복횟수
- 10: 생성할 해시 값의 길이
- sha512: 해싱에 사용될 알고리즘
※ 궁금해서 찾아본 - 왜 salt 인가?
salt 는 복호화를 방해하기 위해 단방향 암호화 시에 소금(salt)을 뿌려 해커가 복호화하는 것을 방해하는 방법이다.
입력으로 들어가는 비밀번호에 salt 라는 추가적인 문자열을 덧붙이고 해쉬함수에 넣으면 비밀번호는 같더라도 다른 해쉬 출력 값이 나온다. (해쉬함수는 단방향 암호화로 암호화만 되고 복호화 되지 않는다)
하지만, 모든 유저에게 동일한 salt 값을 사용하면 해커가 salt 값을 알아내는 순간 모든 유저의 비밀번호가 뚫릴 수 있으니 유저마다 다른 salt 값을 사용해야 한다.
3.1. 회원가입
사용자가 입력한 비밀번호에 salt 값을 더해서 암호화된 비밀번호를 만든다 == hashPassword
만들어진 hashPassword 와 salt 값을 데이터베이스에 INSERT 한다.
해쉬함수는 단방향으로 암호화로 암호화만 되고 복호화가 불가능하기 때문에 만들어진 salt 값을 데이터베이스에 함께 저장해둬야 한다.
-> 로그인할 때 저장해뒀던 salt 값을 꺼내어서 사용자가 입력한 비밀번호에 더해서 해싱하고 암호화되어서 저장되어 있는 비밀번호 값이랑 비교해서 로그인 여부를 결정
const join = (req, res) => {
const {email, password} = req.body;
// 회원가입 시 비밀번호를 암호화해서 암호화된 비밀번호와 salt 값을 같이 DB에 저장
const salt = crypto.randomBytes(10).toString('base64');
const hashPassword = crypto.pbkdf2Sync(password, salt, 10000, 10, 'sha512').toString('base64');
let sql = `INSERT INTO users (email, password, salt) VALUES (?, ?, ?)`;
let values = [email, hashPassword, salt];
conn.query(sql, values,
(err, results) => {
if (err) {
console.log(err);
return res.status(StatusCodes.BAD_REQUEST).end();
}
res.status(StatusCodes.CREATED).json({ message: `환영합니다.` });
}
)
}
3.2. 로그인
유저 정보를 찾았다면, 해당 데이터베이스에 들어있는 salt 값을 꺼내어 유저가 입력한 암호화 되지 않은 비밀번호에 더해 해싱한다.
해싱한 값과 데이터베이스에 들어있는 이미 암호화된 (해싱되어 있는) 값을 비교해서 로그인 여부를 결정한다.
const login = (req, res) => {
const { email, password } = req.body;
let sql = `SELECT * FROM users WHERE email = ?`;
conn.query(sql, email,
(err, results) => {
if (err) {
console.log(err);
return res.status(StatusCodes.BAD_REQUEST).end();
}
let loginUser = results[0];
// 로그인 시, 이메일/비밀번호가 날 것의 상태로 들어옴
// DB에서 salt 값을 꺼내서 날 것으로 들어온 비밀번호 암호화
const hashPassword = crypto.pbkdf2Sync(password, loginUser.salt, 10000, 10, 'sha512').toString('base64');
// DB에 암호화되어 있는 비밀번호와 비교
if (login && loginUser.password === hashPassword) {
const token = jwt.sign({
email: loginUser.email,
password: loginUser.password,
}, process.env.PRIVATE_KEY, {
expiresIn: '5m',
issuer: 'bomya'
});
res.cookie("token", token, {
httpOnly: true
});
console.log(token);
res.status(StatusCodes.OK).json(results);
} else {
res.status(StatusCodes.UNAUTHORIZED).end();
}
}
)
};
3.3. 비밀번호 초기화 (변경)
비밀번호를 바꿀 때는 기존의 salt 값을 그대로 사용하지 않고 salt 값도 새롭게 바꿔준다.
새롭게 salt 값을 생성하고, salt 값에 바꾼 비밀번호를 더해 해싱해준 값을 데이터베이스에 UPDATE 해주면 된다.
const passwordReset = (req, res) => {
// 이전 페이지에서 입력했던 이메일
const {email, password} = req.body;
let sql = `UPDATE users SET password = ?, salt = ? WHERE email = ?`;
// 암호화된 비밀번호와 salt 값을 같이 DB에 저장
const salt = crypto.randomBytes(10).toString('base64');
const hashPassword = crypto.pbkdf2Sync(password, salt, 10000, 10, 'sha512').toString('base64');
let values = [hashPassword, salt, email]
conn.query(sql, values,
(err, results) => {
if (err) {
console.log(err);
return res.status(StatusCodes.BAD_REQUEST).end();
}
if (results.affectedRows)
return res.status(StatusCodes.OK).json(results);
else
return res.status(StatusCodes.BAD_REQUEST).end();
}
)
};
'TIL with Programmers' 카테고리의 다른 글
[회고록] 풀 사이클 개발 데브코스 7주차 회고 (1) | 2024.10.05 |
---|---|
[TIL] 10/4 도서 데이터베이스, API 구현 (1) | 2024.10.04 |
[TIL] 10/1 API 설계 & 수정, ERD 테이블 (1) | 2024.10.01 |
[TIL] 9/30 ERD 테이블, API 설계 및 수정 (1) | 2024.09.30 |
[회고록] 풀 사이클 개발 데브코스 6주차 회고 (0) | 2024.09.30 |