Express-generator 프로젝트 구조
- bin/www : 포트번호 등과 같은 웹 서버를 구축하는데에 필요한 설정 데이터가 정의되어 있는 파일
- .env파일과 같이 설정값을 가지고 에러처리, 기타추가 설정을 해주는 파일
- node_modules : Node.js, Express에 필요한 모듈들이 설치되는폴더
- public : images, javascripts, stylesheets -> 정적(ex. 로고, 회사 소개 페이지) 파일
- routes : 각 경로를 담당하는 모듈들이 들어있는 폴더 = 라우팅 로직을 구현하는 모듈들 : 클라이언트에서 어떤 요청을 주냐에 따라서 어떤 로직을 수행할 지 파일별로 분할 해서 관리하는 정도
- 자바의 controller 역할
- views : 클라이언트에게 html코드로 "화면을 보내는파일"
- app.js(서버의 시작점) -> URL에 따라서 라우팅
- package.json : 이 프로젝트에 설치된 모듈 이름, 버전 등등 정보들이 작성되어 있는 파일
컨트롤러
계층분리의필요성
지금까지 Express를 학습하면서 계층구조는 단순히 app.js에서 routes를 설정해주고 url에 따른 사용자의 요청이 오면 해당 url의 routes로가서 로직을 실행시켜주는 구조였다.
routes에서 로직을 정의하고 실행시키는 것의 장점은 어떤 엔드포인트로 접근햇을 때 어떤 동작을 하게 되는지 한 눈에 볼 수 있어 유용했다. 하지만 단순한프로젝트가 아닌 규모가 커지게 된다면 하나의 routes안에서 코드가 많아져 관리하는 것이 힘들어 질 수 있다. 즉 유지보수가능한코드가 되기 어렵다. 따라서 장기적인 측면을 생각했을때 정확한 계층 분리는 꼭 필요하다.
- Router : 엔드포인트와 해당 엔드포인트에서 실행되어야 할 로직을 연결해주는 역할
- Controller : Middlewar의 일종이지만 메인 로직을 담당하므로 분리해서 관리
- Middleware : 메인 로직의 Controller 앞뒤로 추가적인 일을 담당
따라서 Router는 정말 코드의 전체적인 흐름, 경로만 나타내주고 Controller가 메인 로직을 담당(Service 계층으로연결, Service는 DB계층으로 연결), Middleware는 그 외의 특별한 일을 함수로 따로 빼서 거치도록 해주는 느낌이다.
아래는 회원가입을 컨트롤러를 통해 분리하였다.
users.js
...
const join = require('../controller/UserController');
...
//회원가입
router.post('/join',
[
body('email').notEmpty().isEmail().withMessage('이메일확인필요'),
body('password').notEmpty().isString().withMessage('비밀번호 확인필요'),
validate
]
,join);
UserController.js
const conn = require('../mariadb'); //db모듈
const {StatusCodes} = require('http-status-codes');//status code 모듈
const join = (req,res)=>{
let {email,password}=req.body;
const sql = `INSERT INTO users (email,password)
VALUES(?,?)`
let values =[email,password];
conn.query(sql,values,
function(err,results,fields){
if(err){
console.log(err);
return res.status(StatusCodes.BAD_REQUEST).end();
}
return res.status(StatusCodes.CREATED).json(results);
})
}
module.exports=join;
추가적으로 로그인, 비밀번호초기화요청, 비밀번호초기화까지 모두 컨트롤러에 작성해보았다.
먼저 유저라우터인 users.js이다
const express = require('express'); //express 모듈
const router = express.Router();
const conn = require('../mariadb'); //db모듈
const {body,validationResult}= require('express-validator');
const {
join,
login,
passwordResetRequest,
passwordReset
} = require('../controller/UserController');
router.use(express.json());
const validate = (req,res,next)=>{
const err = validationResult(req);
if(err.isEmpty()){
return next();
}
else{
return res.status(400).json(err.array());
}
}
//회원가입
router.post('/join',
[
body('email').notEmpty().isEmail().withMessage('이메일확인필요'),
body('password').notEmpty().isString().withMessage('비밀번호 확인필요'),
validate
]
,join);
router.post('/login',login); //로그인
router.post('/reset',passwordResetRequest); //비밀번호 초기화 요청
router.put('/reset',passwordReset); //비밀번호 초기화
module.exports=router
다음을 유저 컨트롤러인 UserController.js이다.
const conn = require('../mariadb'); //db모듈
const {StatusCodes} = require('http-status-codes');//status code 모듈
const jwt = require('jsonwebtoken');
const dotenv = require('dotenv');
dotenv.config();
const join = (req,res)=>{
let {email,password}=req.body;
const sql = `INSERT INTO users (email,password)
VALUES(?,?)`;
const values =[email,password];
conn.query(sql,values,
(err,results,fields)=>{
if(err){
console.log(err);
return res.status(StatusCodes.BAD_REQUEST).end();
}
return res.status(StatusCodes.CREATED).json(results);
})
};
const login = (req,res)=>{
let {email,password}=req.body;
const sql = `SELECT * FROM users WHERE email=?`;
const values = [email,password];
conn.query(sql,values,
(err,results,fields)=>{
if(err){
console.log(err);
return res.status(StatusCodes.BAD_REQUEST).end();
}
const loginUser=results[0];
if(loginUser&&loginUser.password==password){
const token = jwt.sign({
email:loginUser.email,
name:loginUser.name
},process.env.PRIVATE_KEY,{
expiresIn:'5m',
issuer:"choims"
});
res.cookie("token",token,{
httpOnly:true
});
console.log(token);
return res.status(StatusCodes.CREATED).json({
message:`${loginUser.email}님 로그인 되었습니다.`
});
}else{
return 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).end();
}
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;
const 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==0)
return res.status(StatusCodes.BAD_REQUEST).end();
else
return res.status(StatusCodes.OK).json(results);
})
};
module.exports={
join,
login,
passwordResetRequest,
passwordReset
};
비밀번호 암호화
현재 user의 db를 보면 password가 암호화가 되어있지 않고 그대로 있는 것을 볼 수 있는데 만약 db가 해킹당하게 되면 회원의 password가 그대로 노출이 될 수 있다. 따라서 반드시 암호화를 해야합니다.
암호화 방법
- 단방향 암호화 : 복호화(암호화된 문자열을 다시 원래 문자열로 돌려놓는 것) 할 수 없는 암호화 방법
- 복호화 할 수 없으면 왜필요? -> 잘생각해보면 홈페이지 비밀번호 같은 경우는 복호화 할 필요가 없다. 비밀번호를 암호화해서 db에 저장해둔 후, 나중에 로그인할 때, 다시 입력받은 비밀번호를 같은 알고리즘으로 암호화해서 DB에 저장된 문자열과 비교하면 된다. 즉 원래 비밀번호는 어디에도 저장되지 않고, 암호화된 문자열로만 비교
- 양방향 암호화 : 비대칭형 암호화, 대칭형 암호화
단방향 암호화를 해보겠습니다. 기본 내장 모듈인 크립토모듈(crypto)를 불러와서 암호화를 해주겠습니다.
...
const crypto = require('crypto'); // crypot 모듈 : 암호화
...
//비밀번호 암호화
const salt = crypto.randomBytes(64).toString('base64');
const hashPassword = crypto.pbkdf2Sync(password, salt, 10000, 64,'sha512').toString('base64');
여기서 salt(소금)라는 특정값을 통해 레인보우테이블을 방지할 수 있습니다.
- 레인보우 테이블 : 서로 다른 유저가 '1234', '1234' 비밀번호로 회원가입을 했다고 가정하면, 둘의 암호화된 비밀번호가 같아진다. 해커는 이를 통해 비밀번호를 유추할 수 있다.(이를 찾는 문자열의 목록을 레인보우 테이블)
즉 slat는 랜덤 문자열을 생성해서 비밀번호와 같이 DB에 저장하면 된다. 위의 코드를 자세히 살펴보면
- randomBytes : 메서드르 64바이트 길이의 salt를 생성, base64문자열 salt로 변경
- pbkdf2Sync : 인자는 차례대로 비밀번호, salt, 반복 횟수, 비밀번호 길이, 해시 알고리즘 순
- 반복 횟수는 해시함수를 몇번 반복하느냐를 나타낸다. 이 숫자가 높을수록 슈퍼컴퓨터를 써도 레인보우 테이블을 만들기 힘들어진다.
그래서 이제 위의 코드를 가지고 어떻게 활용하나면
- 회원가입 시, 비밀번호를 암호화해서 암호화된 비밀번호와, salt 값을 같이 저장
- 로그인 시, 이메일&비밀번호(날 것) -> salt 값 꺼내서 비밀번호 암호화 해보고 -> 디비 비밀번호랑 비교
그러기 위해서는 db를 수정해줘야 한다. users테이블에 salt라는 열을 하나 추가해준다.
위의 코드를 추가한 회원가입 api코드이다.
const join = (req,res)=>{
let {email,password}=req.body;
const sql = `INSERT INTO users (email,password,salt) VALUES(?,?,?)`;
//암호화된 비밀번호와 salt값을 같이 DB에 저장
const salt = crypto.randomBytes(10).toString('base64');
const hashPassword = crypto.pbkdf2Sync(password, salt, 10000, 10,'sha512').toString('base64');
const values =[email,hashPassword,salt];
conn.query(sql,values,
(err,results,fields)=>{
if(err){
console.log(err);
return res.status(StatusCodes.BAD_REQUEST).end();
}
return res.status(StatusCodes.CREATED).json(results);
})
};
수정한 후 post를 해본뒤 db를 확인해보면
그 뒤 로그인을 할때는 어떻게 할까요? db에서 salt를 꺼내서 비밀번호를 body에 받고 다시 암호화를 해서 비교해주면됩니다. 코드입니다.
const login = (req,res)=>{
let {email,password}=req.body;
const sql = `SELECT * FROM users WHERE email=?`;
const values = [email,password];
conn.query(sql,values,
(err,results,fields)=>{
if(err){
console.log(err);
return res.status(StatusCodes.BAD_REQUEST).end();
}
const loginUser=results[0];
//salt값 꺼내서 비밀번호를 암호화하고 디비 비밀번호랑비교
const hashPassword = crypto.pbkdf2Sync(password, loginUser.salt, 10000, 10,'sha512').toString('base64');
if(loginUser&&loginUser.password==hashPassword){
const token = jwt.sign({
email:loginUser.email,
name:loginUser.name
},process.env.PRIVATE_KEY,{
expiresIn:'5m',
issuer:"choims"
});
res.cookie("token",token,{
httpOnly:true
});
console.log(token);
return res.status(StatusCodes.CREATED).json({
message:`${loginUser.email}님 로그인 되었습니다.`
});
}else{
return res.status(StatusCodes.UNAUTHORIZED).end();
}
})
};
비밀번호 초기화
비밀번호초기화입니다. UPDATE SET WHERE을 사용하는데 새로운 비밀번호로 업데이트할때 새로운 salt를 생성해서 회원가입처럼 똑같이 하면됩니다.
const passwordReset = (req,res)=>{
const {email,password} = req.body;
const sql = "UPDATE users SET password=? , salt=? WHERE email=?";
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==0)
return res.status(StatusCodes.BAD_REQUEST).end();
else
return res.status(StatusCodes.OK).json(results);
})
};
'백엔드 > node.js(express)' 카테고리의 다른 글
도서주문관리 프로젝트 - 4.비동기(async-awiat)이용한 쿼리실행(주문) (0) | 2024.01.11 |
---|---|
도서주문관리 프로젝트 - 3. 도서,좋아요 API(SQL시간범위, 페이지네이션,서브쿼리) (1) | 2024.01.11 |
도서주문관리 프로젝트 - 1.API 설계(API,테이블) (0) | 2023.12.28 |
JWT (0) | 2023.12.26 |
토이프로젝트 유튜브4 (유효성검사,next) (0) | 2023.12.23 |