Passport.js OAuth 策略 #
什么是 OAuth? #
OAuth(开放授权)是一个开放标准,允许用户授权第三方应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方应用。
OAuth 2.0 流程 #
text
┌─────────────────────────────────────────────────────────────┐
│ OAuth 2.0 授权流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 用户 ──── 1.点击登录 ────> 你的应用 │
│ │ │ │
│ │ │ 2.重定向到授权页面 │
│ │ ▼ │
│ │ OAuth 服务商 │
│ │ (Google/GitHub) │
│ │ │ │
│ │<──── 3.用户授权 ────────│ │
│ │ │ │
│ │ │ 4.返回授权码 │
│ │ ▼ │
│ │ 你的应用 │
│ │ │ │
│ │ │ 5.用授权码换取令牌 │
│ │ ▼ │
│ │ OAuth 服务商 │
│ │ │ │
│ │ │ 6.返回访问令牌 │
│ │ ▼ │
│ │ 你的应用 │
│ │ │ │
│ │ │ 7.获取用户信息 │
│ │ ▼ │
│ └───────────────────> 登录成功 │
│ │
└─────────────────────────────────────────────────────────────┘
Google OAuth 登录 #
安装依赖 #
bash
npm install passport-google-oauth20
创建 Google OAuth 应用 #
- 访问 Google Cloud Console
- 创建新项目或选择现有项目
- 启用 Google+ API
- 创建 OAuth 2.0 凭据
- 设置授权重定向 URI:
http://localhost:3000/auth/google/callback
配置策略 #
javascript
// config/passport.js
const GoogleStrategy = require('passport-google-oauth20').Strategy;
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: '/auth/google/callback',
scope: ['profile', 'email']
}, async (accessToken, refreshToken, profile, done) => {
try {
// 查找是否已有用户
let user = await User.findOne({ googleId: profile.id });
if (user) {
return done(null, user);
}
// 检查邮箱是否已被注册
user = await User.findOne({ email: profile.emails[0].value });
if (user) {
// 关联 Google ID
user.googleId = profile.id;
await user.save();
return done(null, user);
}
// 创建新用户
user = new User({
googleId: profile.id,
username: profile.displayName,
email: profile.emails[0].value,
avatar: profile.photos[0]?.value,
provider: 'google'
});
await user.save();
done(null, user);
} catch (error) {
done(error, null);
}
}));
路由配置 #
javascript
// routes/auth.js
const passport = require('passport');
// Google 登录
router.get('/google', passport.authenticate('google', {
scope: ['profile', 'email']
}));
// Google 回调
router.get('/google/callback',
passport.authenticate('google', {
failureRedirect: '/auth/login',
failureFlash: true
}),
(req, res) => {
// 登录成功
res.redirect('/dashboard');
}
);
GitHub OAuth 登录 #
安装依赖 #
bash
npm install passport-github2
创建 GitHub OAuth 应用 #
- 访问 GitHub Settings → Developer settings → OAuth Apps
- 点击 “New OAuth App”
- 填写应用信息:
- Application name: 你的应用名称
- Homepage URL:
http://localhost:3000 - Authorization callback URL:
http://localhost:3000/auth/github/callback
- 获取 Client ID 和 Client Secret
配置策略 #
javascript
// config/passport.js
const GitHubStrategy = require('passport-github2').Strategy;
passport.use(new GitHubStrategy({
clientID: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
callbackURL: '/auth/github/callback',
scope: ['user:email']
}, async (accessToken, refreshToken, profile, done) => {
try {
let user = await User.findOne({ githubId: profile.id });
if (user) {
return done(null, user);
}
// 获取邮箱(GitHub 可能不返回公开邮箱)
const email = profile.emails?.[0]?.value ||
profile._json?.email ||
`${profile.id}@github.placeholder`;
user = await User.findOne({ email });
if (user) {
user.githubId = profile.id;
user.githubToken = accessToken;
await user.save();
return done(null, user);
}
user = new User({
githubId: profile.id,
username: profile.username || profile.displayName,
email: email,
avatar: profile.photos?.[0]?.value,
provider: 'github',
githubToken: accessToken
});
await user.save();
done(null, user);
} catch (error) {
done(error, null);
}
}));
路由配置 #
javascript
// routes/auth.js
// GitHub 登录
router.get('/github', passport.authenticate('github', {
scope: ['user:email']
}));
// GitHub 回调
router.get('/github/callback',
passport.authenticate('github', {
failureRedirect: '/auth/login',
failureFlash: true
}),
(req, res) => {
res.redirect('/dashboard');
}
);
Facebook OAuth 登录 #
安装依赖 #
bash
npm install passport-facebook
创建 Facebook 应用 #
- 访问 Facebook Developers
- 创建新应用
- 添加 Facebook 登录产品
- 设置有效 OAuth 重定向 URI:
http://localhost:3000/auth/facebook/callback
配置策略 #
javascript
// config/passport.js
const FacebookStrategy = require('passport-facebook').Strategy;
passport.use(new FacebookStrategy({
clientID: process.env.FACEBOOK_APP_ID,
clientSecret: process.env.FACEBOOK_APP_SECRET,
callbackURL: '/auth/facebook/callback',
profileFields: ['id', 'displayName', 'email', 'photos']
}, async (accessToken, refreshToken, profile, done) => {
try {
let user = await User.findOne({ facebookId: profile.id });
if (user) {
return done(null, user);
}
const email = profile.emails?.[0]?.value;
if (email) {
user = await User.findOne({ email });
if (user) {
user.facebookId = profile.id;
await user.save();
return done(null, user);
}
}
user = new User({
facebookId: profile.id,
username: profile.displayName,
email: email || `${profile.id}@facebook.placeholder`,
avatar: profile.photos?.[0]?.value,
provider: 'facebook'
});
await user.save();
done(null, user);
} catch (error) {
done(error, null);
}
}));
路由配置 #
javascript
// routes/auth.js
// Facebook 登录
router.get('/facebook', passport.authenticate('facebook', {
scope: ['email']
}));
// Facebook 回调
router.get('/facebook/callback',
passport.authenticate('facebook', {
failureRedirect: '/auth/login',
failureFlash: true
}),
(req, res) => {
res.redirect('/dashboard');
}
);
Twitter OAuth 登录 #
安装依赖 #
bash
npm install passport-twitter
创建 Twitter 应用 #
- 访问 Twitter Developer Portal
- 创建项目和应用
- 启用 OAuth 1.0a
- 设置回调 URL:
http://localhost:3000/auth/twitter/callback
配置策略 #
javascript
// config/passport.js
const TwitterStrategy = require('passport-twitter').Strategy;
passport.use(new TwitterStrategy({
consumerKey: process.env.TWITTER_CONSUMER_KEY,
consumerSecret: process.env.TWITTER_CONSUMER_SECRET,
callbackURL: '/auth/twitter/callback',
includeEmail: true
}, async (token, tokenSecret, profile, done) => {
try {
let user = await User.findOne({ twitterId: profile.id });
if (user) {
return done(null, user);
}
const email = profile.emails?.[0]?.value;
if (email) {
user = await User.findOne({ email });
if (user) {
user.twitterId = profile.id;
await user.save();
return done(null, user);
}
}
user = new User({
twitterId: profile.id,
username: profile.username || profile.displayName,
email: email || `${profile.id}@twitter.placeholder`,
avatar: profile.photos?.[0]?.value,
provider: 'twitter'
});
await user.save();
done(null, user);
} catch (error) {
done(error, null);
}
}));
路由配置 #
javascript
// routes/auth.js
// Twitter 登录
router.get('/twitter', passport.authenticate('twitter'));
// Twitter 回调
router.get('/twitter/callback',
passport.authenticate('twitter', {
failureRedirect: '/auth/login',
failureFlash: true
}),
(req, res) => {
res.redirect('/dashboard');
}
);
微信 OAuth 登录 #
安装依赖 #
bash
npm install passport-wechat
配置策略 #
javascript
// config/passport.js
const WechatStrategy = require('passport-wechat').Strategy;
passport.use(new WechatStrategy({
appID: process.env.WECHAT_APP_ID,
appSecret: process.env.WECHAT_APP_SECRET,
callbackURL: '/auth/wechat/callback',
scope: 'snsapi_userinfo',
state: 'STATE'
}, async (accessToken, refreshToken, profile, done) => {
try {
let user = await User.findOne({ wechatId: profile.openid });
if (user) {
return done(null, user);
}
user = new User({
wechatId: profile.openid,
unionId: profile.unionid,
username: profile.nickname,
avatar: profile.headimgurl,
provider: 'wechat'
});
await user.save();
done(null, user);
} catch (error) {
done(error, null);
}
}));
路由配置 #
javascript
// routes/auth.js
// 微信登录
router.get('/wechat', passport.authenticate('wechat'));
// 微信回调
router.get('/wechat/callback',
passport.authenticate('wechat', {
failureRedirect: '/auth/login',
failureFlash: true
}),
(req, res) => {
res.redirect('/dashboard');
}
);
用户模型扩展 #
支持多种登录方式 #
javascript
// models/User.js
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
// 本地登录
username: {
type: String,
unique: true,
sparse: true // 允许为空(OAuth 用户可能没有)
},
email: {
type: String,
required: true,
unique: true
},
password: {
type: String,
select: false
},
// OAuth 提供商
provider: {
type: String,
enum: ['local', 'google', 'github', 'facebook', 'twitter', 'wechat'],
default: 'local'
},
// 第三方 ID
googleId: { type: String, sparse: true, unique: true },
githubId: { type: String, sparse: true, unique: true },
facebookId: { type: String, sparse: true, unique: true },
twitterId: { type: String, sparse: true, unique: true },
wechatId: { type: String, sparse: true, unique: true },
// 第三方 Token
googleToken: String,
githubToken: String,
facebookToken: String,
twitterToken: String,
twitterTokenSecret: String,
wechatToken: String,
// 用户信息
displayName: String,
avatar: String,
// 其他
role: {
type: String,
enum: ['user', 'admin'],
default: 'user'
},
isActive: {
type: Boolean,
default: true
},
lastLogin: Date,
createdAt: {
type: Date,
default: Date.now
}
});
// 静态方法:查找或创建 OAuth 用户
userSchema.statics.findOrCreateOAuthUser = async function(profile, provider) {
const providerIdField = `${provider}Id`;
const query = {};
query[providerIdField] = profile.id;
let user = await this.findOne(query);
if (user) {
return user;
}
// 尝试通过邮箱查找
const email = profile.emails?.[0]?.value;
if (email) {
user = await this.findOne({ email });
if (user) {
user[providerIdField] = profile.id;
await user.save();
return user;
}
}
// 创建新用户
const newUser = new this({
[providerIdField]: profile.id,
email: email || `${profile.id}@${provider}.placeholder`,
username: profile.username || profile.displayName || `user_${profile.id}`,
displayName: profile.displayName,
avatar: profile.photos?.[0]?.value,
provider: provider
});
await newUser.save();
return newUser;
};
module.exports = mongoose.model('User', userSchema);
简化的策略配置 #
javascript
// config/passport.js
const User = require('../models/User');
// Google
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: '/auth/google/callback'
}, async (accessToken, refreshToken, profile, done) => {
try {
const user = await User.findOrCreateOAuthUser(profile, 'google');
done(null, user);
} catch (error) {
done(error, null);
}
}));
// GitHub
passport.use(new GitHubStrategy({
clientID: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
callbackURL: '/auth/github/callback'
}, async (accessToken, refreshToken, profile, done) => {
try {
const user = await User.findOrCreateOAuthUser(profile, 'github');
done(null, user);
} catch (error) {
done(error, null);
}
}));
账户关联 #
关联多个社交账号 #
javascript
// routes/auth.js
// 关联 Google 账号
router.get('/link/google', ensureAuthenticated, passport.authorize('google', {
scope: ['profile', 'email']
}));
router.get('/link/google/callback',
ensureAuthenticated,
passport.authorize('google', {
failureRedirect: '/profile'
}),
async (req, res) => {
// req.account 是 Google 返回的 profile
const user = req.user;
user.googleId = req.account.id;
user.googleToken = req.account.accessToken;
await user.save();
res.redirect('/profile');
}
);
// 取消关联
router.post('/unlink/:provider', ensureAuthenticated, async (req, res) => {
const { provider } = req.params;
const user = req.user;
const providerIdField = `${provider}Id`;
const providerTokenField = `${provider}Token`;
// 确保用户有其他登录方式
if (!user.password && user.provider === provider) {
req.flash('error', '无法取消唯一的登录方式');
return res.redirect('/profile');
}
user[providerIdField] = undefined;
user[providerTokenField] = undefined;
await user.save();
req.flash('success', '已取消关联');
res.redirect('/profile');
});
环境变量配置 #
bash
# .env
# Google
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
# GitHub
GITHUB_CLIENT_ID=your-github-client-id
GITHUB_CLIENT_SECRET=your-github-client-secret
# Facebook
FACEBOOK_APP_ID=your-facebook-app-id
FACEBOOK_APP_SECRET=your-facebook-app-secret
# Twitter
TWITTER_CONSUMER_KEY=your-twitter-consumer-key
TWITTER_CONSUMER_SECRET=your-twitter-consumer-secret
# WeChat
WECHAT_APP_ID=your-wechat-app-id
WECHAT_APP_SECRET=your-wechat-app-secret
完整示例 #
统一登录页面 #
javascript
// routes/auth.js
// 登录页面
router.get('/login', (req, res) => {
res.render('auth/login', {
title: '登录',
error: req.flash('error'),
success: req.flash('success')
});
});
// 登录处理(本地 + OAuth)
router.post('/login', passport.authenticate('local', {
successRedirect: '/dashboard',
failureRedirect: '/auth/login',
failureFlash: true
}));
html
<!-- views/auth/login.ejs -->
<div class="login-container">
<h2>登录</h2>
<!-- 本地登录表单 -->
<form action="/auth/login" method="POST">
<input type="text" name="username" placeholder="用户名" required>
<input type="password" name="password" placeholder="密码" required>
<button type="submit">登录</button>
</form>
<!-- OAuth 登录按钮 -->
<div class="oauth-buttons">
<a href="/auth/google" class="btn btn-google">
<i class="fab fa-google"></i> Google 登录
</a>
<a href="/auth/github" class="btn btn-github">
<i class="fab fa-github"></i> GitHub 登录
</a>
<a href="/auth/facebook" class="btn btn-facebook">
<i class="fab fa-facebook"></i> Facebook 登录
</a>
<a href="/auth/twitter" class="btn btn-twitter">
<i class="fab fa-twitter"></i> Twitter 登录
</a>
</div>
<p>还没有账号?<a href="/auth/register">立即注册</a></p>
</div>
安全注意事项 #
1. 验证回调 URL #
javascript
// 确保 callback URL 与注册的一致
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: process.env.GOOGLE_CALLBACK_URL // 从环境变量读取
}, verifyCallback));
2. 使用 state 参数 #
javascript
// 防止 CSRF 攻击
router.get('/google', (req, res, next) => {
// 生成并存储 state
const state = crypto.randomBytes(16).toString('hex');
req.session.oauthState = state;
passport.authenticate('google', {
scope: ['profile', 'email'],
state: state
})(req, res, next);
});
router.get('/google/callback', (req, res, next) => {
// 验证 state
if (req.query.state !== req.session.oauthState) {
return res.status(400).send('Invalid state');
}
passport.authenticate('google')(req, res, next);
});
3. 安全存储 Token #
javascript
// 加密存储敏感 Token
const crypto = require('crypto');
function encryptToken(token) {
const cipher = crypto.createCipher('aes-256-cbc', process.env.ENCRYPTION_KEY);
let encrypted = cipher.update(token, 'utf8', 'hex');
encrypted += cipher.final('hex');
return encrypted;
}
function decryptToken(encrypted) {
const decipher = crypto.createDecipher('aes-256-cbc', process.env.ENCRYPTION_KEY);
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
下一步 #
现在你已经掌握了 OAuth 策略的使用,接下来学习 会话管理,深入了解 Passport.js 的会话机制!
最后更新:2026-03-28