[Node+React] 로그인 구현 (server)
1. NODE JS 와 EXPRESS JS 다운로드 하기
node.js ? 자바스크립트를 서버사이드에서도 사용이 가능하게 해준다.
express.js? node를 쉽게 이용할 수 있게 해주는 프레임 워크이다.
node.js 설치
1. node -v //버전확인
2. //노드 사이트에서 다운로드 받기
3. npm init //프로젝트 폴더를 만들고 안에다가 입력. 이는 package를 만드는 역할
4. 폴더에 들어가서 index.js 만들기
5. npm install express --save //express설치
2. 몽고 DB 연결
mongoose는 몽고db를 편하게 쓸 수 있는 툴이다.
설치하기
npm install mongoose --save
const express = require("express");
const app = express();
const port = 5000;
const mongoose = require("mongoose");
mongoose
.connect('몽구스에서 복사한 url 넣기', { useNewUrlParser: true, useUnifiedTopology: true })
.then(() => console.log("MongoDB Connected"))
.catch((err) => console.log(err));
//app.use("/api", test);
app.get("/api/hello", (req, res) => {
res.send("안녕하세요~");
});
app.listen(port, () => console.log(`${port}`));
3. MongoDB Model & Schema
model ? 스키마를 감싸주는 역할
schema? 상품이나 사람의 정보 하나하나를 지정해 줄 수 있는 것
파일 생성하기 : models / User.js
const mongoose = require("mongoose"); //몽구스 모듈 가져오기
const bcrypt = require("bcrypt");
const saltRounds = 10;
const jwt = require("jsonwebtoken");
const userSchema = mongoose.Schema({ //스키마생성
name: {
type: String,
maxlength: 50,
},
email: {
type: String,
trim: true, //빈칸 제거
unique: 1,
},
password: {
type: String,
maxlength: 100,
},
lastname: {
type: String,
maxlength: 50,
},
role: {
type: Number,
default: 0,
},
image: String,
token: {
type: String,
},
tokenExp: {
//토큰 유효기간
type: Number,
},
});
const User = mongoose.model("User", userSchema); //모델 이름과 스키마 넣기
module.exports = { User }; //다른곳에서도 쓸 수 있게
4. BodyParser & PostMan & 회원 가입 기능
body-parser 설치
npm install body-parser --save
client 에서 request가 없으니 postman을 이용해서 request 전송
구글 검색 후 postman 설치
index.js 회원가입 코드 작성
const express = require("express");
const app = express();
const port = 5000;
const { User } = require("./models/User");
const bodyParser = require("body-parser");
const mongoose = require("mongoose");
//클라이언트에서 오는 정보를 서버에서 분석해서 가져올 수 있다.
app.use(bodyParser.urlencoded({ extended: true }));
//json 타입으로 된 것을 분석해서 가져올 수 있다.
app.use(bodyParser.json());
mongoose
.connect('몽구스에서 복사한 url 넣기', { useNewUrlParser: true, useUnifiedTopology: true })
.then(() => console.log("MongoDB Connected"))
.catch((err) => console.log(err));
app.get("/api/hello", (req, res) => {
res.send("안녕하세요~");
});
app.post("/api/users/register", (req, res) => {
//회원 가입 할 때 필요한 정보를 client 에서 가져오면
//그것을 데이터베이스에 넣어준다.
const user = new User(req.body);
user.save((err, userInfo) => {
if (err) return res.json({ success: false, err });
return res.status(200).json({
success: true,
});
});
});
const port = 5000;
app.listen(port, () => console.log(`${port}`));
- 만들어 둔 user 모델을 가져온다.
- bodyParser 을 옵션에 준다.
- bodyParser 을 이용하여 클라이언트에서 전달받은 정보를 req.body로 받는다.
- save() 는 몽고db에서 오는 메소드로 , 정보들이 모델에 저장된다.
- 에러가 발생하면 json 형식으로 전달하고 에러메세지도 전달한다.
- 성공하면(status(200)은 성공했다는 의미) 저장한 userInfo를 json형식으로 성공했다는 정보를 전달한다.
5. Nodemon 설치
Nodemon? 소스를 변경할 때 그걸 감지해서 자동으로 서버를 재 시작해주는 툴
npm install nodemon --save-dev
시작할 때 nodemon으로 시작하기 위해 script를 하나 더 만들어 준다.
pakage.json
{
"scripts": {
"backend": "cd server && nodemon server",
"client": "cd to-do-list_client && yarn start"
},
"dependencies": {
"concurrently": "^7.0.0",
"express": "^4.17.2"
}
}
npm run backend로 하면 서버가 켜짐
6. 비밀 설정 정보 관리
몽고db를 연결 할 때, 비밀번호가 유출되지 않게 보관하는것이 중요하다.
우리는 개발 할 때 두가지 환경이 있기 때문에 분기처리를 해줘야 한다.
- Local 환경 -
- Deploy한 후 -
파일 생성 : config/dev.js 생성
module.exports = {
mongoURI:
"몽고 db url을 여기다 넣기",
};
파일 생성 : config/prod.js 생성
module.exports = {
mongoURI: process.env.MONGO_URI,
};
파일 생성 : config/key.js 생성
if (process.env.NODE_ENV === "production") { //deploy시 production이라고 나옴
module.exports = require("./prod");//deploy
} else {
module.exports = require("./dev");//로컬
}
따라서 key.js를 이용해서 index.js에 다시 url을 적용시켜줘야 한다.
const express = require("express");
const app = express();
const port = 5000;
const { User } = require("./models/User");
const bodyParser = require("body-parser");
const mongoose = require("mongoose");
const config = require("./config/key");
//클라이언트에서 오는 정보를 서버에서 분석해서 가져올 수 있다.
app.use(bodyParser.urlencoded({ extended: true }));
//json 타입으로 된 것을 분석해서 가져올 수 있다.
app.use(bodyParser.json());
mongoose
.connect(config.mongoURI, { useNewUrlParser: true, useUnifiedTopology: true })
.then(() => console.log("MongoDB Connected"))
.catch((err) => console.log(err));
app.get("/api/hello", (req, res) => {
res.send("안녕하세요~");
});
app.post("/api/users/register", (req, res) => {
//회원 가입 할 때 필요한 정보를 client 에서 가져오면
//그것을 데이터베이스에 넣어준다.
const user = new User(req.body);
user.save((err, userInfo) => {
if (err) return res.json({ success: false, err });
return res.status(200).json({
success: true,
});
});
});
const port = 5000;
app.listen(port, () => console.log(`${port}`));
7. Bcrypt로 비밀번호 암호화 하기
현재 데이터 베이스에 저장된 비밀번호는 안전하지 않다. 따라서 Bcrypt를 이용하여 비밀번호를 암호화 해준다음 저장해야한다.
Bcrypt 설치하기
npm install bcrypt --save
암호화 하는 순서 :
먼저 Register Route로 가기 (회원가입)
유저 정보들을 데이터베이스에 저장하기 전에(save 전) 암호화할 타이밍
models.User.js 코드 수정하기
const mongoose = require("mongoose");
const bcrypt = require("bcrypt");
const saltRounds = 10;
const userSchema = mongoose.Schema({
name: {
type: String,
maxlength: 50,
},
email: {
type: String,
trim: true,
unique: 1,
},
password: {
type: String,
maxlength: 100,
},
lastname: {
type: String,
maxlength: 50,
},
role: {
type: Number,
default: 0,
},
image: String,
token: {
type: String,
},
tokenExp: {
//토큰 유효기간
type: Number,
},
});
//user 모델에 정보를 저장하기 전에 함수 수행
userSchema.pre("save", function (next) {
var user = this; //순수하게 저장된 정보들
//password를 변경시킬 때만 암호화를 해준다!
if (user.isModified("password")) {
//비밀번호를 암호화 시킨다.
bcrypt.genSalt(saltRounds, function (err, salt) {
if (err) return next(err);
//myPlaintextPassword = user.password : 실제 비밀번호
//hash : 암호화된 비밀번호
bcrypt.hash(user.password, salt, function (err, hash) {
if (err) return next(err);
user.password = hash;
next();
});
});
} else {
//다른것을 바꿀때는 그냥 next
next();
}
});
const User = mongoose.model("User", userSchema);
module.exports = { User };
- pre 함수는 user 모델에 정보를 저장하기 전에 함수 수행
- next 파라미터는 끝나면 next ()로 index.js로 보내버림
- bcrypt 가져오기
- salt(몇글자인지를 의미) 를 이용해서 비밀번호를 암호화해야한다. 그러기 위해서는 salt를 생성한다.
- const saltRounds = 10; 의미는 10자리인 salt를 만들어서 비밀번호를 암호화 한다는 것을 의미
- var user = this는 순수하게 저장된 정보들을 의미한다.
- genSalt로 salt를 만들고 이때 saltRounds 가 필요하기 때문에 넣는다. 에러가 나면 바로 index.js로 보내고 제대로 생성이 되면, hash()함수에 순수하게 저장된 비밀번호와 생성된 salt 를 넣어주고 콜백함수로 hash를 넣어준다. 이때 hash는 암호화된 비밀번호이다. 만약 에러가 들어가면 next로 에러를 보내고, 저장이 잘 되면 저장된 비밀번호를 hash(암호화된 값)으로 바꿔준다. 그리고 완성 후 next()를 넣어준다.
- 사용자가 자신의 정보를 변경하고 저장할 때 마다 비밀번호가 바뀌게 된다. 따라서 비밀번호를 바꿀때만 암호화 해야 하기 때문에 , if (user.isModified("password")) 로 조건을 달아준다.
- 만약 다른것을 바꿀 때는 그냥 next()를 해준다.
8. 로그인 기능 with Bcrypt (1)
index.js
const express = require("express");
const app = express();
const port = 5000;
const { User } = require("./models/User");
const bodyParser = require("body-parser");
const mongoose = require("mongoose");
const config = require("./config/key");
//클라이언트에서 오는 정보를 서버에서 분석해서 가져올 수 있다.
app.use(bodyParser.urlencoded({ extended: true }));
//json 타입으로 된 것을 분석해서 가져올 수 있다.
app.use(bodyParser.json());
mongoose
.connect(config.mongoURI, { useNewUrlParser: true, useUnifiedTopology: true })
.then(() => console.log("MongoDB Connected"))
.catch((err) => console.log(err));
app.get("/api/hello", (req, res) => {
res.send("안녕하세요~");
});
app.post("/api/users/register", (req, res) => {
//회원 가입 할 때 필요한 정보를 client 에서 가져오면
//그것을 데이터베이스에 넣어준다.
const user = new User(req.body);
app.post("/api/users/login", (req, res) => {
//요청된 이메일을 데이터베이스에서 있는지 찾는다
User.findOne({ email: req.body.email }, (err, 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.save((err, userInfo) => {
if (err) return res.json({ success: false, err });
return res.status(200).json({
success: true,
});
});
});
const port = 5000;
app.listen(port, () => console.log(`${port}`));
- 요청된 이메일을 데이터베이스에서 있는지 찾는다. 몽고db 메소드인 findOne으로 이메일을 찾는다. 만약 userInfo가 없다면 json 데이터로 false전달
- 요청된 이메일이 데이터 베이스에 있다면 맞는 비밀번호가 있는지 확인한다. 이때 comparePassword 함수를 만들어서 비밀번호를 비교해준다. 맞는 정보를 isMatch로 가져온다. 메소스는 models/User.js에서 가져온다
models/User.js
const mongoose = require("mongoose");
const bcrypt = require("bcrypt");
const saltRounds = 10;
const jwt = require("jsonwebtoken");
const userSchema = mongoose.Schema({
name: {
type: String,
maxlength: 50,
},
email: {
type: String,
trim: true,
unique: 1,
},
password: {
type: String,
maxlength: 100,
},
lastname: {
type: String,
maxlength: 50,
},
role: {
type: Number,
default: 0,
},
image: String,
token: {
type: String,
},
tokenExp: {
//토큰 유효기간
type: Number,
},
});
//user 모델에 정보를 저장하기 전에 함수 수행
userSchema.pre("save", function (next) {
var user = this;
//password를 변경시킬 때만 암호화를 해준다!
if (user.isModified("password")) {
//비밀번호를 암호화 시킨다.
bcrypt.genSalt(saltRounds, function (err, salt) {
if (err) return next(err);
//myPlaintextPassword = user.password : 실제 비밀번호
//hash : 암호화된 비밀번호
bcrypt.hash(user.password, salt, function (err, hash) {
if (err) return next(err);
user.password = hash;
next();
});
});
} else {
//다른것을 바꿀때는 그냥 next
next();
}
});
userSchema.methods.comparePassword = function (plainPassword, cb) {
//plainPassword 1234567 암호화된 비밀번호 :
bcrypt.compare(plainPassword, this.password, function (err, isMatch) {
if (err) return cb(err);
cb(null, isMatch);
});
};
const User = mongoose.model("User", userSchema);
module.exports = { User };
- userSchema.methods.comparePassword 를 가져오고 plainPassword(=순수한 비밀번호) 와 콜백 function을 준다. 이때 plainPassword를 암호화 해서 암호화된 비밀번호와 맞는지 체크를 해야한다.
- bcrypt.compare를 이용하여 plainPassword와 암호화된 password를 넣어준다. 그리고 콜백함수를 줘서 만약 에러면 return cb(err), 비밀번호가 같으면 cb(null,isMatch)를 준다. 이때 isMatch는 true가 된다.
- 다시 index.js로 와서 isMatch가 없으면 비밀번호가 틀린 것으로 json으로 false를 전달하고, 비밀번호가 맞다면 토큰을 생성해야한다. 이때는 generateToken함수를 만든다.
9. 로그인 기능 with Bcrypt (2)
비밀번호가 맞다면 토큰 생성하기
토큰 생성을 위해서는 JSONWEBTOKEN 라이브러리 설치
npm install jsonwebtoken --save
jsonwebtoken import하고,
sign 메소드를 이용해서 합쳐주면 토큰이 생성된다.
User.js에서 generateToken메소드 정의
const jwt = require("jsonwebtoken");
userSchema.methods.generateToken = function (cb) {
var user = this;
//jsonwebtoken을 이용해서 token 을 생성하기
var token = jwt.sign(user._id.toHexString(), "secreatToken");
user.token = token;
user.save(function (err, user) {
if (err) return cb(err);
cb(null, user);
});
};
jwt.sign에 user의 아이디와 "secreatToken"을 넣어주면
user._id + "secreatToken" = token이 만들어진다.
나중에 토큰을 해석할 때, secreatToken을 넣으면 user._id가 나온다.
user.token을 넣어주고 user을 저장한다.
index.js
//비밀번호가 맞다면 token 생성
user.generateToken((err, user) => {
if (err) return res.status(400).send(err);
});
만약에 에러가 있으면 (status(400)) 에러메세지 전달해주고,
토큰을 쿠키에 저장해야한다. 이때 라이브러리를 설치한다.
npm install cookie-parser --save
index.js
cookie-parser을 가져오기
const cookieParser = require("cookie-parser");
app.use(cookieParser());
//비밀번호가 맞다면 token 생성
user.generateToken((err, user) => {
if (err) return res.status(400).send(err);
//token을 저장한다. 어디에? 쿠키 , 로컬스토리지, 세션이 있다.
//여기선 쿠키로!
res
.cookie("x_auth", user.token)
.status(200)
.json({ loginSuccess: true, userId: user._id });
}
가져온 토큰을 "x_auth” 이름으로 지정해서 cookie에 저장하기
성공하면 json으로 데이터를 전달한다.
10. Auth 기능 만들기
페이지 이동 때마다 로그인이 되어있는지 안되어 있는지 관리자 유저인지 등을 체크하는 기능
쿠키에 저장된 토큰을 서버에서 가져와 복호화한다.
복호화하면 userID가 나오는데 이를 이용해 데이터베이스에 유저를 찾고, 쿠키에서 받아온 token이 유저도 갖고 있는지 확인한다.
쿠키가 일치하지 않으면
middleware/auth.js 만들기
const { User } = require("../models/User");
let auth = (req, res, next) => {
//인증 처리 하는 곳
//클라이언트 쿠키에서 토큰을 가져옴 (cookie parser이용)
let token = req.cookies.x_auth;
//토큰을 복호화 한 후 user을 찾는다.
User.findByToken(token, (err, user) => {
if (err) throw err;
if (!user) return res.json({ isAuth: false, err: true });
req.token = token;
req.user = user;
next();
});
//user가 있으면 인증 okay
//유저가 없으면 인증 no!
};
module.exports = { auth };
User.js
userSchema.statics.findByToken = function (token, cb) {
var user = this;
//user._id + 'secretToken' = token
//토큰을 decode 한다.
jwt.verify(token, "secretToken", function (err, decode) {
//유저 아이디를 이용해서 유저를 찾은 다음에
//클라이언트에서 가져온 토큰과 db에 보관된 토큰이 일치하는지 확인
user.findOne({ " _id": decode, token: token }, function (err, user) {
if (err) return cb(err);
cb(null, user);
});
});
};
index.js
//auth는 미들웨어
//미들웨어 : 리퀘스트 받고 콜백전 중간에서 해주는것
app.get("/api/users/auth", auth, (req, res) => {
//여기까지 미들웨어를 통과해 왔다는 이야기를 auth가 true라는 말
res.status(200).json({
_id: req.user._id,
//role이 0이면 일반유저, 0이 아니면 관리자로 표현
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,
});
});
11. 로그아웃 기능
로그아운 route만들고 로그아웃 하려는 유저를 데이터베이스에서 찾아, 그 유저의 토큰을 지워준다.
index.js
app.get("/api/users/logout", auth, (req, res) => {
User.findOneAndUpdate({ _id: req.user._id }, { token: "" }, (err, user) => {
if (err) return res.json({ success: false, err });
return res.status(200).send({
success: true,
});
});
});
findOneAndUpdate 는 유저를 찾아서 업데이트를 시켜줌
유저 아이디를 찾아 토큰을 지워준다. 그리고 콜백function을 해준다.
<전체코드>
index.js
const express = require("express");
const app = express();
const test = require("./Router/test");
const mongoose = require("mongoose");
const bodyParser = require("body-parser");
const { User } = require("./models/User");
const config = require("./config/key");
const cookieParser = require("cookie-parser");
const { auth } = require("./middleware/auth");
//클라이언트에서 오는 정보를 서버에서 분석해서 가져올 수 있다.
app.use(bodyParser.urlencoded({ extended: true }));
//json 타입으로 된 것을 분석해서 가져올 수 있다.
app.use(bodyParser.json());
app.use(cookieParser());
mongoose
.connect(config.mongoURI, { useNewUrlParser: true, useUnifiedTopology: true })
.then(() => console.log("MongoDB Connected"))
.catch((err) => console.log(err));
//app.use("/api", test);
app.get("/api/hello", (req, res) => {
res.send("안녕하세요~");
});
app.post("/api/users/register", (req, res) => {
//회원 가입 할 때 필요한 정보를 client 에서 가져오면 그것을 데이터베이스에 넣어준다.
const user = new User(req.body);
user.save((err, userInfo) => {
if (err) return res.json({ success: false, err });
return res.status(200).json({
success: true,
});
});
});
app.post("/api/users/login", (req, res) => {
//요청된 이메일을 데이터베이스에서 있는지 찾는다
User.findOne({ email: req.body.email }, (err, user) => {
if (!user) {
return res.json({
loginSuccess: false,
message: "제공된 이메일에 해당하는 유저가 없습니다.",
});
}
//요청된 이메일이 데이터 베이스에 있다면 비밀번호 인지 확인
user.comparePassword(req.body.password, (err, isMatch) => {
if (!isMatch)
return res.json({
loginSuccess: false,
message: "비밀번호가 틀렸습니다.",
});
//비밀번호가 맞다면 token 생성
user.generateToken((err, user) => {
if (err) return res.status(400).send(err);
//token을 저장한다. 어디에? 쿠키 , 로컬스토리지, 세션 - > 나는 쿠키에 저장
res
.cookie("x_auth", user.token)
.status(200)
.json({ loginSuccess: true, userId: user._id });
});
});
});
});
//auth는 미들웨어
//미들웨어 : 리퀘스트 받고 콜백전 중간에서 해주는것
app.get("/api/users/auth", auth, (req, res) => {
//여기까지 미들웨어를 통과해 왔다는 이야기를 auth가 true라는 말
res.status(200).json({
_id: req.user._id,
//role이 0이면 일반유저, 0이 아니면 관리자로 표현
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 }, { token: "" }, (err, user) => {
if (err) return res.json({ success: false, err });
return res.status(200).send({
success: true,
});
});
});
const port = 5000;
app.listen(port, () => console.log(`${port}`));
models/User.js
const mongoose = require("mongoose");
const bcrypt = require("bcrypt");
const saltRounds = 10;
const jwt = require("jsonwebtoken");
const userSchema = mongoose.Schema({
name: {
type: String,
maxlength: 50,
},
email: {
type: String,
trim: true,
unique: 1,
},
password: {
type: String,
maxlength: 100,
},
lastname: {
type: String,
maxlength: 50,
},
role: {
type: Number,
default: 0,
},
image: String,
token: {
type: String,
},
tokenExp: {
//토큰 유효기간
type: Number,
},
});
//user 모델에 정보를 저장하기 전에 함수 수행
userSchema.pre("save", function (next) {
var user = this;
//password를 변경시킬 때만 암호화를 해준다!
if (user.isModified("password")) {
//비밀번호를 암호화 시킨다.
bcrypt.genSalt(saltRounds, function (err, salt) {
if (err) return next(err);
//myPlaintextPassword = user.password : 실제 비밀번호
//hash : 암호화된 비밀번호
bcrypt.hash(user.password, salt, function (err, hash) {
if (err) return next(err);
user.password = hash;
next();
});
});
} else {
//다른것을 바꿀때는 그냥 next
next();
}
});
userSchema.methods.comparePassword = function (plainPassword, cb) {
//plainPassword 1234567 암호화된 비밀번호 :
bcrypt.compare(plainPassword, this.password, function (err, isMatch) {
if (err) return cb(err);
cb(null, isMatch);
});
};
userSchema.methods.generateToken = function (cb) {
var user = this;
//jsonwebtoken을 이용해서 token 을 생성하기
var token = jwt.sign(user._id.toHexString(), "secreatToken");
user.token = token;
user.save(function (err, user) {
if (err) return cb(err);
cb(null, user);
});
};
userSchema.statics.findByToken = function (token, cb) {
var user = this;
//user._id + '' = token
//토큰을 decode 한다.
jwt.verify(token, "secretToken", function (err, decode) {
//유저 아이디를 이용해서 유저를 찾은 다음에
//클라이언트에서 가져온 토큰과 db에 보관된 토큰이 일치하는지 확인
user.findOne({ " _id": decode, token: token }, function (err, user) {
if (err) return cb(err);
cb(null, user);
});
});
};
const User = mongoose.model("User", userSchema);
module.exports = { User };
middleware/auth.js
const { User } = require("../models/User");
let auth = (req, res, next) => {
//인증 처리 하는 곳
//클라이언트 쿠키에서 토큰을 가져옴 (cookie parser이용)
let token = req.cookies.x_auth;
//토큰을 복호화 한 후 user을 찾는다.
User.findByToken(token, (err, user) => {
if (err) throw err;
if (!user) return res.json({ isAuth: false, err: true });
req.token = token;
req.user = user;
next();
});
//user가 있으면 인증 okay
//유저가 없으면 인증 no!
};
module.exports = { auth };
const { User } = require("../models/User");
let auth = (req, res, next) => {
//인증 처리 하는 곳
//클라이언트 쿠키에서 토큰을 가져옴 (cookie parser이용)
let token = req.cookies.x_auth;
//토큰을 복호화 한 후 user을 찾는다.
User.findByToken(token, (err, user) => {
if (err) throw err;
if (!user) return res.json({ isAuth: false, err: true });
req.token = token;
req.user = user;
next();
});
//user가 있으면 인증 okay
//유저가 없으면 인증 no!
};
module.exports = { auth };