회원가입 / 로그인&아웃 / 권한 확인 기능 구현

Hyun·2021년 11월 11일
0

노드 리액트

목록 보기
2/7

서버를 만들고 db와 연결 후 client 에서 request를 받아 회원 가입, 로그인, 권한 확인 을 하는 기능을 구현해보았다.

클라이언트는 따로 다루도록 하고 먼저 request를 받아 처리하는 백엔드 부분만을 다루도록 한다.

회원가입 Register

/api/users/register 에서 post 로 가입 정보를 넘겨주면
User 모델을 이용해서 받은 req.body 를 새로운 모델 인스턴스 user 로 생성한다.
이후 save 메서드를 통해 user 를 db 에 저장한다.(저장하기 전에 password는 hashing 암호화한다.)

암호화 과정: bcrypt 의 genSalt 메서드를 통해 salt 를 생성하고, 유저가 정한 saltRounds 를해 hash 메서드로 유저의 password 를 hashing 한다. 따라서 mongodb엔 hashing 된 password 가 들어간다.

로그인 Login & 로그아웃 Logout

(로그인)
/api/users/login 에서 post 로 로그인 정보를 넘겨주면
User 에서 findOne 메서드를 통해 같은 email 을 가진 객체를 가져온다(해당되는 객체가 없으면 false 이다.)
요청된 이메일이 db 에 있으면 비밀번호가 맞는 비밀번호인지 확인한다. (비밀번호 확인 과정) bcrypt 의 compare 메서드를 통해 /login 에서 post 한 password 와 이메일을 통해 찾은 유저의 db 에 존재하는 hashing 된 password 를 비교한다 .
비밀번호가 맞다면 jwt 를 이용하여 그 유저를 위한 토큰을 생성하고 cookie에 저장한다.

토큰 생성 과정: 로그인 정보와 일치하는 db 의 user 객체의 _id 와 secret key 를 이용해 token 을 생성한다. 이후 user 객체의 token 속성에 생성한 token 을 넣어주고 save 한다.

(로그아웃)
해당 유저의 _id 를 db 에서 찾은 후, 유저의 토큰을 db 에서 없애주어 더 이상 인증이 되지 않도록 해준다.

권한 Authentication

/api/users/auth(인증처리를 하는 곳) 에 get 을 하게 되면
미들웨어인 auth 를 통해 클라이언트의 쿠키에서 토큰을 가져오고
토큰을 복호화 한 후, 해당 유저를 db 에서 찾는다.
해당 유저가 존재한다면 req 에서 해당 user 와 token 을 사용할 수 있게 req 에 넣어준다.

복호화 과정: 쿠키에서 가져온 토큰을 jwt 의 verify 메서드를 통해 복호화 한다. 이때, 토큰을 생성할 때 사용했던 secret key 를 사용하여 복호화 할 수 있다. 복호화를 하면 해당 user 의 _id 를 구할 수 있다. 이후 복호화를 통해 구한 _id 와 db에 존재하는 해당 user 의 _id 와 일치하는지 확인한다.(토큰도 추가로 확인)

server/models/User.js (스키마, 모델, 메서드 생성)

const mongoose = require('mongoose');
const bcrypt = require('bcrypt');
const saltRounds = 10;
const jwt = require("jsonwebtoken")//토큰 발행 
//스키마 생성
const userSchema = mongoose.Schema({
  /*데이터를 mongodb에 저장하려면 먼저 구조(스키마)가 있어야 한다.
  스키마는 해당 컬렉션의 문서에 어떤 종류의 값이 들어가는지를 정의한다.
  mongoose 모듈을 불러와 mongoose.Schema 를 통해 스키마 객체를 생성한다.*/
  
  name: {
    type: String,
    maxlength: 50
  },
  lastname: {
    type: String,
    maxlength: 50
  },
  email: {
    type: String,
    trim: true,//email에 공백이 있을 때 없애주는 역할
    unique: 1//똑같은 이메일 못쓰게
  },
  password: {
    type: String,
    minlength: 5
  },
  role: {
    type: Number,
    default: 0
  },
  image: String,
  token: {//유효성관련
    type: String
  },
  tokenExp: {//token의 유효기간
    type: Number
  }
})
//pre는 mongoose 에서 가져온 메서드, save는 저장하기 전에 function을 실행
userSchema.pre('save', function(next) {//next는 바로 이 과정을 pass 함
  //현재 스키마에 들어있는 post된 password를 가져온다
  var user = this;
  //field에서 password가 변환될때만 password를 암호화 해준다.
  if (user.isModified('password')) {
    //bcrypt 패키지의 salt를 이용해서 비밀번호를 암호화 시킨다.
    //genSalt는 salt를 생성한다
    bcrypt.genSalt(saltRounds, function(err, salt) {
      if (err) return next(err)

      bcrypt.hash(user.password, salt, function(err, hash) {
        if (err) return next(err)
        user.password = hash//password를 암호화된 hash 로 바꿔준다
        next()//완료 후 돌아감
      })
    })
  } else {//그냥 나갈 곳을 만들어준다.
    next()
  }
});

