엘리스 33일차 수요일 실시간강의 회원가입, 회원정보 암호화 및 검증

치즈말랑이·2022년 5월 18일
0

엘리스

목록 보기
34/47
post-thumbnail

박두현님 강의

app단에서 localhost:3000/users로 연결시켜놨다.

user 모델 설정

models/newuser.js

const mongoose = require('mongoose');
const Schema = mongoose.Schema;

const userSchema = new Schema({
    email: {
        type: String,
        required: true,
    },
    password: {
        type: String,
        required: true,
    }
});

const userData = mongoose.model('newusers', userSchema);
module.exports = userData;

userSchema를 만들어준다.
간단하게 이메일과 비밀번호만 설정하고, newusers 모델을 만들어준다.

ejs

views/blog/login.ejs

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <h2>로그인하기</h2>
    <form action="/users/login" method="post">
        <input type="text" name="email" placeholder="이메일">
        <br><br>
        <input type="password" name="password" id="" placeholder="비밀번호">
        <br><br>
        <input type="submit" value="전송하기">
    </form>
</body>
</html>

localhost:3000/users/login으로 form data를 post method로 전송한다.

views/blog/auth.js

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <h2>가입하기</h2>
    <form action="/users/signup" method="post">
        <input type="text" name="email" placeholder="이메일">
        <br><br>
        <input type="password" name="password" id="" placeholder="비밀번호">
        <br><br>
        <input type="submit" value="전송하기">
    </form>
</body>
</html>

localhost:3000/users/signup으로 form data를 post method로 전송한다.

user routing 설정

routes/users.js
통코드

var express = require('express');
const userSchema = require('../models/newuser');
const bcrypt = require('bcrypt');
const {body, validationResult} = require('express-validator');
const session = require('express-session');
const parseurl = require('parseurl');
var router = express.Router();

/* GET users listing. */
router.get('/', function(req, res, next) {
  res.render('blog/auth');
});

// 정규 표현식으로 인증
//passport

router.get('/cookie', (req, res) => {
  // key(변수-이름) ,value(저장하고싶은값)
  res.cookie('drink', 'water');
  res.send('set cookies');
});

// 프로그래밍 => 디지털 프랜스포메이션
// 실제 생활에 있는걸 => 컴퓨터로 옮기는 일
// 

router.post('/signup'
        , body('email').isEmail().withMessage('아이디는 email 형태를 따르셔야 합니다.')
        , body('password').isLength({min:5}).withMessage('비밀번호는 최소 5글자 이상입니다.')
        , async (req, res) => {
  const email = req.body.email;
  const password = req.body.password;
  
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(400).json({
      errors: errors.array()
    });
  }

  // 중복 가입
  const findresult = await userSchema.findOne({email});

  if (findresult) {
    res.status(401).json({msg:'이미 가입된 계정입니다.'});
  } else {
    const salt = bcrypt.genSaltSync(10);
    const bcryptpw = bcrypt.hashSync(password, salt);
    // 복호화 할때 기준이 되는 메세지의 길이가 존재함.

    userSchema.create({
      email: email,
      password: bcryptpw
    }).then(result => {
      // console.log(result);
      res.status(200).json(result);
    });
  }
  // 찾는 쿼리.
  // 결과 존재 => 중복으로 가입이 되어 있는 경우.
  // 결과가 X => 신규가입.
  // 

});

router.post('/login', async (req, res) => {
  const email = req.body.email;
  const password = req.body.password;
  // 가입을 했던 유저인지 아닌지
  const userdata = await userSchema.findOne({email}).exec();

  if (!userdata) { // 유저 데이터가 없다면
    return res.status(401).json({msg: '가입되지 않은 계정입니다.'});
  } else { // 유저 데이터가 존재한다면 => 비밀번호가 매칭되는지
    const pwMatch = bcrypt.compareSync(password, userdata.password);
    if (pwMatch) {
      res.status(200).json({msg:'OK'});
    } else {
      res.status(401).json({msg:'비밀번호가 일치하지 않습니다.'});
    }
  }
});

router.get('/login', (req, res) => {
  res.render('blog/login');
});

