Passport.js는 이름과 같이 여권의 역할을 하는 라이브러리로서 일반적으로 인증과정에서 작성해야하는 복잡한 로직을 간단하게 구현하도록 도와준다. 일반적으로 로그인 인증시에 많이 사용되며 로컬로그인, 구글, 페이스북, 카카오, 트위터 등등 여러 소셜로그인에 대한 인증도 지원한다.
Passport.js에서 전략이란 Passport.js에서 제공하는 인증방식을 말한다.
여러가지 전략은 공식문서에서 확인 할 수 있다.
npm install passport passport-local express-session
3개의 패키지를 설치한다.
쿠키와 세션, passport 관련 세팅을 app.js에 설정해주어야 한다.
const cookieParser = require('cookie-parser');
const session = require("express-session");
const cookieParser = require("cookie-parser");
const passport = require("passport");
const passportConfig = require("./passport");
db.sequelize
.sync()
.then(() => {
console.log("db 연결 성공!");
})
.catch(console.error);
passportConfig();
app.use(cookieParser(process.env.COOKIE_SECRET));
app.use(
session({
secret: process.env.COOKIE_SECRET,
resave: false,
saveUninitialized: false,
proxy: process.env.NODE_ENV === "production",
cookie: {
httpOnly: true,
secure: true,
domain: process.env.NODE_ENV === "production" && ".devpost.site",
},
})
);
app.use(passport.initialize()); // passport 초기화(요청 객체에 passport 설정을 심음)
app.use(passport.session()); // req.session 객체에 passport정보를 추가 저장
// passport.session()이 실행되면, 세션쿠키 정보를 바탕으로 해서 passport/index.js의 deserializeUser()가 실행하게 한다.
// 주의 : passport에서 session을 사용하려면 express-session을 통해 세션을 먼저 req.session을 먼저 생성해야 한다.
cookie-parser
express-session 모듈
passport.initialize()
passport.session()
** 주의
req.session 객체는 express-session에서 생성하는 것이므로
express-session 미들웨어를 먼저 열결하고 passport 미들웨어를 연결해야 한다.
routes/user.js
const express = require("express");
const router = express.Router();
const passport = require("passport");
const { User, Post, Comment, Nested_Comment, Image } = require("../models");
// 로컬 로그인
router.post("/local/auth", isNotLoggedIn, (req, res, next) => {
passport.authenticate("local", (err, user, info) => {
// POST /user/auth
// 매개변수 : passport local.js 에서 done으로 전달된 인자들
if (err) {
console.error(err);
next(err);
}
if (info) {
// 클라이언트 쪽 에러
console.log(info);
return res.status(401).send(info.message); // 401 : 허가되지 않음
}
// passport 로그인
// index.js의 serializeUser(user, done) => {...} 실행
return req.login(user, async (loginErr) => {
// passport 로그인에서 에러가 날 경우
if (loginErr) {
console.error(loginErr);
return next(loginErr);
}
const fullUserWithoutPassword = await User.findOne({
where: { id: user.id },
attributes: { exclude: ["password", "createdAt", "updatedAt"] }, // password 제외하고 가져오기
include: [
{ model: Post, attributes: ["id"] },
{ model: Post, as: "Liked", attributes: ["id"] },
{ model: Post, as: "Bookmarked", attributes: ["id"] },
{ model: Comment, attributes: ["id"] },
],
});
return res.status(201).json(fullUserWithoutPassword);
});
})(req, res, next);
});
module.exports = router;
passport/local.js
const passport = require("passport");
const { Strategy: LocalStrategy } = require("passport-local");
const bcrypt = require("bcrypt");
const { User } = require("../models");
// { 가져올 변수, 새로지을 이름 }
module.exports = () => {
passport.use(
new LocalStrategy( // local 전략을 세움
{
usernameField: "email",
passwordField: "password",
session: true, // 세션에 저장 여부
passReqToCallback: false, // 뒤에 콜백함수 형태를 바꿀것인가
},
async (email, password, done) => {
try {
// 존재하는 사용자인지 확인
const user = await User.findOne({
where: { email },
});
if (!user) {
return done(null, false, {
message: "존재하지 않는 사용자입니다.",
});
}
// 탈퇴 여부
if (user.withdraw) {
return done(null, false, { message: "회원 탈퇴한 사용자입니다." });
}
// 비밀번호 일치 여부
const result = await bcrypt.compare(password, user.password);
if (result) {
return done(null, user); // 검증 성공
}
return done(null, false, { message: "비밀번호가 틀렸습니다." });
} catch (error) {
console.error(error);
return done(error);
}
}
)
);
};
usernameField, passwordField : 아이디와 비밀번호를 전달받은 폼 필드를 설정
{email: 'apple', password: 'pswd'}
이렇게 올 경우 뒤에 오는 콜백함수의 첫번째 매개변수(email)값은 'apple', 두번째 매개변수(password)값은 'pswd'가 된다.session : 세션을 사용 할지에 대한 여부
passReqToCallback : true로 설정하면 뒤의 콜백이 (req, id, password, done) => {};
로 바뀜, req는 express의 req 객체를 가져옴
done : 다음 로직으로 넘겨주는 메소드
passport/index.js
const passport = require("passport");
const { User } = require("../models");
const local = require("./local");
const kakao = require("./kakaoStrategy");
const facebook = require("./facebookStrategy");
const google = require("./googleStrategy");
module.exports = () => {
passport.serializeUser((user, done) => {
// Strategy 성공 시 호출됨
// 여기의 user.id가 req.session.passport.user에 저장
done(null, user.id); // 여기의 user.id가 deserializeUser의 첫 번째 매개변수로 이동
});
passport.deserializeUser(async (id, done) => {
try {
// 매개변수 id는 serializeUser의 done의 인자 user.id를 받은 것
// 매개변수 id는 req.session.passport.user에 저장된 값
// id 값으로 사용자인증 (서버로 들어오는 매 요청마다 실행)
const user = await User.findOne({ where: { id } });
done(null, user); // 여기의 user가 req.user가 됨
} catch (error) {
console.error(error);
done(error);
}
});
local();
};
app.use(passport.session())
로 인해 매 요청시 passport.deserializeUser() 메소드를 호출한다.await User.findOne({ where: { id } });
)done(null, user)
로 넘겨주고 req.user에 user 값 저장routes/user.js
// 로그인 정보 유지
router.get("/me", async (req, res, next) => {
// GET /user
try {
if (req.user) {
const fullUserWithoutPassword = await User.findOne({
where: { id: req.user.id },
attributes: { exclude: ["password", "createdAt", "updatedAt"] }, // password 제외하고 가져오기
include: [
{ model: Post, as: "Liked", attributes: ["id"] },
{ model: Post, attributes: ["id"] },
{
model: Comment,
attributes: ["id"],
},
{
model: Nested_Comment,
attributes: ["id"],
},
{ model: Post, as: "Bookmarked", attributes: ["id"] },
],
});
return res.status(201).json(fullUserWithoutPassword);
} else {
res.status(200).json(null);
}
} catch (error) {
console.error(error);
next(error);
}
});