안녕하세요. 이번 시간에는 Passport.js 패키지를 사용해 회원가입과 로그인을 구현하겠습니다! 이 강좌는 ReactJS 리덕스 강좌와도 이어집니다.
npm install passport passport-local express-session
일단 두 패키지를 설치합니다. passport는 한글로 여권입니다. 이름처럼 자신의 웹사이트에 방문할 때 여권같은 역할을 합니다. 로그인을 쉽게 할 수 있게 도와줍니다. 이름 잘 지었죠? passport-local은 로그인을 직접 구현할 때 사용됩니다. 이외에 passport-google-oauth, passport-facebook, passport-twitter, passport-kakao, passport-naver 같이 SNS 계정을 통해서 바로 로그인할 수 있는 패키지도 있습니다. 제 블로그의 로그인 창을 보면 위의 패키지들을 사용한 것을 알 수 있습니다.
express-session은 passport로 로그인 후 유저 정보를 세션에 저장하기 위해 사용합니다.
이렇게 설치했으니 passport를 사용해보겠습니다. 이게 우리나라에 들어올 수 있게 해주는 여권입니다.
passport.js
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const Users = require('./user');
module.exports = () => {
passport.serializeUser((user, done) => { // Strategy 성공 시 호출됨
done(null, user); // 여기의 user가 deserializeUser의 첫 번째 매개변수로 이동
});
passport.deserializeUser((user, done) => { // 매개변수 user는 serializeUser의 done의 인자 user를 받은 것
done(null, user); // 여기의 user가 req.user가 됨
});
passport.use(new LocalStrategy({ // local 전략을 세움
usernameField: 'id',
passwordField: 'pw',
session: true, // 세션에 저장 여부
passReqToCallback: false,
}, (id, password, done) => {
Users.findOne({ id: id }, (findError, user) => {
if (findError) return done(findError); // 서버 에러 처리
if (!user) return done(null, false, { message: '존재하지 않는 아이디입니다' }); // 임의 에러 처리
return user.comparePassword(password, (passError, isMatch) => {
if (isMatch) {
return done(null, user); // 검증 성공
}
return done(null, false, { message: '비밀번호가 틀렸습니다' }); // 임의 에러 처리
});
});
}));
};
passport는 독특하게 Strategy(전략)이라는 것을 사용합니다. 모든 passport의 플러그인들은 사용하려면 전략을 짜 주어야 합니다. 위의 경우는 local 로그인의 경우의 전략입니다. 일단 serializeUser과 deserializeUser은 잠시 뒤에 설명할 거고요. LocalStrategy 부분을 보시죠.
usernameField와 passwordField는 어떤 폼 필드로부터 아이디와 비밀번호를 전달받을 지 설정하는 옵션입니다. 위의 경우는 body에 데이터가 { id: 'zerocho', pw: 'pswd' }
이렇게 오면 뒤의 콜백 함수의 id 값이 zerocho, password 값이 pswd가 됩니다. session은 말 그대로 세션을 사용할 지 안 할 지를 선택합니다. passReqToCallback은 true로 해두면 뒤의 콜백이
(req, id, password, done) => {};
으로 바뀝니다. id 매개변수 앞에 req 매개변수가 추가되었습니다. req를 통해서 express의 req 객체에 접근할 수 있습니다. 저는 보통 passReqToCallback을 true로 해 두어 req 객체를 passport 인증 시 활용합니다.
아이디와 비밀번호 값이 들어오면 뒤의 콜백 함수가 실행되는데, 내용을 보면 id로 유저를 찾은 후, 유저가 없으면 존재하지 않는 아이디라고 에러를 보냅니다. 유저가 있다면, 이제 비밀번호를 비교해서(비밀번호를 비교하는 부분은 아래 user.js 파일의 comparePassword를 참조하세요), 비교 과정에서 서버 에러가 나면 done(findError)
로 에러를 리턴하고, 비밀번호가 맞을 경우 done(null, user);
로 user 객체를 전송해주고, 틀렸을 경우는 비밀번호가 틀렸다고 done(null, false, { message: '에러메시지' })
로 메시지를 전송합니다.
done이 인자를 세 개나 받아 헷갈릴 수도 있는데 다음과 같습니다. 첫 번째 인자는 DB조회 같은 때 발생하는 서버 에러를 넣는 곳입니다. 무조건 실패하는 경우에만 사용합니다. 두 번째 인자는 성공했을 때 return할 값을 넣는 곳이고요. 성공했으면 당연히 첫 번째 인자는 null이어야겠죠? 에러가 있으면 안 되니까요. 세 번째 인자는 언제 사용하나면, 사용자가 임의로 실패를 만들고 싶을 때 사용합니다. 첫 번째 인자를 사용하는 경우는 서버에서 에러가 났을 때 무조건 실패하는 경우라고 했죠. 세 번째 인자는 위에서 비밀번호가 틀렸다는 에러를 표현하고 싶을 때 사용하면 됩니다. 이것은 서버 에러도 아니고, 사용자가 임의로 만드는 에러이기 때문에, 직접 에러 메시지도 써주는 겁니다.
다음은 많은 분들이 헷갈려하는 serializeUser와 deserializeUser입니다.
serializeUser은 방금 전에 로그인 성공 시 실행되는 done(null, user);
에서 user 객체를 전달받아 세션(정확히는 req.session.passport.user
)에 저장합니다. 세션이 있어야 페이지 이동 시에도 로그인 정보가 유지될 수 있습니다. deserializeUser은 실제 서버로 들어오는 요청마다 세션 정보(serializeUser에서 저장됨)를 실제 DB의 데이터와
비교합니다. 해당하는 유저 정보가 있으면 done의 두 번째 인자를 req.user
에 저장하고, 요청을 처리할 때 유저의 정보를 req.user
를 통해서 넘겨줍니다. 하지만 위의 예시에서는 그냥 아무런 처리과정 없이 (물론 세션에는 저장이 됩니다) 그냥 넘겨주도록 했습니다.
serializeUser에서 done으로 넘겨주는 user가 deserializeUser의 첫 번째 매개변수로 전달되기 때문에 둘의 타입이 항상 일치해야 합니다. 만약 serializeUser에서 id만 넘겨줬다면 deserializeUser의 첫 번째 매개변수도 id를 받아야 하고요. id만 있으면 그 자체로는 req.user
을 만들 수 없기 때문에 User.findById(id)
메소드로 완전한 user 객체를 만들어서 done을 해주면 됩니다. 아래와 같이 주로 사용합니다.
passport.serializeUser((user, done) => { // Strategy 성공 시 호출됨
done(null, user._id); // 여기의 user._id가 req.session.passport.user에 저장
});
passport.deserializeUser((id, done) => { // 매개변수 id는 req.session.passport.user에 저장된 값
User.findById(id, (err, user) => {
done(null, user); // 여기의 user가 req.user가 됨
});
});
이 두 메소드는 꼭 있어야 passport가 작동합니다. 반드시 넣어주셔야 합니다.
user.js
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
id: String,
password: String
});
userSchema.methods.comparePassword = function(inputPassword, cb) {
if (inputPassword === this.password) {
cb(null, true);
} else {
cb('error');
}
};
module.exports = mongoose.model('users', userSchema, 'users');
몽구스를 사용하여 스키마를 만들고 비밀번호 비교 메소드 comparePassword도 만들었습니다. 현재 코드에는 비밀번호를 평문 그대로 저장하고, 비교할 때도 평문끼리 비교하고 있는데, 실제 배포 단계에서는 절대로 이래서는 안 됩니다. 이것에 관한 강좌로 암호화 강좌 가 있습니다. 서버에는 passport를 모듈을 불러와 사용할 수 있도록 코드를 추가해줍니다.
server.js
const express = require('express');
const path = require('path');
const session = require('express-session'); // 세션 설정
const db = require('./db.js');
const route = require('./route.js');
const passport = require('passport'); // 여기와
const passportConfig = require('./passport'); // 여기
const app = express();
app.set('view engine', 'pug');
app.set('views', path.join(__dirname, 'html'));
app.use(session({ secret: '비밀코드', resave: true, saveUninitialized: false })); // 세션 활성화
app.use(passport.initialize()); // passport 구동
app.use(passport.session()); // 세션 연결
db();
passportConfig(); // 이 부분 추가
app.use(express.static(path.join(__dirname, 'html')));
app.use('/', route);
app.listen(8080, () => {
console.log('Express App on port 8080!');
});
가운데 session을 설정하는 session()
, passport.initialize()
, passport.session()
이 부분을 잊지 말고 넣어주셔야 합니다. 마지막으로 route.js에 로그인을 처리하는 라우트를 추가해줍니다.
route.js
const passport = require('passport');
...
router.post('/login', passport.authenticate('local', {
failureRedirect: '/'
}), (req, res) => {
res.redirect('/');
});
이제 /login으로 post 요청을 보내면(로그인 html은 직접 만들어 보세요. action이 '/login'이고 method가 'post', 각각의 input name이 id와 pw인 form을 만들면 됩니다) passport에서 local에 대한 인증 작업을 시작합니다. LocalStrategy를 사용해서요. 실패할 경우와 성공할 경우 어디로 돌려보낼 지 각각 failureRedirect와 res.redirect에 위치를 지정할 수 있습니다.
후, 이제 로컬 로그인이 끝났네요. 다음 시간에는 passport로 facebook 로그인을 만드는 것을 알아볼까요? 로컬 로그인보다는 간단합니다.