// 쿠키와 세션
// 쿠키 => 사용자의 브라우저에 저장 데이터 모음 => JWT token => 정보 저쟝량 분산 => 비용 절감
// 보안 이슈의 문제로부터 훨씬 자유로움
// 쿠키로서 너의 정보를 너에게 저장. 그것이 잘못되는 것은 너의 책임이다.

// 세션 => 서버쪽에 저장하는 데이터 모음 => session 에 저장
// 각 나라마다 언어정보가 다름 ==> 번역된 텍스트를 각 나라에 맞게.

// 복붙
router.use(
  session({
    secret: "12345",
    resave: false,
    saveUninitialized: true
  })
);
// users.js -> router구간에서만 사용 가능하게 함
// 프로젝트 전체구간에서는 어떻게해얗할지 각자 고민


// 조건부 렌더링
//세션으 활용해서 어떻게 핳ㄹ건가
router.use(function (req, res, next) {
  if (!req.session.views) {
    req.session.views = {}
  }

  // get the url pathname
  var pathname = parseurl(req).pathname

  // count the views
  req.session.views[pathname] = (req.session.views[pathname] || 0) + 1

  next()
})
//api 과부하: Apache JMeter
// 외래키를 타고, 타고, 타고, 3~4 가지의 개념을 
// 하나의 큰 쿼리 경우들이 많이 생긴다.
// 1일 접속자 10만명단위

// node pm2 => 서버 과부하 있으면 서버 터짐, cpu 점유율이 초과되거나 메모리
// 1000명 을 25명단위로 자동으로 나누어서 과부하에 대한 영역을 줄여주고 안정을 높여주는 역할
// aws 로드밸런싱 : 컴퓨터 1대를 3대로
// pm2: cpu영역을 할당해서 안정성있게 처리해주는 역할

// React, vue, svelte
// single page application
// SPA => ReadableStreamDefaultController={, }
// Web => SPA => 모바일 =>  업로드 반영 18시간

// SPA => 모바일
// Saas 

router.get('/foo', function (req, res, next) {
  res.send('you viewed this page ' + req.session.views['/foo'] + ' times')
})

module.exports = router;

회원가입 GET

var express = require('express');
var router = express.Router();

router.get('/', function(req, res, next) {
  res.render('blog/auth');
});

localhost:3000/users/로 접속하면 views/blog/auth.ejs파일을 렌더링해서 보여준다.

회원가입 POST

var express = require('express');
const userSchema = require('../models/newuser');
const bcrypt = require('bcrypt');
const {body, validationResult} = require('express-validator');
var router = express.Router();

router.post('/signup'
        , body('email').isEmail().withMessage('아이디는 email 형태를 따르셔야 합니다.')
        , body('password').isLength({min:5}).withMessage('비밀번호는 최소 5글자 이상입니다.')
        , async (req, res) => {
  const email = req.body.email;
  const password = req.body.password;
  
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(400).json({
      errors: errors.array()
    });
  }

  // 중복 가입
  const findresult = await userSchema.findOne({email});

  if (findresult) {
    res.status(401).json({msg:'이미 가입된 계정입니다.'});
  } else {
    const salt = bcrypt.genSaltSync(10);
    const bcryptpw = bcrypt.hashSync(password, salt);
    // 복호화 할때 기준이 되는 메세지의 길이가 존재함.

    userSchema.create({
      email: email,
      password: bcryptpw
    }).then(result => {
      // console.log(result);
      res.status(200).json(result);
    });
  }
  // 찾는 쿼리.
  // 결과 존재 => 중복으로 가입이 되어 있는 경우.
  // 결과가 X => 신규가입.
  // 

});

장고에서 사용하는 is_valid와 비슷하게 검증을 하려면 node.js에서는 express-validator 패키지를 다운받아서 사용한다.
body('email').isEmail().withMessage('문구')로 req.body.email이 이메일형식을 갖추고 있는지, 만약 이메일 형식이 아니라면 메세지로 '문구'를 출력한다.
body('password').isLength({min:5}).withMessage('문구') 이것도 비슷한데, 비밀번호의 길이를 검증한다. min이 있따면 max로 있을듯.

이걸 미들웨어 형태로 검증을 하고 (req, res)로 넘겨준다.

