영벨롭 개발 일지

[Express]express & mongoose 회원가입과 로그인 구현하기 본문

Back-end/Express

[Express]express & mongoose 회원가입과 로그인 구현하기

영벨롭 2022. 7. 8. 19:21

[ User Schema 작성 ]

1. 모듈 설치

$ npm i mongoose
$ npm i bcrypt

 

2. bycrpt 모듈

 Bcrypt는 Blowfish를 기반으로 만들어진 단방향 해시 함수로 흔히 사용되는 해시 알고리즘인 SHA-256을 사용해서 데이터를 해싱합니다. 이는 단방향 암호화이기 때문에 복호화가 불가능하여 회원가입 시 비밀번호 암호화에 용이합니다. 

 

3 saltRountds

 라운드는 블록체인이나 해시그래프 등 탈중앙화 분산형 시스템에서 다수의 노드들이 일정한 합의 알고리즘에 따라 의사결정을 내리는 최소한의 시간 단위를 말합니다. 

 

 saltROuntd는 bcrypt 해시를 계산하는 데 필요한 시간을 제어하며 값이 높을수록 더 많은 해싱 라운드가 수행되게 됩니다.

 

const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const bcrypt = require('bcrypt');
const saltRounds = 10;

const userSchema = new Schema({
  name: {
    type: String,
    required: true
  },
  email: {
    type: String,
    trim: true,
    required: true,
  },
  password: {
    type: String,
    validate: [
      function(password){
        if(password && password.length > 6){
          return true
        } else {
          console.log('비밀번호 8자 이하 오류')
          return false;
        }
      }
    ]
  }
})

// 회원가입 시 비밀번호 암호화
// .pre()를 통해 해당 스키마에 데이터가 저장(.save)되기 전 수행할 작업들
userSchema.pre('save', function(next) {
  var user = this;

  // 비번 변경될 때만 해싱작업 처리
  if(user.isModified('password')){
    // salt: 공격자가 암호를 유추할 수 없도록, 평문 데이터에 뿌려넣는 의미없는 데이터
    bcrypt.genSalt(saltRounds, function(err, salt){
      if(err){
        return next(err);
      }

      // 생성된 salt 값과 비밀번호를 인자로 넘겨줌
      // hash 생성
      bcrypt.hash(user.password, salt, function(err, hash){
        if(err){
          return next(err);
        }
        
        // hash 값을 user.password에 저장
        user.password = hash
        next()    // save() 처리
      })
    })
  } else {
    next();
  }
}, {});

// 로그인 시 비밀번호 암호화 -> 디비에 저장된 비밀번호와 비교
userSchema.methods.comparePassword = function(plainPassword, cb){
  bcrypt.compare(plainPassword, this.password, function(err, isMatch){
    if(err){
      return cb(err);
    }
    cb(null, isMatch)   // err은 null, isMatch는 true
  })
}

module.exports = mongoose.model('User', userSchema);

 

 

[ server.js ]

 

 server.js에서 서버 기본 세팅과, mongoDB를 연결한 뒤, /user인 엔드포인트로 회원가입과 로그인 관련 라우팅이 정의된 모듈을 불러줍니다. 

 

const express = require('express');
const app = express();
const cors = require('cors');
const bodyParser = require('body-parser')
const PORT = 4000;
const DBURI = `###`

// routes
const userRouter = require('./routes/user')

const mongoose = require('mongoose');

// mongoose의 connection 메소드를 변수 db에 할당
let db = mongoose.connection;
// db 연결 실패 시
db.on('error', console.error);
// db 연결 성공 시
db.once('open', () => {
  console.log('MongoDB is connected');
})

// mongodb cluster와 연결
mongoose.connect(DBURI, {
  useUnifiedTopology: true,
  useNewUrlParser: true
})


app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cors());

app.get('/', (req, res) => {
  res.send({ success: true })
})

app.use('/user', userRouter)

app.listen(PORT, () => {
  console.log(`Server is running on ${PORT}`)
})

 

 

 

[ 회원가입 구현 ]

// routes/user.js

// 회원가입
router.post('/register', async(req, res, next) => {
  const checkUser = await User.findOne({ email: req.body.email });

  // 이미 존재하는 이메일이라면 success: false
  if(checkUser){
    console.log('이미 존재하는 이메일')
    res.send({ success: false })
    return;
  }

  const user = new User();
  const { name, email, password } = req.body;

  user.name = name;
  user.email = email;
  user.password = password;

  user.save((err) => {
    if(err){
      return res.status(400).send(err);
    } else {
      return res.status(201).send({
        success: true
      })
    }
  })
})

 

1. 이메일 유효성 검사

 

 findOne을 통해 User 모델 중, 요청 body의 email과 동일한 email 값을 갖는 모델이 있는지 찾는 동작을 비동기 처리합니다. 

 

 이때 해당 모델이 존재하다면 DB에 유저 정보를 저장하지 않고 회원가입을 제한합니다. 

 

 