userSchema.methods.comparePassword = function(plainPassword, callback) {
  //클라이언트가 입력한 비밀번호와 db의 비밀번호를 비교한다.
  bcrypt.compare(plainPassword, this.password, function(err, isMatch) {
    if (err) return callback(err)//같을때
    callback(null, isMatch)//다를때
  })
}

userSchema.methods.generateToken = function(callback) {
  var user = this;
  //jsonwebtoken을 이용해서 token 생성
  var token = jwt.sign(user._id.toHexString(), 'secretToken');//임의로 두번째(secret key) 지정
  //user._id + 'secretToken' = token (incode)
  //->
  //'secretToken' -> user._id (decode)

  user.token = token;
  user.save(function(err, user) {
    if (err) return callback(err)
    callback(null, user)
  })
}

userSchema.statics.findByToken = function(token, callback) {
  var user = this;

  //토큰을 복호화(decode) 한다
  jwt.verify(token, 'secretToken', function(err, decoded) {
    user.findOne({ "_id": decoded, "token": token }, function(err, user) {

      if (err) return callback(err);
      callback(null, user);
    })
  })
}
const User = mongoose.model('User', userSchema);
module.exports = { User };
//모델 다른 곳에서도 쓸 수 있게 export 해준다.

server/middleware/auth.js (권한 인증 미들웨어)

const { User } = require('../models/User');
//decode 복호화(암호화해제) incode 암호화
let auth = (req, res, next) => {
  //인증 처리를 하는 곳
  //1. 클라이언트 쿠키에서 토큰을 가져온다. cookie parser 이용
  let token = req.cookies.x_auth;//암호화(incode)되있는 상태
  
  //2. 토큰을 복호화 한 후, 해당 유저를 찾는다.
  User.findByToken(token, (err, user) => {
    if(err) throw err;
    if(!user) return res.json({isAuth: false, error: true})

  //req에서 user와 token 을 사용할 수 있게 해준다.
  req.token = token;
  req.user = user;
  next();//미들웨어를 벗어나 계속 갈 수 있게
  })
}

module.exports = { auth };

server/index.js (서버 생성 및 db 연결, 요청 처리)

const express = require("express");
const app = express()
//client 에서 보내는 정보를 분석해서 서버에서 받을 수 있게 해준다.
//bodyParser를 사용하지 않으면 req.body가 undefinded를 default로 받는다.
const bodyParser = require("body-parser")
//모델을 가져온다.
const cookieParser = require("cookie-parser");
const config = require('./config/key');
const { auth } = require('./middleware/auth');
const { User } = require("./models/User");
//application/x-www-form-urlencoded
app.use(bodyParser.urlencoded({ extended: true }));
//application/json
app.use(bodyParser.json());
/*bodyparser는 client 에서 오는 정보를 "분석해서" 가져올 수 있게 한다.
x-www-form-urlencoded 이렇게 된 데이터와
json 형식의 데이터를 분석할 수 있게 하기 위해 윗 문장을 적어준다.*/
app.use(cookieParser());

const mongoose = require('mongoose');
//스키마를 만들고, 해당 스키마에 맞는 모델을 만들어 공통된 조건에 맞게 조회 및 저장이 가능하다.
mongoose.connect(config.mongoURI)//서버와 데이터베이스(mongoDB)를 연결
  .then(() => console.log('MongoDB Connected...'))
  .catch(err => console.log("MongoDB error: ", err));

app.get('/', (req, res) => res.send('hello world'))

