2022. 11. 15. 01:58ㆍHow to become a real programmer/Back-End
Bcrypt를 이용해 사용자정보 암호화하기
클라이언트로 부터 제공받은 정보들 중, 민감한 정보들은 암호화를 거쳐 데이터베이스에 저장되어야 한다. 보안 상 문제가 생길 때 사용자의 계정이 해킹당할 수 있다는 우려때문인데 이를 위해서 Node.js에서는 Bcrypt라는 모듈이 있다.
npm install bcrypt --save
해주면 설치가 시작된다.
현재 register route를 확인해보면,
app.post('/register', (req, res) => {
// 회원 가입 시 필요한 정보들을 클라이언트에서 가져오면, 그것들을 DB에 넣어준다
// 그래서 유저 모델을 가져와야
const user = new User(req.body)
user.save((err, userInfo) => {
//저장시 에러가 있다면, 클라이언트에 에러가 있다고 전달(json형식으로)
if(err) return res.json({ success: false, err })
return res.status(200).json({
// 성공 시에는, 성공했다고 전달
success: true
})
})
})
user에 대한 정보가 생성되고, 이후 user.save 이전에 암호화가 이루어져야 할 것이다.
Bcrypt 패키지를 설명하는 npm의 공식 사이트에서는 salt라는 것을 이용해주어야 이후에 hashing을 할 수 있다는 것 같은데, saltRounds는 salt의 글자 수를 나타낸다.
유저 정보 암호화를 위해선 유저 모델(<User.js>)로 이동해서 몇 가지 추가가 필요한데
const bcrypt = require('bcrypt')
const saltRounds = 10
우선 bcrypt와 saltRounds를 가져온다, 이후 스키마 정의 아래부분에
userSchema.pre('save', function( next ){
var user = this; // this는 위 스키마 부분을 가리킴
// 비밀번호를 암호화시킨다
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
// 다음(index.js에서의 User.save())를 실행하는 next()
next()
})
})
})
userSchema.pre('')하면, ''을 실행하기 이전에 실행하는 function을 설정할 수 있는데, bcrypt.genSalt를 통해서 salt를 생성해주고, bcrypt.hash로 user.password와 salt를 넘겨주면 된다. 이후 err가 없는지 확인 후 user.password = hash 해주면 사용자 패스워드가 암호값으로 바뀌고, next()하면 이후 단계인 index.js의 User.save()를 실행하는 곳으로 이동한다.
여기서 하나 생각해보아야 할 점은, 유저 정보가 업데이트 될 때 userSchema가 save되는 과정을 거치므로 다시 password 값이 암호화 된다는 것인데, 비밀번호 변경이 아닌 이메일이나 이름 변경의 경우에도 바뀔 것이기에 이에 대한 예외를 만들어줘야한다. 그래서 조건식
if(user.isModified('password')
로 hash과정을 감싸주면 되겠다. else 인 경우엔 next()로,
<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,
minlength:5
},
lastname: {
type: String,
maxlength: 50
},
role:{ // 관리자, 유저 모드 구별위함
type: Number, // 유저 - 0 / 관리자 - 1 이런 식
default: 0
},
image:{
image: String,
token:{
type: String// 유효성 관리 가능
},
tokenExp:{
type: Number // 토큰 사용기간 지정
}
}
})
userSchema.pre('save', function( next ){
var user = this; // this는 위 스키마 부분을 가리킴
if(user.isModified('password')){
// 비밀번호를 암호화시킨다
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
// 다음(index.js에서의 User.save())를 실행하는 next()
next()
})
})
} else {
next()
}
})
const User = mongoose.model('User', userSchema)
// 유저라는 모델이 userSchema 스키마를 감싼다
module.exports = { User }
// 다른 파일에서도 유저 모델이 사용될 수 있게 export 해준다
이제 postman으로 확인을 해보자
이렇게 값을 보내주고,
Mongodb에서 클러스터의 Collection 탭에 들어가면 저렇게 유저들 정보를 확인 할 수 있다.
아까 우리가 보낸 November14를 name으로 가지는 유저의 password는 123456이었는데 암호화가 되어 저렇게 hash값이 들어간 것을 볼 수 있다.
이제 회원가입과 비밀번호 암호화 까지 했으니 로그인 route를 만들어보자.
<index.js>
app.post('/login', (req, res) => {
// 요청된 이메일을 데이터베이스에서 있는지 찾는다
user.findOne({ email: req.body.email }, (err, user) => {
if(!user){ // 유저가 없다면
return res.json({
loginSucees: false,
message: "제공된 이메일에 해당하는 유저가 없습니다"
})
}
// 요청된 이메일이 데이터 베이스에 있다면 비밀번호가 맞는 비밀번호인지 확인
user.comparePassword(req.body.password, (err, isMatch) => {
//isMatch는 비밀번호가 맞다면 True 아니면 False
if(!isMatch)
return res.json({ loginSuccess: false, message: "비밀번호가 틀렸습니다"})
// 비밀번호가 맞다면 토큰을 생성
user.generateToken((err, user) => {
})
})
})
})
})
우선 요청된 이메일이 데이터 베이스에 있는지 없는지에 따라서 만들어준다.
있는지 찾는것은 User.findOne 함수를 통해서 email값이 우리가 클라이언트로 부터 가져온 정보들이 req에 들어있기에 req.body.email을 통해서 찾아 볼 수 있고 바로 콜백함수를 써서 만약 유저가 없다면 json 형식으로 loginSuccess가 실패했고, 해당유저가 없다는 메세지를 return 하도록 했다.
만약 데이터베이스에 요청된 유저 이메일이 있다면 실행되는 코드인 user.comparePassword는 우리가 지정한 함수인데, 만약 isMatch가 false라면 아까 처럼 실패했다는 json을 return하고 혹은 성공 시 user.generateToken이라는 우리가 임의로 지은 함수를 일단 작성해준다.
user.generateToken? 즉, 토큰을 생성하고 싶다는 것인데, 토큰이 무엇일까. 아래 블로거 분께서 명확하게 설명해주신 것 같아 잠깐 참고해 설명하자면, 서버가 각각의 클라이언트를 누군지 명확하게 구별할 수 있도록 유니크한 정보를 담은 암호화된 데이터라고 한다. 그니까 이전에 인스타그램 클론코딩 글을 봤다면 세션과 좀 유사하다고 볼 수 있다. 로그인한 사용자에 따라서 화면을 다르게 구성해주거나 할 수 있기 때문에. 그렇게만 우선 알아놓자.
* 참고 :
https://defineall.tistory.com/861
[Node.js] 토큰(token) 이란? / 사용법 ( JWT )
토큰이란? 서버가, 각각의 클라이언트를 누군지 정확히 구별할 수 있도록, 유니크한 정보를 담은 암호화 데이터. 유저 구별이 가능해야, 유저의 권한에 맞는 정확한 기능을 응답할 수 있다. ( 사
defineall.tistory.com
User.js에서는 아까 우리가 만들어 둔 함수인 comparePassword의 function이 직접 일어나야(비교되어 isMatch가 True인지 판별해야)하므로 plainPassword를 보내주고 bcrypt.compare 함수를 통해서 plainpassword와 this.password(즉 db내 암호화된 패스워드)를 비교한다. 콜백 함수로 만약 에러가 없다면 에러를 null, isMatch(True)를 콜백해준다
<User.js>
userSchema.methods.comparePassword = function(plainPassword, cb){
// plainPassword 123456
// 암호화된 비밀번호 $2b$10$5E9P.VyfneqH/.eXmTaPQeqIqoPTaXmdM2GLSnEYbxdasW3gUZOr2
// 암호화 체크 방법? plainPassword를 암호화해서 비교
bcrypt.compare(plainPassword, this.password, function(err, isMatch){
if(err) return cb(err), // 에러 있을 시 콜백
cb(null, isMatch) // 에러 없을 시 콜백, 에러는 null(없고), isMatch는 true값이 들어가 있을 것
})
}
이렇게 해두고 토큰 생성 부분을 마저 처리해보자, 토큰을 생성하려면 JSONWEBTOKEN이라는 라이브러리가 필요하기에 다운로드 해주자.
npm install jsonwebtoken --save
명령어를 통해서 설치 가능하다
https://www.npmjs.com/package/jsonwebtoken
jsonwebtoken
JSON Web Token implementation (symmetric and asymmetric). Latest version: 8.5.1, last published: 4 years ago. Start using jsonwebtoken in your project by running `npm i jsonwebtoken`. There are 21437 other projects in the npm registry using jsonwebtoken.
www.npmjs.com
jsonwebtoken을 설명하는 npm 공식 사이트에서는 토큰을 생성하려면 우선
jsonwebtoken을 가져오고,
sign이라는 함수에서 두개 인자를 뭐 같이 보내주면 된다는 것 같다.
우선은 아까 생성한 user.generateToken의 함수를 제대로 완성하지 않았기에, User.js에서 이를 만들어줘야한다.
const jwt = require('jsonwebtoken') // 위 쪽에 추가
---------------------------------------------------------------------------------------
userSchema.methods.generateToken = function(cb){
var user = this;
// jsonwebtoken을 이용해서 토큰을 생성하기
var token = jwt.sign(user._id.toHexString(), 'secretToken')
// secretToken이라는 임의의 문자열을 설정하는데, 여기서 만약 토큰이 생성되면 secretToken에 따라서 user id를 판별한다
// 즉, user._id + 'secretToken' = token 인데, secretToken을 통해 user._id 알 수 있다는 거
user.token = token
user.save(function(err, user){
if(err) return cb(err)
cb(null, user)
})
}
우선 설명하자면 jsonwebtoken을 가져오고, 이후에 userSchema.methods로 메소드를 생성해준다.
토큰 생성은 아까 글을 참조했듯 jwt.sign을 통해 해주는데 첫 번째 파라미터는 유저의 id값, 그리고 두 번째는 임의로 설정한 'secretToken'이다,
여기서 user._id + secretToken = '토큰' 이 된다고 생각하면 된다. 그리고 유저 스키마에 있는 token 부분에 만들어진 token을 할당하고, user.save를 해주면 되는데 콜백함수로 만약 에러가 있다면 err를 갖다주고, 없으면 err는 null, user를 return 한다.
이렇게 generateToken 함수의 실행으로 토큰이 생성되고 유저 정보에 저장된 후 에러가 나지 않아서 user가 잘 return 된다면, 이후 index.js에서 사용되는(아까 만들어 둔)
// 비밀번호가 맞다면 토큰을 생성
user.generateToken((err, user) => {
})
에서 두 번째 파라미터인 user에 아까 return된 user가 담기게 되는 것이다.
이렇게 token이 생성된 user 정보를 받아오면, 해당 토큰을 어디엔가 담아서 관리해야 할 텐데, 여기서 여러가지 선택지가 존재한다. 첫 번째는 쿠키에 담는 것이고, 두 번째는 로컬 스토리지, 세 번째는 세션이 있는데. 여기 강의에서는 쿠키에다 담을 예정인가 보다. 쿠키에 저장하기 위해서는 또 다른 패키지 설치가 필요하기에 쿠키 사용에 필요한 패키지인 cookie parser을 설치해주자.
npm install cookie-parser --save
const cookieParser = require('cookie-parser')
app.use(cookieParser());
이후 위와 같이 사용을 위한 세팅을 해준다.
이후 생성하고 토큰을 저장하는 부분에서
// 비밀번호가 맞다면 토큰을 생성
user.generateToken((err, user) => {
if (err) return res.status(400).send(err);
// 에러코드(400) 일 시 err 보냄
// 토큰을 저장한다. 어디에 ? - 쿠키에 해도, 로컬 스토리지에 해도, 세션에 해도 된다. 여기서는 쿠키
res.cookie("x_auth", user.token)
.status(200)
.json({loginSuccess: true, userId: user._id})
})
res.cookie로 cookie 이름으로는 x_auth를 설정하고 그 안의 값으로 user.token을 할당한다. 그리고 할당이 잘 되었다면 status(200) - 성공, json으로 loginSuccess가 true 이고 userId가 user._id 라는 것을 보내주면 되겠다.
이렇게 하면 로그인 라우트는 다 만들어 진 것 이다 한번 쿠키에 잘 들어가있는지 확인해보도록 하자. postman을 통해서 이전에 register에 데이터를 보내는 것과 동일하게 해주면 된다.
true가 잘 리턴된다.
'How to become a real programmer > Back-End' 카테고리의 다른 글
Kotlin + Ktor + React 프로젝트를 Java + Spring + React 프로젝트로(2 - MVC 패턴 먹이고 게시판 가져오자) (2) | 2025.01.13 |
---|---|
Kotlin + Ktor + React 프로젝트를 Java + Spring + React 프로젝트로(1 - 구조) (1) | 2025.01.12 |
Node.js, React 익히기 - 2 (0) | 2022.11.13 |
Node.js, React 익히기 - 1(시작) (0) | 2022.11.13 |
Python - Django를 이용한 인스타그램 클론 - 8 (0) | 2022.11.10 |