Passport.js — 인증 미들웨어
이 토픽을 마치면
Passport.js가 왜 필요한지 이해하고, Local Strategy로 아이디/비밀번호 인증을 구현할 수 있으며, serialize/deserialize의 역할을 설명할 수 있습니다.
왜 Passport.js를 쓰는가
이전 토픽에서 세션 기반 인증을 직접 구현했습니다. 작동은 하지만, 인증 방식이 다양해지면 코드가 복잡해집니다.
- 아이디/비밀번호 로그인
- Google 로그인
- GitHub 로그인
- JWT 토큰 인증
각각을 직접 구현하면 라우트마다 인증 로직이 흩어집니다. Passport.js는 이 문제를 해결하는 인증 전문 미들웨어입니다. 500+ 가지의 인증 전략(Strategy)을 플러그인 형태로 제공합니다.
핵심 개념 — Strategy 패턴
Passport.js는 Strategy 패턴을 사용합니다. "어떻게 인증할 것인가"를 Strategy 객체로 분리합니다.
Passport.js (프레임워크)
├── Local Strategy → 아이디/비밀번호
├── Google Strategy → Google OAuth
├── GitHub Strategy → GitHub OAuth
└── JWT Strategy → JSON Web Token각 Strategy는 독립적인 npm 패키지입니다. 필요한 것만 설치해서 씁니다.
설치
npm install passport passport-local express-sessionpassport: 코어 라이브러리passport-local: 아이디/비밀번호 인증 전략express-session: 세션 관리 (Passport가 내부적으로 사용)
Local Strategy 설정
const express = require('express');
const session = require('express-session');
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const app = express();
// 가상의 사용자 DB
const users = [
{ id: 1, username: 'alice', password: 'pass123', displayName: 'Alice Kim' },
{ id: 2, username: 'bob', password: 'pass456', displayName: 'Bob Lee' }
];
// 1. Strategy 등록
passport.use(new LocalStrategy(
(username, password, done) => {
const user = users.find(u => u.username === username);
if (!user) {
return done(null, false, { message: 'User not found' });
}
if (user.password !== password) {
return done(null, false, { message: 'Wrong password' });
}
return done(null, user);
}
));done 콜백의 세 가지 호출 패턴:
| 호출 | 의미 |
|---|---|
done(null, user) | 인증 성공. user 객체 전달 |
done(null, false, {message}) | 인증 실패. 사용자 없음 또는 비밀번호 불일치 |
done(err) | 시스템 에러. DB 연결 실패 등 |
Serialize / Deserialize
세션에 사용자 전체 객체를 저장하면 메모리 낭비입니다. Passport는 세션에 **최소한의 식별 정보(보통 ID)**만 저장하고, 요청마다 그 ID로 사용자를 복원합니다.
// 2. 세션에 저장할 데이터 (로그인 시 1회)
passport.serializeUser((user, done) => {
done(null, user.id); // 세션에 user.id만 저장
});
// 3. 세션에서 사용자 복원 (매 요청마다)
passport.deserializeUser((id, done) => {
const user = users.find(u => u.id === id);
done(null, user); // req.user에 전체 사용자 객체 설정
});로그인 시: user 객체 → serializeUser → 세션에 id=1 저장
매 요청 시: 세션에서 id=1 → deserializeUser → user 객체 복원 → req.userdeserializeUser에서 DB를 조회하면, 세션에 ID만 저장하면서도 매 요청에서 최신 사용자 정보를 사용할 수 있습니다.
미들웨어 연결
// 4. 미들웨어 설정 (순서 중요!)
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(session({
secret: process.env.SESSION_SECRET || 'dev-secret',
resave: false,
saveUninitialized: false
}));
app.use(passport.initialize()); // Passport 초기화
app.use(passport.session()); // 세션과 연결순서가 중요합니다: express-session → passport.initialize() → passport.session(). 순서가 바뀌면 세션이 제대로 작동하지 않습니다.
로그인 / 로그아웃 라우트
// 로그인
app.post('/login',
passport.authenticate('local', {
successRedirect: '/dashboard',
failureRedirect: '/login',
failureMessage: true
})
);
// 또는 JSON 응답을 원할 때
app.post('/api/login', (req, res, next) => {
passport.authenticate('local', (err, user, info) => {
if (err) return next(err);
if (!user) {
return res.status(401).json({ error: info.message });
}
req.logIn(user, (err) => {
if (err) return next(err);
res.json({ message: `Welcome, ${user.displayName}` });
});
})(req, res, next);
});
// 로그아웃
app.post('/logout', (req, res) => {
req.logout((err) => {
if (err) return res.status(500).json({ error: 'Logout failed' });
res.json({ message: 'Logged out' });
});
});passport.authenticate('local')은 앞서 등록한 LocalStrategy를 실행합니다. 성공하면 serializeUser가 호출되고, 세션에 사용자 ID가 저장됩니다.
보호된 라우트
function ensureAuthenticated(req, res, next) {
if (req.isAuthenticated()) {
return next();
}
res.status(401).json({ error: 'Login required' });
}
app.get('/dashboard', ensureAuthenticated, (req, res) => {
res.json({
message: `Hello, ${req.user.displayName}`,
user: { id: req.user.id, username: req.user.username }
});
});req.isAuthenticated()는 Passport가 제공하는 메서드로, 세션에 유효한 사용자가 있으면 true를 반환합니다. req.user는 deserializeUser에서 복원된 사용자 객체입니다.
전체 흐름 정리
1. 서버 시작
passport.use(LocalStrategy) — 인증 방법 등록
passport.serializeUser() — 세션 저장 방법 등록
passport.deserializeUser() — 세션 복원 방법 등록
2. POST /login
passport.authenticate('local') — Strategy 실행
→ 성공: serializeUser → 세션에 ID 저장 → 응답
→ 실패: 401 응답
3. GET /dashboard (인증 필요)
세션 쿠키 확인 → deserializeUser → req.user 설정
ensureAuthenticated → req.isAuthenticated() → true → 라우트 실행
4. POST /logout
req.logout() → 세션에서 사용자 제거 → 응답자주 하는 실수
| 실수 | 결과 | 해결 |
|---|---|---|
app.use(session(...)) 전에 passport.session() 호출 | 세션이 초기화 안 됨 | express-session → passport.initialize() → passport.session() 순서 |
serializeUser에서 전체 user 객체 저장 | 세션 크기 폭발 | user.id만 저장 |
deserializeUser에서 비동기 에러 무시 | 인증 실패 시 서버 크래시 | done(err) 호출 |
Strategy callback에서 done() 호출 누락 | 요청이 무한 대기 | 모든 분기에서 done() 호출 확인 |
핵심 정리
| 개념 | 정리 |
|---|---|
| Passport.js | 인증 전문 미들웨어. Strategy 패턴으로 다양한 인증 방식 지원 |
| Strategy | 인증 방법을 캡슐화한 객체 (Local, Google, JWT 등) |
| serializeUser | 세션에 저장할 최소 정보 결정 (보통 user.id) |
| deserializeUser | 매 요청마다 세션의 ID로 사용자 객체 복원 |
req.user | deserializeUser가 복원한 현재 로그인 사용자 |
req.isAuthenticated() | 로그인 상태 확인 |
Passport.js의 가치는 "인증 로직의 표준화"입니다. Local에서 Google OAuth로 바꿀 때, Strategy만 교체하면 됩니다. serialize/deserialize, 미들웨어 체인, req.user — 나머지 코드는 그대로 유지됩니다.