-
[데브코스] 44일차 TILTIL/교육 내용 정리 2024. 5. 22. 19:32
회원가입 API 구현
// mariadb.js const mariadb = require('mysql2'); const connection = mariadb.createConnection({ host: '127.0.0.1', user: 'root', password: 'root', database: 'Bookshop', dateStrings : true }); module.exports = connection
mysql2 모듈을 mariadb에 저장하고 연결 통로를 만들어준다. 여기서 host자리에 2가지가 가능하다. 더 자세히 ← 클릭 다른 파일에서도 모듈을 사용할 수 있게 수출.
// users.js const express = require('express'); const router = express.Router(); const conn = require('../mariadb'); router.use(express.json()); // 회원가입 router.post('/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(400).end(); } return res.status(201).json(results); } ) });
conn 변수에 mariadb.js 파일을 불러온다. email 과 password 를 request body에 저장을 하고 sql , values 변수를 통해 쿼리문과 email,password를 저장.
왜 err 가 발생했을 때, status(400) 을 던져줄까? cf. 404 - not found, 400 - bad request 우선 err 원인은 입력값 혹은 db 의 문제일 것이다. 우리는 여기서 입력값 에 문제가 있다고 생각하고 날려줄 것이다. 이유는 db 에서는 데이터가 잘못 들어와도 모르기 때문이다.
<aside> 💡 status(400) 고도화 하드코딩 (= 날 것으로 들어감) 하지 말고 변수에 담아서 보내주자. 그 이유는 400을 못 외웠을 수도 있고, 어떤 상태인지 가독성을 높이기 위해.
아래는 npm 을 활용해서 고도화 하는 방법이다.</aside>
http-status-codes 모듈 활용해보기
- npm 으로 http-status-codes 모듈 불러온다.
// users.js const {StatusCodes} = require('http-status-codes');
- http-status-codes 는 다양한 형태로 존재하기 때문에 비구조화를 사용해서 json 형태로 받아야 된다.
// users.js res.status(StatusCodes.BAD_REQUEST).end(); // 400 res.status(StatusCodes.CREATED).json(results); // 201
node.js 패키지(파일) 구조(feat. controller)
현재 우리 패키지는
- 라우터의 역할 : 어떤 req가 날라올 때 어떤 res 돌려줄지 경로를 찾아줌.
- app.js : 프로젝트의 main router 역할
- /route
- /users.js : 하위 라우터 역할 + 로직까지 있음.
- /books.js : 하위 라우터 역할 + 로직까지 있음.
💡 라우터가 로직까지 다 수행할 때 단점
- 프로젝트 규모가 커질 수록, 코드 복잡해짐.
- 가독성이 떨어짐.
- 트러블 슈팅 (에러를 찾고 해결하는 과정) 힘들다.
즉, 유지 보수 하기 어렵다.
cf. 유지 보수란? 운영을 하면서 요구 사항 반영, 에러 해결,…. 안정적으로 작동하게 하는 것.
💡 코드를 간결하고 가독성이 높게 만들어 주자
- 경로를 찾은 다음 역할인 콜백 함수 를 빼자.
- 어떻게 해야되나? 우선 컨트롤러의 개념을 알아야 된다.
컨트롤러
- 프로젝트에서 매니저 역할을 하는 파일 (관장을 하는 파일)
- 매니저이기 때문에 누군가에게 일을 어떻게 시켜야 할 지 알고 있다. (=직접 일을 하진 않을 것.)
라우터를 통해서 “사용자의 요청”이 길을 찾아오면 매니저가 환영해준다. → 매니저가 서비스(알바생)한테 일을 시킴 → 서비스(알바생)이 결과물을 매니저에게 전달 → 매니저는 사용자에게 res 돌려준다.
- router을 통해서 ( 라우터는 길을 찾는 용도로만 사용)
- 사용자의 요청(req)이 길(url)을 찾아오면
- controller(=콜백함수)가 환영해주고
- 서비스한테 일을 시키고 결과물을 controller 전달
- controller 가 사용자에게 res을 돌려줌.
자 이제 시도해보자.
// /controller/UserController.js const conn = require('../mariadb'); const {StatusCodes} = require('http-status-codes'); 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(); } return res.status(StatusCodes.CREATED).json(results); } ) } module.exports = join
원래 users.js 에 있던 콜백함수를 가져와서 join에 넣어주고 수출하면 된다.
// users.js const join = require('../controller/UserController'); // 회원가입 router.post('/join', join);
users.js 에서는 join를 불러오고 콜백 함수 있던 자리에 join을 넣어주면 된다.
로그인 API 구현 + unauthorized
//UserController.js /* 여기서 이제 여러 모듈을 수출할 건데 module.exports 은 한 개 밖에 안된다 그러면 어떻게 */ module.exports ={join, login, passwordResetRequest, passwordReset} // json 형태로 보내준다 ~
// users.js // 여기서도 받아올 떄 비구조화를 json형태 const { join, login, passwordResetRequest, passwordReset } = require('../controller/UserController');
// UserController.js const jwt = require('jsonwebtoken'); const dotenv = require('dotenv'); dotenv.config(); 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(); } const loginUser = results[0]; if(loginUser && loginUser.password == password){ const token = jwt.sign({ email : loginUser.email }, process.env.PRIVATE_KEY,{ expiresIn : '5m', issuer : 'donghyun' }); res.cookie("token", token,{ httpOnly : true }); console.log(token); return res.status(StatusCodes.OK).json(results); } else{ return res.status(StatusCodes.UNAUTHORIZED).end(); } }) };
1. const { email, password } = req.body;
- **req.body**에서 클라이언트가 보낸 **email**과 **password**를 추출합니다.
- 이는 클라이언트가 보낸 요청 데이터에서 이메일과 비밀번호를 가져오는 부분입니다.
2. let sql = SELECT * FROM users WHERE email = ?;
- SQL 쿼리를 정의합니다. 여기서는 users 테이블에서 특정 이메일을 가진 사용자를 찾는 쿼리입니다.
- **?**는 쿼리 매개변수 자리표시자입니다. 실제 값은 나중에 conn.query 호출 시 제공됩니다.
3. conn.query(sql, email, (err, results) => { ... })
- MySQL 데이터베이스에 쿼리를 실행합니다.
- **SELECT * FROM users WHERE email = 'user@example.com';**와 같은 쿼리를 실행한 후, 결과는 results 변수에 저장됩니다.
- 보통 이런 결과는 배열 형태로 반환됩니다. 그래서 **results[0]**는 첫 번째 결과를 의미하고, 그 결과는 해당 이메일 주소를 가진 사용자의 정보를 포함한 객체일 것입니다.
//results[0]은 다음과 같은 객체입니다: { id: 1, email: 'john@example.com', password: 'hashedpassword123', name: 'John Doe', created_at: '2023-01-01 12:00:00' }
- **sql**은 실행할 쿼리이고, **email**은 쿼리 매개변수로 전달됩니다.
- 콜백 함수는 쿼리 결과를 처리합니다.
4. if (err) { ... }
- 쿼리 실행 중 오류가 발생한 경우 이를 처리합니다.
- **console.log(err);**로 오류를 로그에 출력하고, 클라이언트에 400 상태 코드를 반환합니다.
5. const loginUser = results[0];
- 쿼리 결과의 첫 번째 항목을 loginUser 변수에 할당합니다.
- results 배열의 첫 번째 요소가 로그인하려는 사용자입니다.
- results[0] : email 컬럼이 주어진 이메일과 일치하는 모든 행을 선택함. 따라서 users 테이블에 있는 email과 password를 가져옴.
- kim@mail.com 를 검색하면 중복 없으니깐 kim@mail,com 하나만 나올 것이고 배열의 인덱스 0은 당연히 kim@mail.com에 해당 되는 정보가 나올 것임.
6. if (loginUser && loginUser.password == password) { ... }
- 사용자가 존재하고 비밀번호가 일치하는지 확인합니다.
- **loginUser.password**와 **password**를 비교하여 비밀번호가 맞는지 확인합니다.
7. const token = jwt.sign({ email: loginUser.email }, process.env.PRIVATE_KEY, { ... });
- JWT(JSON Web Token)를 생성합니다.
- jwt.sign 메서드를 사용하여 토큰을 생성하며, 페이로드로 이메일을 포함합니다.
- **process.env.PRIVATE_KEY**는 토큰을 서명하는 데 사용되는 비밀 키입니다.
- **expiresIn**은 토큰의 유효 기간을 5분으로 설정합니다.
- **issuer**는 토큰 발급자를 나타냅니다.
8. res.cookie("token", token, { httpOnly: true });
- 생성된 토큰을 HTTP 쿠키로 설정하여 클라이언트에 보냅니다.
- httpOnly 옵션은 클라이언트 측 JavaScript에서 쿠키에 접근할 수 없도록 합니다.
9. return res.status(StatusCodes.OK).json(results);
- 로그인 성공 시, 200 상태 코드와 함께 사용자 정보를 JSON 형식으로 응답합니다.
10. else { return res.status(StatusCodes.UNAUTHORIZED).end(); }
- 로그인 실패 시, 401 상태 코드로 응답합니다.
403 접근 권리 없다 → 서버가 이 사람 누군지 알고 있다.
401 비인증 → 서버가 이 사람 누군지 모른다.
비밀번호 초기화 요청 & 초기화
Method POST
URI /users/reset status 200 req body { email : 사용자가 입력한 이메일
} | | res body | { email : 이메일 } |
Method PUT
URI /users/reset status 200 req body { email : 이전 페이지에서 입력했던 이메일 password : 사용자가 입력한 비밀번호
} | | res body | |
// 비밀번호 초기화 요청 단계에서 json형태로 email을 보내주자. const user = results[0]; if(user){ return res.status(StatusCodes.ACCEPTED.OK).json({ **email : email** }); }else{ return res.status(StatusCodes.UNAUTHORIZED).end(); }
// UserController.js 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 == 0) return res.status(StatusCodes.BAD_REQUEST).end(); else{ return res.status(StatusCodes.OK).json(results); } } ) };
- if(results.affectedRows == 0): 쿼리가 실행되었지만 영향을 받은 행이 없는 경우를 확인합니다. 이는 해당 이메일을 가진 사용자가 데이터베이스에 존재하지 않는 경우입니다.
- return res.status(StatusCodes.OK).json(results);: 쿼리가 성공적으로 실행되고 영향을 받은 행이 있을 때, 즉 비밀번호가 업데이트된 경우에는 클라이언트에게 200 OK 상태 코드와 업데이트된 결과를 JSON 형식으로 반환합니다.
회원가입 시 비밀번호 암호화
💡 내장 모듈을 사용해서 db에 들어가 있는 비밀번호 암호화
// UserController.js const cryto = require('crypto');
// UserController.js const salt = cryto.randomBytes(64).toString('base64'); const hashPassword = cryto.pbkdf2Sync(password, salt, 10000, 64, 'sha512').toString('base64');
- 위 코드만으로는 단방향이라 암호화는 가능하지만 복호화는 불가능하다.
- cryto.randomBytes(64).toString('base64'); 부분 때문에
💡 그럼 어떻게 해야될까?
- salt를 고정을 하거나, 반쪽짜리 암호화.,.,,
- salt는 db에 저장하는 것.
회원가입시 비밀번호를 암호화해서 암호화된 비밀번호와 salt 값을 같이 저장 로그인 시, 이메일이랑 비밀번호(날것) 받으면 salt 값 꺼내서 암호화 해보고 db에 저장된 비밀번호랑 비교한다.
// UserController.js const join = (req, res) =>{ const {email, password} = req.body; let sql = `INSERT INTO users (email, password, **salt**) VALUES (?,?,?)`; // 암호화된 비밀번호와 salt 값을 같이 DB에 저장 const salt = cryto.randomBytes(64).toString('base64'); const hashPassword = cryto.pbkdf2Sync(password, salt, 10000, 64, 'sha512').toString('base64'); // 로그인시 emai,pwd(날것) 0> salt 값 꺼내서 비밀번호 암호화 // 해보고 db 비밀번호랑 비 let values = [email, **hashPassword**, **salt**]; conn.query(sql,values, (err, results) => { if(err){ console.log(err); return res.status(StatusCodes.BAD_REQUEST).end(); } return res.status(StatusCodes.CREATED).json(results); } ) };
- salt 에서 만들어진 랜덤 값 + 날 것의 비번 → hashPassword
- 그런데 salt 값이 매번 바뀌니깐 db에 저장
- 그러면 이제 db에서 salt값 + 날것의 비번 → hashPassword 나오겠지
로그인 어떻게? 비밀번호 초기화?
// UserController.js 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(); } const loginUser = results[0]; const hashPassword = cryto.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 : '5m', issuer : 'donghyun' }); res.cookie("token", token,{ httpOnly : true }); console.log(token); return res.status(StatusCodes.OK).json(results); } else{ return res.status(StatusCodes.UNAUTHORIZED).end(); } }) };
- 날 것의 비밀번호를 암호화해줘서 비교한다.
업데이트 여러개 어떻게? + 원래대로라면 이메일을 조회해서 새롭게 만들어야 되는 데 바꿔 말하면 애초에 그냥 업데이트를 해줘버림.
'TIL > 교육 내용 정리' 카테고리의 다른 글
[데브코스] 43일차 TIL (0) 2024.05.21 [데브코스]42일차 TIL (0) 2024.05.20 [데브코스] 30일차 TIL (0) 2024.05.07 [데브코스] 28일차 TIL (0) 2024.05.05 [데브코스] 27일차 TIL (0) 2024.05.04