2. DB에 모델 저장

 

 이메일 유효성 검사를 마치고 나면, 새로운 모델을 선언 및 초기화하여 DB에 저장합니다. 

 

 

(mongoDB collection 확인)

 

 

 

[ 로그인 구현 ]

// routes/user.js

router.post('/login', async(req, res) => {
  const user = await User.findOne({ email : req.body.email })
  if(!user) {
      res.send({ success : false, error : 1 })
      return
  }

  user.comparePassword(req.body.password, (err, isMatch) => {
      if (!isMatch) {
          res.send({ success : false, error : 1 })
          return
      }

      res.send({
        success: true,
        user_info: {
          user_id: user._id,
          user_name: user.name
        }
      })
  })
})

 

1. 이메일 유효성 검사

 

 findOne()을 통해 요청 body의 email이 DB 모델 안에 있는지 확인합니다. 

 

 없다면, 회원가입되지 않은 유저이므로 success: false를 보냅니다. 

 

 

2. 비밀번호 유효성 검사

 

 스키마 작성 시 비밀번호 비교를 위해 작성한 comparePassword 메소드를 호출해 비밀번호가 매치되는지 확인합니다. 

 

 이메일과 비밀번호의 유효성 검사를 모두 마쳤다면 클라이언트에게 모델의 _id(ObejctId)와 name 값을 보내줍니다.

 

 

 

 

[ React 코드 ]

// Register.jsx

import React, { useState } from 'react';
import axios from 'axios';
import { APIURL } from '../apiUrl';
import { useNavigate } from 'react-router-dom';

// POST: apiurl/register
// body: { name, email, password }
// return: success
const Register = () => {
  const [input, setInput] = useState({
    name: '',
    email: '',
    password: ''
  })
  const { name, email, password } = input;

  const navigate = useNavigate();
  const sendRequest = async() => {
    const res = await axios.post(`${APIURL}/user/register`, {
      name, email, password
    })

    if(res.data.success){
      console.log('성공')
      navigate('/');
    } else {
      console.log('이미 존재하는 이메일')
    }
  }
  const onChange = (e) => {
    const {name, value} = e.target;
    setInput({
      ...input,
      [name]: value
    })
  }

  return (
    <>
      <div>이름 
        <input 
          type="text"
          name="name"
          value={name}
          onChange={onChange}
        />
      </div>
      <div>이메일 
        <input 
          type="text"
          name="email"
          value={email}
          onChange={onChange}
        />
      </div>
      <div>비번 
        <input 
          type="text"
          name="password"
          value={password}
          onChange={onChange}
        />
      </div>
      <button type="button" onClick={sendRequest}>등록</button>
    </>
  );
};

export default Register;
// Login.jsx

import React, { useState } from 'react';
import axios from 'axios';
import { APIURL } from '../apiUrl';
import { useNavigate } from 'react-router-dom';

const Login = () => {
  const [input, setInput] = useState({
    email: '',
    password: ''
  })
  const { email, password } = input;

  const navigate = useNavigate();
  const sendRequest = async() => {
    const res = await axios.post(`${APIURL}/user/login`, {
      email, password
    })

    if(res.data.success){
      console.log('성공', res.data)
      navigate('/');
    }
  }
  const onChange = (e) => {
    const {name, value} = e.target; 
    setInput({
      ...input,
      [name]: value
    })
  }

  return (
    <>
      <div>이메일 
        <input 
          type="text"
          name="email"
          value={email}
          onChange={onChange}
        />
      </div>
      <div>비번 
        <input 
          type="text"
          name="password"
          value={password}
          onChange={onChange}
        />
      </div>
      <button type="button" onClick={sendRequest}>Login</button>
    </>
  );
};

export default Login;

 

[ routes/user.js 전체코드 ]

const express = require('express');
const router = express.Router();
const User = require('../model/User');

router.post('/login', async(req, res) => {
  const user = await User.findOne({ email : req.body.email })
  if(!user) {
      res.send({ success : false, error : 1 })
      return
  }

  user.comparePassword(req.body.password, (err, isMatch) => {
      if (!isMatch) {
          res.send({ success : false, error : 1 })
          return
      }

      res.send({
        success: true,
        user_info: {
          user_id: user._id,
          user_name: user.name
        }
      })
  })
})

router.post('/register', async(req, res, next) => {
  const checkUser = await User.findOne({ email: req.body.email });

  // 이미 존재하는 이메일이라면 success: false
  if(checkUser){
    console.log('이미 존재하는 이메일')
    res.send({ success: false })
    return;
  }

  const user = new User();
  const { name, email, password } = req.body;

  user.name = name;
  user.email = email;
  user.password = password;

  user.save((err) => {
    if(err){
      return res.status(400).send(err);
    } else {
      return res.status(201).send({
        success: true
      })
    }
  })
})

module.exports = router;

 

반응형