Node.js OAuth2服务器实现求助:第三方登录流程及方法调用困惑
我明白你在搭建OAuth2服务器时的困惑——oauth2orize的文档和示例有时候确实绕,尤其是刚接触授权码流程的时候。咱们一步步拆解,从核心逻辑到代码实现,把整个流程理清楚。
先搞懂核心流程(授权码模式)
你要实现的“类似谷歌登录”的模式,本质是OAuth2的授权码模式,这是第三方登录最安全、最常用的流程,核心步骤是:
- 第三方客户端应用跳转到你的授权页面,携带
client_id、redirect_uri、response_type=code、scope等参数 - 用户在你的页面完成登录,确认是否授权给该客户端
- 你的服务器生成授权码,跳转到客户端指定的
redirect_uri并携带该授权码 - 客户端拿着授权码+自身凭证(
client_id/client_secret)向你的服务器兑换access_token - 客户端用
access_token访问你的受保护API资源
用oauth2orize实现的具体步骤
oauth2orize需要和Passport配合使用,以下是完整的落地步骤:
1. 基础依赖准备
先安装必要的包:
npm install oauth2orize express passport passport-local passport-oauth2-client-password passport-http-bearer mongoose bcryptjs uuid
2. 定义数据库模型
需要三个核心模型来存储OAuth2相关数据:
客户端模型(Client)
代表接入你的OAuth2服务的第三方应用:
const mongoose = require('mongoose'); const ClientSchema = new mongoose.Schema({ clientId: { type: String, required: true, unique: true }, clientSecret: { type: String, required: true }, // 存bcrypt哈希后的密文 redirectUris: { type: [String], required: true }, // 允许的回调地址 scopes: { type: [String], default: ['read'] } // 支持的权限范围 }); module.exports = mongoose.model('Client', ClientSchema);
授权码模型(AuthorizationCode)
存储一次性的授权码,用完即失效:
const AuthorizationCodeSchema = new mongoose.Schema({ code: { type: String, required: true, unique: true }, clientId: { type: String, required: true }, userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, redirectUri: { type: String, required: true }, expires: { type: Date, required: true } // 建议10分钟过期 }); module.exports = mongoose.model('AuthorizationCode', AuthorizationCodeSchema);
AccessToken模型
存储用于访问API的令牌:
const AccessTokenSchema = new mongoose.Schema({ token: { type: String, required: true, unique: true }, clientId: { type: String, required: true }, userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, expires: { type: Date, required: true } // 建议1小时过期 }); module.exports = mongoose.model('AccessToken', AccessTokenSchema);
另外别忘了用户模型(User),用来存储你的平台用户信息,这里就不展开了。
3. 配置Passport认证策略
Passport负责用户登录和客户端凭证验证:
本地用户登录策略
处理用户在授权页面的登录:
const passport = require('passport'); const LocalStrategy = require('passport-local').Strategy; const User = require('./models/User'); const bcrypt = require('bcryptjs'); passport.use(new LocalStrategy( (username, password, done) => { User.findOne({ username }, (err, user) => { if (err) return done(err); if (!user) return done(null, false, { message: '用户名不存在' }); bcrypt.compare(password, user.password, (err, isMatch) => { if (err) return done(err); return isMatch ? done(null, user) : done(null, false, { message: '密码错误' }); }); }); } )); // 序列化/反序列化用户到Session passport.serializeUser((user, done) => done(null, user._id)); passport.deserializeUser((id, done) => User.findById(id, done));
客户端凭证验证策略
验证第三方客户端的client_id和client_secret:
const ClientPasswordStrategy = require('passport-oauth2-client-password').Strategy; const Client = require('./models/Client'); passport.use(new ClientPasswordStrategy( (clientId, clientSecret, done) => { Client.findOne({ clientId }, (err, client) => { if (err) return done(err); if (!client) return done(null, false); bcrypt.compare(clientSecret, client.clientSecret, (err, isMatch) => { if (err) return done(err); return isMatch ? done(null, client) : done(null, false); }); }); } ));
AccessToken验证策略
用来保护你的API,验证请求中的access_token:
const BearerStrategy = require('passport-http-bearer').Strategy; const AccessToken = require('./models/AccessToken'); passport.use(new BearerStrategy( (token, done) => { AccessToken.findOne({ token }, (err, accessToken) => { if (err) return done(err); if (!accessToken || accessToken.expires < new Date()) return done(null, false); User.findById(accessToken.userId, (err, user) => { if (err) return done(err); return done(null, user, { scope: accessToken.scope }); }); }); } ));
4. 配置oauth2orize服务器
这是核心逻辑,处理授权码的发放和兑换:
const oauth2orize = require('oauth2orize'); const { v4: uuidv4 } = require('uuid'); const AuthorizationCode = require('./models/AuthorizationCode'); const AccessToken = require('./models/AccessToken'); // 创建oauth2服务器实例 const server = oauth2orize.createServer(); // 序列化/反序列化客户端(Passport需要) server.serializeClient((client, done) => done(null, client._id)); server.deserializeClient((id, done) => Client.findById(id, done)); // 1. 定义授权码生成逻辑 server.grant(oauth2orize.grant.code((client, redirectUri, user, _, done) => { const code = uuidv4(); const authCode = new AuthorizationCode({ code, clientId: client.clientId, userId: user._id, redirectUri, expires: new Date(Date.now() + 10 * 60 * 1000) // 10分钟过期 }); authCode.save((err) => { if (err) return done(err); done(null, code); }); })); // 2. 定义授权码兑换AccessToken的逻辑 server.exchange(oauth2orize.exchange.code((client, code, redirectUri, done) => { AuthorizationCode.findOne({ code }, (err, authCode) => { if (err) return done(err); if (!authCode || authCode.expires < new Date()) return done(null, false); if (authCode.clientId !== client.clientId || authCode.redirectUri !== redirectUri) return done(null, false); // 生成AccessToken const accessToken = uuidv4(); const token = new AccessToken({ token: accessToken, clientId: client.clientId, userId: authCode.userId, expires: new Date(Date.now() + 60 * 60 * 1000) // 1小时过期 }); token.save((err) => { if (err) return done(err); // 授权码一次性使用,用完删除 AuthorizationCode.deleteOne({ code }, (err) => done(err, accessToken)); }); }); }));
5. 挂载路由到Express
把OAuth2的端点和受保护API挂载到你的Express应用:
const express = require('express'); const app = express(); // 初始化Passport和Session app.use(require('express-session')({ secret: 'your-secret-key', resave: false, saveUninitialized: false })); app.use(passport.initialize()); app.use(passport.session()); app.use(express.urlencoded({ extended: true })); // 授权端点:引导用户登录并确认授权 app.get('/oauth/authorize', passport.authenticate('local', { session: true }), // 先强制用户登录 server.authorization((clientId, redirectUri, done) => { Client.findOne({ clientId }, (err, client) => { if (err) return done(err); if (!client || !client.redirectUris.includes(redirectUri)) return done(null, false); done(null, client, redirectUri); }); }), (req, res) => { // 渲染授权确认页面,让用户选择是否同意授权 res.render('authorize', { client: req.oauth2.client, user: req.user, redirectUri: req.oauth2.redirectUri }); } ); // 处理用户的授权决策(同意/拒绝) app.post('/oauth/decision', passport.authenticate('local', { session: true }), server.decision(), server.errorHandler() ); // Token端点:兑换AccessToken app.post('/oauth/token', passport.authenticate('oauth2-client-password', { session: false }), server.token(), server.errorHandler() ); // 受保护的API示例 app.get('/api/userinfo', passport.authenticate('bearer', { session: false }), (req, res) => { res.json({ id: req.user._id, username: req.user.username, email: req.user.email }); } ); // 启动服务器 mongoose.connect('mongodb://localhost:27017/oauth2-server') .then(() => app.listen(3000, () => console.log('OAuth2服务器运行在3000端口')));
6. 测试整个流程
- 先在数据库中创建一个测试客户端(记得
clientSecret要存bcrypt哈希后的值) - 第三方客户端跳转至授权地址:
http://localhost:3000/oauth/authorize?client_id=test-client&redirect_uri=http://localhost:3000/callback&response_type=code&scope=read - 用户登录后,确认授权,服务器会跳转到
http://localhost:3000/callback?code=xxx - 客户端用POST请求
http://localhost:3000/oauth/token,携带参数:grant_type=authorization_code、code=xxx、client_id=test-client、client_secret=xxx、redirect_uri=http://localhost:3000/callback,即可拿到access_token - 客户端用
access_token请求/api/userinfo,携带Authorization: Bearer xxx头,就能获取用户信息
内容的提问来源于stack exchange,提问作者Tanzil KM
相关产品推荐
相关产品推荐

