Passport.js 전략을 활용한 Express 인증 마스터하기
Takashi Yamamoto
Infrastructure Engineer · Leapcell

소개
오늘날의 상호 연결된 디지털 환경에서 거의 모든 웹 애플리케이션은 강력하고 안전한 인증 시스템을 필요로 합니다. 민감한 사용자 데이터를 보호하는 것부터 사용자 경험을 개인화하는 것까지, 신뢰할 수 있는 인증은 현대 애플리케이션 개발의 기반입니다. 그러나 인증 시스템을 처음부터 구축하는 것은 보안상의 함정과 복잡한 구현 세부 사항으로 가득 찬 어려운 작업이 될 수 있습니다. 이때 Passport.js가 등장합니다. Passport.js는 Node.js를 위한 사실상의 인증 미들웨어로, 요청을 인증하는 데 있어 유연하고 모듈화된 접근 방식을 제공합니다. 다양한 인증 전략, 즉 전통적인 사용자 이름/비밀번호 조합부터 최신 토큰 기반 로그인 및 소셜 로그인까지 "플러그인"할 수 있는 간결한 API를 제공합니다. 이 글에서는 Passport.js의 복잡한 부분을 깊이 파고들어 Express.js 애플리케이션 내에서 로컬, JSON Web Token (JWT) 및 일반적인 소셜 로그인 전략을 구현하는 방법을 탐구하여, 프로젝트에서 안전하고 확장 가능한 인증을 구축하는 데 필요한 지식과 도구를 제공합니다.
Passport.js의 핵심 개념
구현에 들어가기 전에 Passport.js의 중심이 되는 핵심 개념에 대한 공통된 이해를 확립해 보겠습니다.
- 전략 (Strategies): Passport.js의 핵심에는 "전략"이 있습니다. 전략은 특정 유형의 인증을 처리하는 자체 포함 모듈입니다. 예를 들어,
passport-local은 사용자 이름/비밀번호 인증을 처리하고,passport-jwt는 JWT 검증을 처리하며,passport-google-oauth20은 Google 로그인을 처리합니다. 각 전략은 특정 옵션으로 구성되며verify함수를 구현합니다. - Verify 함수 (Verify Function): 이것은 모든 Passport.js 전략의 가장 중요한 부분입니다.
verify함수는 전략에서 제공한 자격 증명(예: 로컬 전략의 사용자 이름 및 비밀번호, JWT 전략의 토큰, 소셜 전략의 프로필 정보)을 기반으로 사용자를 찾는 역할을 합니다. 사용자가 발견되고 인증되면 사용자 객체와 함께done콜백을 호출합니다. 인증에 실패하면false또는 오류와 함께done을 호출합니다. - 직렬화/역직렬화 (Serialization/Deserialization): Passport.js는 종종 세션과 통합됩니다. 사용자가 성공적으로 인증되면 Passport.js는 후속 요청에서 사용자를 식별하기 위해 세션에 최소한의 사용자 정보를 저장할 방법을 필요로 합니다. 이는
serializeUser및deserializeUser함수를 통해 처리됩니다.serializeUser는 세션에 저장될 사용자 데이터를 결정하고,deserializeUser는 저장된 데이터를 기반으로 데이터베이스에서 전체 사용자 객체를 검색합니다. 이 과정을 통해 사용자가 자격 증명을 다시 입력할 필요 없이 후속 요청을 인증할 수 있습니다.
인증 전략 구현
이제 Express.js 애플리케이션에 다양한 인증 전략을 통합하는 방법을 살펴보겠습니다.
기본 Express 애플리케이션 설정
먼저 Express 애플리케이션이 설정되었는지 확인합니다.
// app.js const express = require('express'); const session = require('express-session'); const passport = require('passport'); const bcrypt = require('bcryptjs'); // 로컬 전략 비밀번호 해싱용 const app = express(); app.use(express.json()); app.use(express.urlencoded({ extended: true })); // 세션 미들웨어 구성 app.use(session({ secret: 'a very secret key for session', // 강력하고 무작위적인 키로 바꾸세요 resave: false, saveUninitialized: false, cookie: { maxAge: 24 * 60 * 60 * 1000 } // 24시간 })); // Passport.js 초기화 app.use(passport.initialize()); app.use(passport.session()); // 세션을 사용하는 경우에만 필요 // 시연을 위한 더미 사용자 데이터베이스 const users = []; // Passport 직렬화/역직렬화 (세션 기반 인증에 필수) passport.serializeUser((user, done) => { done(null, user.id); }); passport.deserializeUser((id, done) => { const user = users.find(u => u.id === id); done(null, user); }); // 인증되었는지 확인하는 기본 라우트 app.get('/profile', (req, res) => { if (req.isAuthenticated()) { res.send(`Welcome, ${req.user.username}!`); } else { res.status(401).send('Not authenticated'); } }); const PORT = process.env.PORT || 3000; app.listen(PORT, () => { console.log(`Server running on port ${PORT}`); });
1. 로컬 전략 (사용자 이름/비밀번호)
로컬 전략은 데이터베이스에 저장된 사용자 이름과 비밀번호를 기반으로 하는 가장 기본적인 인증 방법입니다.
설치:
npm install passport-local --save
구현:
// app.js (기존 app.js에 추가) const LocalStrategy = require('passport-local').Strategy; // 테스트를 위해 더미 사용자 등록 bcrypt.hash('password123', 10, (err, hash) => { if (err) throw err; users.push({ id: '1', username: 'testuser', password: hash }); }); passport.use(new LocalStrategy( async (username, password, done) => { try { const user = users.find(u => u.username === username); if (!user) { return done(null, false, { message: 'Incorrect username.' }); } const isMatch = await bcrypt.compare(password, user.password); if (!isMatch) { return done(null, false, { message: 'Incorrect password.' }); } return done(null, user); } catch (err) { return done(err); } } )); // 로컬 로그인 라우트 app.post('/login', passport.authenticate('local', { successRedirect: '/profile', failureRedirect: '/login', // 일반적으로 여기에서 로그인 폼을 렌더링합니다. failureFlash: true // connect-flash 미들웨어 필요 })); app.get('/logout', (req, res) => { req.logout((err) => { if (err) { return next(err); } res.redirect('/login'); // 로그아웃 후 로그인 페이지로 리디렉션 }); });
설명:
LocalStrategy를 초기화하고verify함수를 제공합니다.verify함수는username,password,done콜백을 받습니다.verify내부에서users배열에서 사용자를 찾습니다 (실제 앱에서는 데이터베이스 쿼리가 될 것입니다).- 사용자를 찾을 수 없거나 비밀번호가
bcrypt.compare후에도 일치하지 않으면done(null, false, { message: ... })를 호출하여 인증 실패를 알립니다. - 자격 증명이 유효하면
done(null, user)를 호출하여 인증된 사용자 객체를 전달합니다. /loginPOST 라우트는passport.authenticate('local', ...)를 사용하여 전략을 트리거합니다.successRedirect및failureRedirect는 인증 후 사용자가 이동할 위치를 처리합니다.
2. JWT 전략 (토큰 기반)
JWT (JSON Web Token) 인증은 상태 비저장 API에 널리 사용됩니다. 세션 대신 서버는 성공적인 로그인 시 클라이언트에게 토큰을 보내고, 클라이언트는 이후 요청에서 인증을 위해 이 토큰을 포함시킵니다.
설치:
npm install passport-jwt jsonwebtoken --save
구현:
// app.js (기존 app.js에 추가, jwt 종속성도 추가되었는지 확인) const JwtStrategy = require('passport-jwt').Strategy; const ExtractJwt = require('passport-jwt').ExtractJwt; const jwt = require('jsonwebtoken'); const JWT_SECRET = 'your_jwt_secret'; // 강력하고 무작위적인 키로 바꾸세요 // JWT 전략 옵션 const jwtOptions = { jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), secretOrKey: JWT_SECRET }; passport.use(new JwtStrategy(jwtOptions, async (jwt_payload, done) => { try { const user = users.find(u => u.id === jwt_payload.sub); // 'sub'는 사용자 ID에 대한 표준입니다. if (user) { return done(null, user); } else { return done(null, false); } } catch (err) { return done(err, false); } })); // JWT 생성 라우트 (예: 로컬 로그인 성공 후) app.post('/api/login-jwt', async (req, res, next) => { passport.authenticate('local', { session: false }, (err, user, info) => { if (err || !user) { return res.status(401).json({ message: info ? info.message : 'Login failed' }); } req.login(user, { session: false }, (err) => { if (err) res.send(err); const token = jwt.sign({ sub: user.id, username: user.username }, JWT_SECRET, { expiresIn: '1h' }); return res.json({ user, token }); }); })(req, res, next); }); // 보호된 JWT 라우트 app.get('/api/protected', passport.authenticate('jwt', { session: false }), (req, res) => { res.json({ message: `Welcome ${req.user.username} to the protected JWT route!`, user: req.user }); });
설명:
- JWT를 어디서 찾을지(예:
Authorization헤더에 Bearer 토큰으로)와 검증에 사용할 비밀 키를 전략에 알려주는jwtOptions를 정의합니다. - JWT 전략의
verify함수는 디코딩된 JWT 페이로드 (jwt_payload)와done콜백을 받습니다. - 그런 다음
jwt_payload.sub(일반적으로 사용자 ID)를 사용하여 데이터베이스에서 사용자를 찾습니다. - 발견되면
done(null, user)를 호출합니다. 그렇지 않으면done(null, false)를 호출합니다. /api/login-jwt라우트는 먼저 자격 증명을 인증하기 위해 로컬 전략을 사용합니다. 성공하면jsonwebtoken.sign을 사용하여 JWT를 생성하고 클라이언트에 다시 보냅니다./api/protected라우트는session: false로passport.authenticate('jwt', ...)를 사용하여 라우트를 보호합니다. JWT는 상태 비저장이므로 여기에서session: false가 중요합니다.
3. 소셜 로그인 (Google OAuth 2.0 예제)
소셜 로그인을 통해 사용자는 Google, Facebook 또는 GitHub와 같은 플랫폼의 기존 계정을 사용하여 인증할 수 있습니다. 이는 사용자 경험을 개선하고 마찰을 줄입니다.
설치:
npm install passport-google-oauth20 --save
Google API 프로젝트 설정:
- Google Cloud Console로 이동합니다.
- 새 프로젝트를 만듭니다.
APIs & Services > Credentials로 이동합니다.+ Create Credentials를 클릭하고OAuth client ID를 선택합니다.Web application을 선택합니다.Authorized JavaScript origins를 앱 URL(예:http://localhost:3000)로 설정합니다.Authorized redirect URIs를 콜백 URL(예:http://localhost:3000/auth/google/callback)로 설정합니다.client ID와client secret을 얻게 됩니다.
구현:
// app.js (기존 app.js에 추가) const GoogleStrategy = require('passport-google-oauth20').Strategy; const GOOGLE_CLIENT_ID = 'YOUR_GOOGLE_CLIENT_ID'; // 실제 클라이언트 ID로 바꾸세요 const GOOGLE_CLIENT_SECRET = 'YOUR_GOOGLE_CLIENT_SECRET'; // 실제 클라이언트 secret으로 바꾸세요 passport.use(new GoogleStrategy({ clientID: GOOGLE_CLIENT_ID, clientSecret: GOOGLE_CLIENT_SECRET, callbackURL: "/auth/google/callback" // Google Cloud Console 리디렉션 URI와 일치해야 함 }, async (accessToken, refreshToken, profile, done) => { try { // 실제 애플리케이션에서는 profile.id 또는 profile.emails[0].value를 기반으로 // 데이터베이스에 사용자를 저장/찾게 됩니다. let user = users.find(u => u.googleId === profile.id); if (!user) { // 사용자가 존재하지 않으면 새 사용자 생성 const newUser = { id: profile.id, // 단순화를 위해 googleId를 기본 ID로 사용 googleId: profile.id, username: profile.displayName, email: profile.emails && profile.emails[0] ? profile.emails[0].value : null, // 더 많은 프로필 데이터를 저장할 수 있습니다. }; users.push(newUser); user = newUser; } return done(null, user); } catch (err) { return done(err, null); } })); // Google 인증 라우트 app.get('/auth/google', passport.authenticate('google', { scope: ['profile', 'email'] }) ); app.get('/auth/google/callback', passport.authenticate('google', { failureRedirect: '/login' }), (req, res) => { // 인증 성공, 홈으로 리디렉션. res.redirect('/profile'); } );
설명:
clientID,clientSecret,callbackURL로GoogleStrategy를 구성합니다.- Google의
verify함수는accessToken,refreshToken,profile(Google의 사용자 데이터 포함) 및done을 받습니다. verify내부에서users데이터베이스에profile.id(Google ID)를 가진 사용자가 이미 있는지 확인합니다.- 없는 경우 새 사용자를 생성하고 추가합니다.
- 인증되면
done(null, user)를 호출하여 프로세스를 완료합니다. /auth/google라우트는 Google로 리디렉션하여 인증을 요청하며,scope로 권한 범위를 지정합니다.- Google 측에서 성공적으로 인증된 후 Google은 사용자를
/auth/google/callback으로 다시 리디렉션합니다. Passport.js가 이 콜백을 가로채, Google 응답을 처리하고 전략의verify함수를 호출합니다. - 성공적인 검증 후 사용자는
/profile로 리디렉션됩니다.
애플리케이션 시나리오
- 로컬 전략: 사용자가 시스템 내에서 직접 계정을 관리하는 전통적인 웹 애플리케이션에 이상적입니다. 내부 도구나 사용자 데이터에 대한 엄격한 제어가 필요한 애플리케이션에 적합합니다.
- JWT 전략: 상태 비저장 접근 방식이 선호되는 API, 모바일 애플리케이션 및 단일 페이지 애플리케이션 (SPA)에 가장 적합합니다. 서버 측 세션 저장 없이 확장 가능한 인증을 허용합니다.
- 소셜 로그인: 사용자가 기존 계정을 활용하여 가입 편의성을 제공하고 가입 마찰을 줄입니다. 특히 사용자가 기존 계정을 선호하는 소비자 대면 애플리케이션에 유용합니다.
이러한 전략을 결합하는 것이 일반적입니다. 예를 들어, 사용자는 처음에 이메일/비밀번호 (로컬 전략)로 가입한 다음 나중에 Google 계정을 연결할 수 있습니다. 또는 웹 애플리케이션이 일반 사용자에게는 로컬 인증을 사용하지만 모바일 클라이언트를 위해 JWT로 보호되는 API를 노출할 수 있습니다.
결론
Passport.js는 Express 애플리케이션에 인증을 구축하는 JavaScript 개발자에게 필수적인 도구입니다. 모듈화된 전략 기반 아키텍처를 이해함으로써 전통적인 로컬 로그인부터 최신 토큰 기반 및 소셜 인증 흐름에 이르기까지 다양한 인증 메커니즘을 원활하게 통합할 수 있습니다. Passport.js의 유연성과 확장성은 개발자가 특정 애플리케이션 요구 사항에 맞는 안전하고 강력하며 사용자 친화적인 인증 시스템을 만들어, 모든 웹 프로젝트의 견고한 기반을 보장할 수 있도록 지원합니다. Passport.js를 마스터하는 것은 안전하고 확장 가능한 사용자 중심 애플리케이션 구축을 향한 기본적인 단계입니다.