const errors = validationResult(req)에서 에러가 있으면 반환값이 있고, 에러가 없으면 반환값이 없다.
반환값이 있으면 (빈값이 아니라면) 400을 내보낸다.

이렇게 이메일과 비밀번호가 양식 조건을 충족했는지 검증을 한 후, 데이터베이스에 중복계정인지 또 검증한다.
중복계정이 아니라면 bcrypt로 비밀번호를 암호화를 한다.

const salt = bcrypt.genSaltSync(10);
const bcryptpw = bcrypt.hashSync(password, salt);

bcrypt.genSaltSync(숫자)는 암호화길이를 숫자만큼이라고 정해주는것이고, bcrypt.hashSync(암호화할거, 길이)를 통해 암호화한다.

원본 비밀번호는 냅두고 암호화된 비밀번호를 이메일과 함께 데이터베이스에 저장한다. 이제 이것은 본인 외에 아무도 모른다.

로그인 GET

router.get('/login', (req, res) => {
  res.render('blog/login');
});

로그인 POST

var express = require('express');
const userSchema = require('../models/newuser');
const bcrypt = require('bcrypt');
var router = express.Router();

router.post('/login', async (req, res) => {
  const email = req.body.email;
  const password = req.body.password;
  // 가입을 했던 유저인지 아닌지
  const userdata = await userSchema.findOne({email}).exec();

  if (!userdata) { // 유저 데이터가 없다면
    return res.status(401).json({msg: '가입되지 않은 계정입니다.'});
  } else { // 유저 데이터가 존재한다면 => 비밀번호가 매칭되는지
    const pwMatch = bcrypt.compareSync(password, userdata.password);
    if (pwMatch) {
      res.status(200).json({msg:'OK'});
    } else {
      res.status(401).json({msg:'비밀번호가 일치하지 않습니다.'});
    }
  }
});
  1. 데이터베이스에서 이메일이 일치하는 계정이 있는지 찾는다.
    1-2. 이메일이 일치하는 유저데이터가 존재하지 않는다면 401 띄운다.
  2. bcrypt.compareSync(입력받은 비밀번호, 데이터베이스에 저장된 비밀번호)로 비밀번호가 일치하는지 확인한다.
    2-1. 비밀번호가 다르다면 pwMatch가 false이므로 401을 내보낸다.
    2-2. 비밀번호가 같다면 pwMatch가 true이므로 200을 내보낸다.

참고자료

  1. bcrypt: https://www.npmjs.com/package/bcrypt
  2. express-validator: https://express-validator.github.io/docs/

코치님 강의

JWT (Json Web Token)
https://jwt.io/introduction
Debugger: https://jwt.io/#debugger-io

잠깐 자바스크립트

class UserAuthService {

  async getUser({ email, password }) {
    // 이메일 db에 존재 여부 확인
 
    // if isPasswordEqual false, password not matches
    if(!isPasswordEqual) {
        const errorMessage = "비밀번호가 일치하지 않습니다.";
        return { errorMessage };
    }

여기서 errorMessage는 다른 파일에서 UserAuthService.getUser({email, pwd}).errorMessage로 접근할 수 있다.

jwt token 생성방법

const secretKey = process.env.JWT_SECRET_KEY || "jwt-secret-key";
// jwt 의 sign 함수를 이용하여 토큰 생성, 이 때 위의 secretKey 사용

// jsonwebtoken은 비동기 처리 안함
const token = jwt.sign({user_id: user.id}, secretKey);
// jwt.verify 함수를 이용하여 정상적인 jwt인지 확인 
const jwtDecoded = jwt.verify(userToken, secretKey);

// verify 함수로부터 반환된 결과에서 user_id 추출
const user_id = jwtDecoded.user_id;

jwt.verify(토큰, 시크릿키)는 토큰이 유효하다면 토큰안에 저장해놓은 값이 반환된다. 여기에서는 jwt.sign({user_id:user.id})로 토큰을 발급받았으므로 {user_id:user.id}가 반환값이다.

참고: https://carrotweb.tistory.com/117
https://redbinalgorithm.tistory.com/692

profile
공부일기

0개의 댓글