app.get('/api/hello', (req,res)=> {
  res.send("Hello World~ ")
})

app.post('/api/users/register', (req, res) => {
  //받은 정보로 모델 생성, json 형식으로 req가 들어있다.
  const user = new User(req.body)
  
  user.save((err, doc) => {
    if (err) return res.json({ success: false, err })
    return res.status(200).json({ success: true })
  })
  //결과적으로 http post 메소드로 백엔드 서버로 유저 정보를 날려주고
  //백엔드 서버에서 save 메소드로 DB에 저장을 해준다
})

app.post('/api/users/login', (req, res) => {
  //요청된 이메일을 데이터베이스에서 찾는다. mongoDB 메서드 이용
  User.findOne({ email: req.body.email }, (err, user) => {
    //요청한 email이 db정보 안에 있을 때 해당 db정보를 담은 객체 user 가 생성된다.
    if (!user) {
      return res.json({
        loginSuccess: false,
        message: "제공된 이메일에 해당하는 유저가 없습니다."
      })
    }
    //요청된 이메일이 데이터 베이스에 있다면 비밀번호가 맞는 비밀번호 인지 확인 
    user.comparePassword(req.body.password, (err, isMatch) => {
      if (!isMatch) //비밀번호가 틀림
        return res.json({ loginSuccess: false, message: "비밀번호가 틀렸습니다" })
      //비밀번호가 맞다면 그 유저를 위한 토큰 생성
      user.generateToken((err, user) => {//token이 들어있는 user 객체
        if (err) return res.status(400).send(err);

        //토큰을 저장한다. 어디에? 쿠키, localStorage 등.. 지금은 쿠키에
        res.cookie("x_auth", user.token)
          .status(200)//성공했다는 표시
          .json({ loginSuccess: true, userId: user._id })
      })
    })
  })
})

//현재의 role => role 0 -> 일반유저, role 0 아니면 관리자
app.get('/api/users/auth', auth/*미들웨어*/, (req, res) => {
  //여기 까지 미들웨어를 통과해 왔다는 얘기는 authentication 이 true 라는 말
  res.status(200).json({
    _id: req.user._id,//auth에서 user 를 req 에 넣었기 때문에 사용가능
    isAdmin: req.user.role === 0 ? false : true,//변경가능
    isAuth: true,
    email: req.user.email,
    name: req.user.name,
    lastname: req.user.lastname,
    role: req.user.role,
    image: req.user.image
  })
})

app.get('/api/users/logout', auth, (req, res)=> {
  //첫번째 인자에 찾을 조건, 두번재 인자에 변경할 것
  User.findOneAndUpdate({_id: req.user._id/*auth에서 req에 넣어줌*/}, 
  {token: ""},
  (err, user)=> {
    if(err) return res.json({success: false, err});
    return res.status(200).send({success:true})
  })
})


const port = 5000
//5000번 포트에서 연결을 청취하고, 연결됬을 시 콜백함수를 실행한다.
app.listen(port, () => console.log(`Example app listening on port ${port}!`))

개발 환경

개발을 할 때 두가지 환경에서 할 수 있다. 하나는 local에서 local development 모드에서 할 수 있고, 나머지 하나는 heroku 나 다른 클라우드 서비스를 이용해서 deploy(배포)를 한 후 production 모드에서 개발을 할 수 있다.

두가지는 따로 나눠서 생각해야 한다.

server/config/key.js (환경에 따른 mongodb 연결방법)

if(process.env.NODE_ENV/*환경변수*/ === 'development')
//process.env.NODE_ENV 는 현재 위치한 모드를 가리킨다.
{//development 모드
  module.exports = require('./prod');
} else {//production 모드(deploy, 배포한 후)))
  module.exports = require('./dev');
}

server/config/dev.js (development 모드일때)

아이디와 비밀번호는 생략하였다.

//development 모드에서 사용할 것

module.exports = {
  mongoURI: 'mongodb+srv://--id--:--password--@nodereact.typ9a.mongodb.net/myFirstDatabase?retryWrites=true&w=majority'
}

server/config/prod.js (production 모드일때)

//production 모드에서 사용할 것
module.exports = {
  mongoURI: process.env.MONGO_URL
}

github - NodeReact

profile
better than yesterday

0개의 댓글