Node.js中OAuth2服务器实现咨询:支持第三方客户端登录
用oauth2orize搭建Node.js OAuth2服务器(适配第三方登录场景)
我之前用oauth2orize搭过适配第三方登录的OAuth2服务,完全懂你那种理不清方法调用逻辑的困惑。咱们从授权码模式(就是你说的“使用Google登录”那种最常用的第三方登录模式)入手,一步步拆解实现流程,把每个环节的逻辑讲透。
先搞懂核心流程(别跳过!)
授权码模式的完整流程是这样的:
- 第三方客户端(比如Alexa)跳转到你的授权页,带上
client_id、redirect_uri、response_type=code、scope这些参数 - 用户在你的网站登录,确认是否授权给这个客户端
- 你的服务器生成临时授权码,跳回客户端的
redirect_uri并带上这个code - 客户端拿着
code、client_id、client_secret向你的服务器请求访问令牌(access token) - 你的服务器验证信息后,返回
access_token和refresh_token - 客户端拿着
access_token就能访问你的受保护API了
一步步落地实现
1. 初始化项目&安装依赖
先创建项目,安装必要的包:
npm init -y npm install express oauth2orize passport passport-local mongoose express-session body-parser bcrypt
oauth2orize: 核心的OAuth2服务器库passport: 处理用户本地登录认证mongoose: 操作MongoDB,存储客户端、用户、授权码、令牌信息express-session: 保存用户会话,维护登录状态bcrypt: 加密用户密码,保障安全
2. 定义数据模型
你需要四个核心模型,用mongoose实现如下:
Client模型(存储第三方客户端信息)
// models/Client.js const mongoose = require('mongoose'); const Schema = mongoose.Schema; const ClientSchema = new Schema({ clientId: { type: String, required: true, unique: true }, clientSecret: { type: String, required: true }, redirectUri: { type: String, required: true }, // 客户端的回调地址,比如Alexa指定的地址 name: { type: String } // 客户端名称,比如"Amazon Alexa" }); module.exports = mongoose.model('Client', ClientSchema);
User模型(你的网站用户)
// models/User.js const mongoose = require('mongoose'); const Schema = mongoose.Schema; const bcrypt = require('bcrypt'); const UserSchema = new Schema({ username: { type: String, required: true, unique: true }, password: { type: String, required: true } }); // 保存前自动加密密码 UserSchema.pre('save', async function(next) { if (!this.isModified('password')) return next(); this.password = await bcrypt.hash(this.password, 10); next(); }); // 验证密码方法 UserSchema.methods.validatePassword = async function(password) { return await bcrypt.compare(password, this.password); }; module.exports = mongoose.model('User', UserSchema);
AuthorizationCode模型(临时授权码)
// models/AuthorizationCode.js const mongoose = require('mongoose'); const Schema = mongoose.Schema; const AuthorizationCodeSchema = new Schema({ value: { type: String, required: true, unique: true }, clientId: { type: String, required: true }, userId: { type: Schema.Types.ObjectId, ref: 'User', required: true }, redirectUri: { type: String, required: true }, expiresAt: { type: Date, default: () => new Date(Date.now() + 10*60*1000) } // 10分钟过期,避免滥用 }); module.exports = mongoose.model('AuthorizationCode', AuthorizationCodeSchema);
AccessToken模型(访问令牌)
// models/AccessToken.js const mongoose = require('mongoose'); const Schema = mongoose.Schema; const AccessTokenSchema = new Schema({ value: { type: String, required: true, unique: true }, clientId: { type: String, required: true }, userId: { type: Schema.Types.ObjectId, ref: 'User', required: true }, expiresAt: { type: Date, default: () => new Date(Date.now() + 24*60*60*1000) } // 24小时过期 }); module.exports = mongoose.model('AccessToken', AccessTokenSchema);
3. 配置oauth2orize服务器(核心逻辑)
这部分是整个服务的核心,我会逐个方法拆解作用:
// oauth2.js const oauth2orize = require('oauth2orize'); const crypto = require('crypto'); const Client = require('./models/Client'); const AuthorizationCode = require('./models/AuthorizationCode'); const AccessToken = require('./models/AccessToken'); // 创建OAuth2服务器实例 const server = oauth2orize.createServer(); // 1. 序列化/反序列化客户端:oauth2orize需要跟踪客户端身份 server.serializeClient((client, done) => { return done(null, client.clientId); }); server.deserializeClient(async (clientId, done) => { try { const client = await Client.findOne({ clientId }); return done(null, client); } catch (err) { return done(err); } }); // 2. 定义授权码授予规则:生成并保存临时授权码 server.grant(oauth2orize.grant.code(async (client, redirectUri, user, ares, done) => { // 生成随机授权码 const codeValue = crypto.randomBytes(16).toString('hex'); // 保存到数据库 const code = new AuthorizationCode({ value: codeValue, clientId: client.clientId, userId: user._id, redirectUri: redirectUri }); await code.save(); return done(null, codeValue); })); // 3. 处理授权请求:验证客户端身份和回调地址 server.authorization(async (clientId, redirectUri, done) => { try { const client = await Client.findOne({ clientId }); if (!client) return done(null, false); // 客户端不存在 if (client.redirectUri !== redirectUri) return done(null, false); // 回调地址不匹配 return done(null, client, redirectUri); } catch (err) { return done(err); } }); // 4. 处理用户授权决策:用户登录后点击"允许/拒绝"的逻辑 server.decision(async (req, done) => { // req.user是passport登录后的用户对象 return done(null, { user: req.user }); }); // 5. 处理令牌请求:用授权码交换access token server.exchange(oauth2orize.exchange.code(async (client, code, redirectUri, done) => { try { const authCode = await AuthorizationCode.findOne({ value: code }); if (!authCode) return done(null, false); // 授权码无效 // 验证客户端和回调地址 if (authCode.clientId !== client.clientId || authCode.redirectUri !== redirectUri) { return done(null, false); } // 检查授权码是否过期 if (authCode.expiresAt < new Date()) return done(null, false); // 生成access token const tokenValue = crypto.randomBytes(32).toString('hex'); const accessToken = new AccessToken({ value: tokenValue, clientId: client.clientId, userId: authCode.userId }); await accessToken.save(); // 删除已使用的授权码(可选,更安全) await AuthorizationCode.deleteOne({ value: code }); return done(null, tokenValue); } catch (err) { return done(err); } })); module.exports = server;
4. 配置Passport处理用户登录
// passport.js const passport = require('passport'); const LocalStrategy = require('passport-local').Strategy; const User = require('./models/User'); // 本地登录策略:验证用户名密码 passport.use(new LocalStrategy(async (username, password, done) => { try { const user = await User.findOne({ username }); if (!user) return done(null, false, { message: '用户不存在' }); const isValid = await user.validatePassword(password); if (!isValid) return done(null, false, { message: '密码错误' }); return done(null, user); } catch (err) { return done(err); } })); // 序列化用户到session passport.serializeUser((user, done) => { done(null, user.id); }); // 从session反序列化用户 passport.deserializeUser(async (id, done) => { try { const user = await User.findById(id); done(null, user); } catch (err) { done(err); } }); module.exports = passport;
5. 搭建Express路由和服务器
// app.js const express = require('express'); const mongoose = require('mongoose'); const session = require('express-session'); const bodyParser = require('body-parser'); const passport = require('./passport'); const oauth2server = require('./oauth2'); const app = express(); // 连接MongoDB数据库 mongoose.connect('mongodb://localhost:27017/oauth2_server', { useNewUrlParser: true, useUnifiedTopology: true }).then(() => console.log('数据库连接成功')) .catch(err => console.error('数据库连接失败', err)); // 中间件配置 app.use(bodyParser.urlencoded({ extended: true })); app.use(session({ secret: 'your-custom-secret-key', // 换成自己的安全密钥 resave: false, saveUninitialized: false })); app.use(passport.initialize()); app.use(passport.session()); // 1. 授权相关路由 // GET: 显示登录&授权确认页 app.get('/oauth/authorize', oauth2server.authorization((req, res) => { // 用户未登录则跳转到登录页 if (!req.isAuthenticated()) { return res.redirect(`/login?redirect=${req.originalUrl}`); } // 简单渲染授权确认页 res.send(` <h1>是否授权给 ${req.oauth2.client.name}?</h1> <form method="post" action="/oauth/authorize/decision"> <button type="submit" name="allow" value="true">允许</button> <button type="submit" name="allow" value="false">拒绝</button> </form> `); })); // POST: 处理用户授权决策 app.post('/oauth/authorize/decision', passport.authenticate('session'), oauth2server.decision()); // 2. 令牌交换路由 app.post('/oauth/token', passport.authenticate(['basic', 'oauth2-client-password'], { session: false }), oauth2server.token(), oauth2server.errorHandler()); // 3. 用户登录路由 app.get('/login', (req, res) => { res.send(` <h1>登录</h1> <form method="post" action="/login"> <div> <label>用户名:<input type="text" name="username"></label> </div> <div> <label>密码:<input type="password" name="password"></label> </div> <button type="submit">登录</button> <input type="hidden" name="redirect" value="${req.query.redirect || '/'}"> </form> `); }); app.post('/login', passport.authenticate('local', { failureRedirect: '/login' }), (req, res) => { res.redirect(req.body.redirect || '/'); }); // 4. 受保护API示例 app.get('/api/user', passport.authenticate('oauth2-bearer', { session: false }), (req, res) => { res.json({ id: req.user._id, username: req.user.username }); }); // 启动服务器 app.listen(3000, () => { console.log('OAuth2服务器运行在 http://localhost:3000'); });
6. 测试流程
- 先往数据库插入测试客户端和用户:
- 客户端:
Client.create({ clientId: 'alexa-test', clientSecret: 'alexa-secret', name: 'Amazon Alexa', redirectUri: 'https://example.com/callback' }) - 用户:
User.create({ username: 'testuser', password: 'testpass' })
- 客户端:
- 启动服务器:
node app.js - 模拟客户端请求授权:访问
http://localhost:3000/oauth/authorize?client_id=alexa-test&redirect_uri=https://example.com/callback&response_type=code&scope=user - 登录测试用户,点击"允许",服务器会跳转到回调地址并带上
code参数 - 用Postman发送POST请求到
http://localhost:3000/oauth/token,参数:grant_type=authorization_codecode=拿到的授权码redirect_uri=https://example.com/callback- 头部加Basic Auth:用户名
alexa-test,密码alexa-secret
- 拿到
access_token后,请求http://localhost:3000/api/user,头部加Authorization: Bearer 你的access_token,即可获取用户信息
常见坑点提示
- 务必验证
redirectUri,防止恶意回调地址 - 授权码使用后建议删除,避免重复使用
- 生产环境必须使用HTTPS,防止敏感信息明文传输
- Session存储建议用Redis等持久化方案,不要用内存存储
内容的提问来源于stack exchange,提问作者mahendra
相关产品推荐
相关产品推荐

