passport는 node.js express에서 사용하는 미들웨어이다.
기본적인 인증 시스템 지원과 더불어 구글, 트위터, 페이스북 과 같이 소셜로그인도 passport를 이용해 간편하게 할 수 있다.
passport.js에서 다양한 인증 방법을 stragies로 제공한다.
공식문서를 보면 다양한 stragies가 존재함을 볼 수 있다.
그 중 난 기본적인 사용자 id와 passport를 이용한 인증 방법인 local strategy를 사용해볼거다.
npm install express-session passport passport-local
passport-local : 아이디/비번 방식 회원인증을 사용할 때 쓰는 라이브러리
express-session : 세션을 만드는 걸 도와주는 라이브러리
const session = require('express-session')
const passport = require('passport')
const LocalStrategy = require('passport-local')
app.use(passport.initialize())
app.use(session({
secret: '암호화에 쓸 비번',
resave : false,
saveUninitialized : false
}))
app.use(passport.session())
server.js 상단에 해당 코드를 복붙해주면 passport 라이브러리 셋팅은 끝이 난다.
여기서 session() 안에 있는 설정들이 있다.
secret : 이 안에는 나만의 비번을 넣어주면 된다.
saveUninitialized : 유저가 로그인을 안해도 세션을 저장해둘지 여부이다
resave : 유저가 오ㅛ청을 날릴 때 마다 session 데이터를 다시 갱신할것인지 여부이다.
일단
나 같은 경우 user라는 컬렉션을 하나 만들고
username: test
password : 1234
로 document를 하나 발행했다.
로그인은 간단하다.
로그인 페이지에서 유저가 입력하는 아이디 비번이 DB와 일치하는지를 비교해주고 일치하다면, 세션 document를 하나 만들어 유저에게 주면 된다.
server.js
app.get('/login', async (req, resp) =>{
resp.render('login.ejs')
})
// passport 라이브러리 사용
passport.use(new LocalStrategy(async (id, pw, cb) => {
let result = await db.collection('user').findOne({ username : id})
if (!result) {
return cb(null, false, { message: '아이디 DB에 없음' })
}
if (result.password == pw) {
return cb(null, result)
} else {
return cb(null, false, { message: '비번불일치' });
}
}))
app.post('/login', async (req, resp, next) => {
passport.authenticate('local', (error, user, info) => {
if (error) return resp.status(500).json(error)
if (!user) return resp.status(401).json(info.message)
req.logIn(user, (err) => {
if (err) return next(err)
resp.redirect('/')
})
})(req, resp, next)
})
login.ejs
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>login</title>
<link href="/main.css" rel="stylesheet">
</head>
<body class="grey-bg">
<%- include('nav.ejs' , {age : 20 }) %>
<form class="form-box" action="/login" method="POST">
<h4>로그인</h4>
<input name="username">
<input name="password" type="password">
<button type="submit">전송</button>
</form>
</body>
</html>
여기서 new LocalStrategy를 설명하면, 유저가 아이디/비번을 제출하면 파라미터로
<input name=”username”> <input name=”password”>
태그들이 들어오게 된다.
그래서 일단 유저가 보낸 아이디/비번을 DB에 있던거와 비교하고
일치하면 유저정보를, 일치하지 않으면 false와 메세지를 넣어준다.
그리고 passport.authenticate('local', 콜백함수)(req,resp,next) 이렇게 코드를 작성하면 위에 작성된 검증 코드가 실행이 된다.
검증 성공이나, 실패시에는 콜백함수 안에 첫째 파라미터에는 에러, 둘째 파라미터는 검증 완료된 유저정보, 셋째는 아이디/비번 검증 실패 에러메시지 가 들어오게 된다.
로그인이 되면 서버에서 세션을 만들어서 유저에게 보내주게 된다.
나는 passport 라이브러리가 있기에 쉽게 세션을 만들 수 있다.
passport.serializeUser((user, done) => {
process.nextTick(() => {
done(null, { id: user._id, username: user.username })
})
})
여기서 먼저 process.nexztTick은 Node.js 환경에서 특정 코드를 비동기적으로 처리하고 싶을 때 쓰는 문법이다.
자바스크립트는 원래 동기적으로 처리가 되는데, 어떤 특정 오래 걸리는 코드를 강제로 process.nextTick안에 작성해주면 비동기적으로 처리할 수 있다.
위에서 작성해둔 authenticate() 함수안에 req.logIn()이라는 함수가 실행이 되면 serializeUser가 실행이 되면서 세션을 알아서 만들어주게 된다.
done() 함수의 둘째 파라미터는 세션 document에 기록이 되고, 유효기간은 알아서 기록이 되어주기 때문에
id, username만 적어둬도 된다.
user라는 파라미터는
passport.use(new LocalStrategy(async (id, pw, cb) => {
let result = await db.collection('user').findOne({ username : id})
if (!result) {
return cb(null, false, { message: '아이디 DB에 없음' })
}
if (result.password == pw) {
return cb(null, result)
} else {
return cb(null, false, { message: '비번불일치' });
}
}))
result가 user 파라미터로 넘어오게 되는 것인데 아직까지는 DB 연결을 해놓지 않았기에, 메모리에 세션이 저장이 된다.
serializeUser를 작성해줬으면 deserializeUser 코드 또한 작성을 해야한다.
serializeUser는 로그인 성공시, 세션을 만들어주고 유저 브라우저 쿠키에 세션 정보를 저장해주는 것이고
deserializeUser는 유저가 서버에 쿠키를 제출했을 때, 확인하는 것이다.
// 수정 전
passport.deserializeUser((user, done) => {
process.nextTick(() => {
return done(null, user)
})
})
// 수정 후
passport.deserializeUser((user, done) => {
let result = await db.collection('user').findOne({_id : new ObjectId(user.id) })
delete result.password
process.nextTick(() => {
return done(null, result)
})
})
수정 전 코드에서 세션이 혹여나 오래돼서 최신 유저이름과 다를 수 있기에,
DB를 한번 조회해서 최신 유저 정보를 가져오도록 했다.
이제 개발자 도구를 통해서 application에서 내가 만든 쿠키를 볼 수 있다.
로그인을 했으니깐, 가입을 해보자
server.js
app.get('/register', (req, resp)=>{
req.render('register.ejs')
})
/register 페이지로 register.ejs를 render시켜주고,
server.js
app.post('/register', async (req, resp) => {
await db.collection('user').insertOne({
username : req.body.username,
password : req.body.password
})
응답.redirect('/')
})
register.ejs
<form class="form-box" action="/register" method="POST">
<h4>가입</h4>
<input name="username">
<input name="password" type="password">
<button type="submit">전송</button>
</form>
이렇게 넣게 되면 내 비밀번호가 입력한 그대로 디비에 저장이 되고, 해킹 위험이 크게 존재하게 된다.
그래서 비밀번호를 해싱 시켜서 디비에 저장해두는 것이 좋다.
SHA3-256, SHA3-512, bcrypt, scrypt, argon2 와 같이 수 많은 해싱 알고리즘이 있는데
난 여기서 bcrypt라는 알고리즘을 쓰도록 도와주는 라이브러리를 사용할 것이다.
설치, 셋팅
npm install bcrypt (터미널)
server.js 상단
const bcrypt = require('bcrypt')
셋팅 끝
사용법은 간단하다.
let result = await bcrypt.hash(해싱할문자, 10)
해싱할 비밀번호와 얼마나 해싱을 더 복잡하게 할 것인지 숫자를 파라미터로 전달해주면 된다.
해시 결과를 콘솔로 찍어보면
salt와 hash가 합쳐진 해싱암호가 출력되었다.
그래서 앞서 작성한 코드들을 수정하자면
server.js
app.post('/register', async (req, resp) => {
let hashingPw = await bcrypt.hash(req.body.password, 10)
await db.collection('user').insertOne({
username : req.body.username,
password : hashingPw
})
resp.redirect('/')
})
해싱 암호를 디비로 인서트 해주면 된다.
그리고 로그인을 할 때는 이제 해싱 암호와 유저가 입력한 비밀번호를 서로 비교를 하게 되는데
앞서 passport.use(new LocalStrategy ~~ 코드에서 비밀번호를 체크를 해주는 코드가 있었다.
passport.use(new LocalStrategy(async (id, pw, cb) => {
let result = await db.collection('user').findOne({ username : id})
if (!result) {
return cb(null, false, { message: '아이디 DB에 없음' })
}
if (await bcrypt.compare(pw, result.password)) {
return cb(null, result)
} else {
return cb(null, false, { message: '비번불일치' });
}
}))
bcrypt.compare를 통해서 내가 입력한 비밀번호와 해싱된 비밀번호를 비교해서 user 세션을 보내주도록 하였다.
세션 정보를 디비에 저장하기에 앞서 connect-mongo 라는 라이브러리를 사용하기 위해서 설치를 하자
npm install connect-mongo
const MongoStore = require('connect-mongo')
app.use(session({
resave : false,
saveUninitialized : false,
secret: '세션 암호화 비번~~',
cookie : {maxAge : 1000 * 60},
store: MongoStore.create({
mongoUrl : 'DB접속용 URL~~',
dbName: 'forum',
})
}))
session() 안에 store 라는 항목을 추가하고, MongoStore create 메서드를 통해서 forum이라는 데이터베이스 안에 session이라는 컬렉션을 만들어 세션 정보를 추가해줄것이다.
유효기간이 지나면 알아서 삭제된다.