ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [데브코스] 44일차 TIL
    TIL/교육 내용 정리 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 : 하위 라우터 역할 + 로직까지 있음.

     💡 라우터가 로직까지 다 수행할 때 단점

     

    1. 프로젝트 규모가 커질 수록, 코드 복잡해짐.
      1. 가독성이 떨어짐.
      2. 트러블 슈팅 (에러를 찾고 해결하는 과정) 힘들다.

    즉, 유지 보수 하기 어렵다.

    cf. 유지 보수란? 운영을 하면서 요구 사항 반영, 에러 해결,…. 안정적으로 작동하게 하는 것.

     💡 코드를 간결하고 가독성이 높게 만들어 주자

     

    • 경로를 찾은 다음 역할인 콜백 함수 를 빼자.
    • 어떻게 해야되나? 우선 컨트롤러의 개념을 알아야 된다.

    컨트롤러

    • 프로젝트에서 매니저 역할을 하는 파일 (관장을 하는 파일)
    • 매니저이기 때문에 누군가에게 일을 어떻게 시켜야 할 지 알고 있다. (=직접 일을 하진 않을 것.)

    라우터를 통해서 “사용자의 요청”이 길을 찾아오면 매니저가 환영해준다. → 매니저가 서비스(알바생)한테 일을 시킴 → 서비스(알바생)이 결과물을 매니저에게 전달 → 매니저는 사용자에게 res 돌려준다.

    1. router을 통해서 ( 라우터는 길을 찾는 용도로만 사용)
    2. 사용자의 요청(req)이 길(url)을 찾아오면
    3. controller(=콜백함수)가 환영해주고
    4. 서비스한테 일을 시키고 결과물을 controller 전달
    5. 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'); 부분 때문에

    💡 그럼 어떻게 해야될까?

     

    1. salt를 고정을 하거나, 반쪽짜리 암호화.,.,,
    2. 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
Designed by Tistory.