서버를 만들고 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 와 일치하는지 확인한다.(토큰도 추가로 확인)
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 해준다.
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 };
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 모드에서 개발을 할 수 있다.
두가지는 따로 나눠서 생각해야 한다.
if(process.env.NODE_ENV/*환경변수*/ === 'development')
//process.env.NODE_ENV 는 현재 위치한 모드를 가리킨다.
{//development 모드
module.exports = require('./prod');
} else {//production 모드(deploy, 배포한 후)))
module.exports = require('./dev');
}
아이디와 비밀번호는 생략하였다.
//development 모드에서 사용할 것
module.exports = {
mongoURI: 'mongodb+srv://--id--:--password--@nodereact.typ9a.mongodb.net/myFirstDatabase?retryWrites=true&w=majority'
}
//production 모드에서 사용할 것
module.exports = {
mongoURI: process.env.MONGO_URL
}