http-status-codes 모듈
상태 코드를 코드에 숫자로 적어두면 상태 코드를 모르는 사람들은 그 숫자가 무슨 의미인지 파악하기 힘들다. 상태 코드를 모르는 사람들도 알아볼 수 있도록 모듈을 사용해 보자.
npm i http-status-codes
const { StatusCodes } = require('http-status-codes');
const join = (req, res) => {
// ...
res.status(StatusCodes.CREATED).json(results);
};
상태 코드 | http-status-codes |
200 | OK |
201 | CREATED |
401 | UNAUTHORIZED |
403 | FORBIDDEN |
404 | NOT_FOUND |
컨트롤러
컨트롤러는 HTTP 요청을 처리하는 파일이다.
라우터는 라우팅(길 찾기) 역할만 수행해야 하지만 현재까지 구현한 코드에서는 라우터가 로직까지 수행하고 있다.
라우터가 로직까지 수행할 때의 단점은 다음과 같으며, 결과적으로 유지 보수가 힘들어진다.
- 프로젝트 규모가 커질수록 코드가 복잡해진다.
- 가독성이 떨어진다.
- 트러블슈팅이 힘들어진다.
따라서 로직을 수행하는 콜백 함수를 별도의 파일로 분리한다. 아래 코드는 컨트롤러를 사용한 예시이다.
users.js
const join = require('../controller/UserController');
// ...
router.post('/join', join); // 회원가입
UserController.js
const join = (req, res) => {
const { email, password } = req.body;
const salt = crypto.randomBytes(10).toString('base64');
const hashPassword = crypto.pbkdf2Sync(password, salt, 10000, 10, 'sha512').toString('base64');
const sql = `INSERT INTO users(email, password, salt) VALUES(?, ?, ?)`;
const values = [email, hashPassword, salt]
conn.query(sql, values,
(err, results) => {
if (err) {
console.log(err);
return res.status(StatusCodes.BAD_REQUEST).end(); // bad request
}
res.status(StatusCodes.CREATED).json(results);
});
};
// ...
module.exports = join;
비밀번호 암호화
raw한 비밀번호를 데이터베이스에 그대로 저장하면 안 된다. 누군가의 공격으로 데이터베이스가 유출될 경우 사용자가 큰 피해를 입을 수 있기 때문이다. 그래서 비밀번호는 암호화해서 저장해야 한다.
node.js에는 암호화를 할 수 있는 내장 모듈인 'crypto'가 있다.
const crypto = require('crypto');
const password = "1111";
const salt = crypto.randomBytes(64).toString('base64');
const hashPassword = crypto.pbkdf2Sync(password, salt, 10000, 64, 'sha512').toString('base64');
console.log(hashPassword);
메서드 | 설명 |
randomBytes(길이) | 입력한 길이만큼의 랜덤한 바이트 값을 만들어준다. |
toString(표현 방법) | 입력한 방식에 맞는 문자열로 변환시켜준다. 'base64'는 6비트 이진 데이터를 아스키 영역의 문자들로 이루어진 문자열로 바꾸는 인코딩 방식이다. |
pbkdf2Sync(원문, 솔트, 해시 함수반복 횟수, 출력 바이트 수, 해시 알고리즘) |
단방향 해시 함수를 사용하여 비밀번호를 암호화하며 동기적으로 동작한다. 단방향 암호화이다. |
salt를 사용하는 이유
사용자 A와 B가 모두 "1234"라는 비밀번호를 사용하고, "1234"를 sha512 알고리즘으로 암호화하여 DB에 저장했다고 가정해 보자.
이 상황에서 해커가 DB에 접근해서 사용자 A의 비밀번호를 복호화하는 데 성공했다. 그러면 해커는 사용자 B의 평문 비밀번호가 무엇인지도 알 수 있게 될 것이다. 해커가 평문-암호화 비밀번호 테이블을 만들어놓고 데이터베이스에 있는 사용자들의 비밀번호를 알아내는 게 가능해진다는 뜻이다.
그래서 같은 비밀번호를 암호화해도 같은 암호문이 되지 않도록 원본 비밀번호에 랜덤한 문자열을 덧붙여 암호화한다. 여기서 덧붙여지는 문자열을 salt라고 한다.
해시 함수를 여러 번 사용하는 이유
원문을 해시 함수에 돌려 나온 결과를 다시 해시 함수에 넣고, 그 결과를 다시 해시 함수에 넣는 작업을 반복한다.
해시 함수에 몇 번을 돌릴지는 개발자만 알고 있기 때문에 해커는 해시 함수를 몇 번 돌려야 하는지 모른다. 그래서 해커는 해시 함수를 몇 번 돌려야 하는지도 알아내야 하며, 해커가 함수를 돌리는 횟수를 알아냈다고 해도 큰 횟수를 지정해 놓았다면 한 번 암호화하는 데에 시간이 많이 소모된다. 결과적으로 무차별적으로 값을 대입해 보는 공격을 막을 수 있다.
사용자 API 로직 구현
회원가입
사용자에게 입력받은 비밀번호를 암호화해서 데이터베이스에 저장한다.
const join = (req, res) => {
const { email, password } = req.body;
const salt = crypto.randomBytes(10).toString('base64');
const hashPassword = crypto.pbkdf2Sync(password, salt, 10000, 10, 'sha512').toString('base64');
const sql = `INSERT INTO users(email, password, salt) VALUES(?, ?, ?)`;
const values = [email, hashPassword, salt]
conn.query(sql, values,
(err, results) => {
if (err) {
console.log(err);
return res.status(StatusCodes.BAD_REQUEST).end(); // bad request
}
res.status(StatusCodes.CREATED).json(results);
});
};
로그인
입력받은 이메일로 사용자를 조회한다. 조회한 결과의 salt를 이용하여 입력받은 비밀번호가 일치하는지 확인한다.
아이디나 비밀번호가 틀렸을 경우 미인증을 의미하는 401 상태 코드를 보내준다.
나는 401은 미인증, 403은 미인가이고, 로그인은 인증 과정인데 인증을 못한 것이므로 401이 맞다고 생각하는데 이 부분은 사람 취향마다 다른 것 같다.
const login = (req, res) => {
const { email, password } = req.body;
const sql = `SELECT * FROM users WHERE email = ?`;
conn.query(sql, email,
(err, results) => {
if (err) {
console.log(err);
return res.status(StatusCodes.BAD_REQUEST).end();
}
const loginUser = results[0];
const hashPassword = crypto.pbkdf2Sync(password, loginUser.salt, 10000, 10, 'sha512').toString('base64');
if (loginUser && loginUser.password === hashPassword) {
const token = jwt.sign({
email: loginUser.email
}, process.env.PRIVATE_KEY, {
expiresIn: '30m',
issuer: 'minjin'
});
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;
const sql = `SELECT * FROM users WHERE email = ?`;
conn.query(sql, email,
(err, results) => {
if (err) {
console.log(err);
return res.status(StatusCodes.BAD_REQUEST).end();
}
const user = results[0];
if (user) {
return res.status(StatusCodes.OK).jaon({
email: email
});
} else {
return res.status(StatusCodes.UNAUTHORIZED).end();
}
}
)
};
비밀번호 초기화
비밀번호를 수정하면 salt도 새로 만든다.
const passwordReset = (req, res) => {
const { email, password } = req.body;
const salt = crypto.randomBytes(10).toString('base64');
const hashPassword = crypto.pbkdf2Sync(password, salt, 10000, 10, 'sha512').toString('base64');
const sql = `UPDATE users SET password = ?, salt = ? WHERE email = ?`;
const 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();
}
})
};
'데브코스' 카테고리의 다른 글
[7주차 복습 발표] 암호화 (0) | 2024.04.11 |
---|---|
[7주차 - DAY4] 도서 API 구현 (0) | 2024.04.11 |
[7주차 - DAY2] 도서 정보 API 설계 및 구현 (0) | 2024.04.09 |
[7주차 - DAY1] 도서 정보 API 설계 및 ERD 설계 (0) | 2024.04.08 |
[6주차 - DAY5] 도서 정보 API 설계 (0) | 2024.04.05 |