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)
를 호출하여 인증된 사용자 객체를 전달합니다. /login
POST 라우트는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를 마스터하는 것은 안전하고 확장 가능한 사용자 중심 애플리케이션 구축을 향한 기본적인 단계입니다.