Node.js中OAuth2授权实现故障及第三方应用认证求助
嘿,Raj!我来帮你搞定这个Node.js下的OAuth2服务端和第三方应用集成的问题——我之前也踩过类似的坑,咱们一步步来拆解排查。
核心架构先理清楚
首先得明确两个角色的职责,避免混淆:
- 认证服务器(你的服务器dummy应用):负责管理用户信息、处理授权请求、颁发访问令牌
- 第三方客户端(你的另一个dummy应用):引导用户跳转到认证服务器完成授权,拿到令牌后调用服务器的受保护接口
我会基于常用的oauth2-server(服务端)和passport-oauth2(客户端)包来给你写可运行的示例,同时排查常见问题。
第一步:搭建认证服务器
先初始化项目,安装依赖:
npm init -y npm install oauth2-server express body-parser cors
然后创建server.js,核心代码如下(我加了详细注释):
const express = require('express'); const bodyParser = require('body-parser'); const OAuth2Server = require('oauth2-server'); const cors = require('cors'); const app = express(); // 解决跨域问题,避免客户端调用时被浏览器拦截 app.use(cors()); app.use(bodyParser.urlencoded({ extended: false })); app.use(bodyParser.json()); // 模拟数据库(实际项目要换成MongoDB/MySQL等持久化存储) const mockDB = { // 用户列表:实际应该存加密后的密码! users: [{ id: 1, username: 'testuser', password: 'testpass' }], // 第三方客户端信息:clientId和secret要和客户端配置一致 clients: [{ id: 'dummy-client', secret: 'dummy-secret', redirectUris: ['http://localhost:3001/callback'] }], authorizationCodes: [], accessTokens: [] }; // 配置OAuth2核心模型(必须实现这些方法) const oauth = new OAuth2Server({ model: { // 根据clientId和secret验证客户端合法性 getClient: async (clientId, clientSecret) => { return mockDB.clients.find(client => client.id === clientId && client.secret === clientSecret ); }, // 根据用户名密码验证用户身份 getUser: async (username, password) => { return mockDB.users.find(user => user.username === username && user.password === password ); }, // 保存授权码(用户授权后生成,用于交换令牌) saveAuthorizationCode: async (code, client, user) => { mockDB.authorizationCodes.push({ ...code, client, user }); return code; }, // 根据授权码查询信息 getAuthorizationCode: async (authorizationCode) => { return mockDB.authorizationCodes.find(code => code.authorizationCode === authorizationCode ); }, // 授权码使用后销毁,避免重复使用 revokeAuthorizationCode: async (code) => { const index = mockDB.authorizationCodes.findIndex(c => c.authorizationCode === code.authorizationCode ); if (index !== -1) mockDB.authorizationCodes.splice(index, 1); return true; }, // 保存生成的访问令牌 saveToken: async (token, client, user) => { mockDB.accessTokens.push({ ...token, client, user }); return token; }, // 根据访问令牌验证合法性 getAccessToken: async (accessToken) => { return mockDB.accessTokens.find(token => token.accessToken === accessToken); } }, // 可选:配置令牌过期时间,默认是1小时 accessTokenLifetime: 3600 }); // 1. 授权端点:引导用户进行登录和授权 app.get('/oauth/authorize', (req, res) => { const request = new OAuth2Server.Request(req); const response = new OAuth2Server.Response(res); return oauth.authorize(request, response) .then(() => { // 如果用户未登录,这里可以跳转到你的登录页面 // 示例里直接模拟登录成功,返回授权页面(实际要做登录表单) res.send(` <h1>授权确认</h1> <p>dummy-client请求访问你的账号</p> <form method="POST" action="/oauth/authorize"> <input type="hidden" name="user_id" value="1"> <button type="submit" name="allow" value="true">允许</button> <button type="submit" name="allow" value="false">拒绝</button> </form> `); }) .catch(err => res.status(err.code || 500).json(err)); }); // 处理授权提交 app.post('/oauth/authorize', (req, res) => { const request = new OAuth2Server.Request(req); const response = new OAuth2Server.Response(res); // 模拟已登录用户,实际要从session里取user_id request.body.user = mockDB.users.find(u => u.id === parseInt(req.body.user_id)); request.body.allow = req.body.allow === 'true'; return oauth.authorize(request, response) .then(() => res.redirect(response.headers.location)) .catch(err => res.status(err.code || 500).json(err)); }); // 2. 令牌端点:用授权码交换访问令牌 app.post('/oauth/token', (req, res) => { const request = new OAuth2Server.Request(req); const response = new OAuth2Server.Response(res); return oauth.token(request, response) .then(token => res.json(token)) .catch(err => res.status(err.code || 500).json(err)); }); // 3. 受保护的示例接口:需要携带访问令牌才能访问 app.get('/api/protected', (req, res) => { const request = new OAuth2Server.Request(req); const response = new OAuth2Server.Response(res); return oauth.authenticate(request, response) .then(user => res.json({ message: '这是只有授权用户才能看到的内容', userInfo: { id: user.id, username: user.username } })) .catch(err => res.status(err.code || 401).json(err)); }); app.listen(3000, () => { console.log('认证服务器运行在 http://localhost:3000'); });
第二步:搭建第三方客户端应用
同样初始化项目,安装依赖:
npm init -y npm install express passport passport-oauth2 axios express-session
创建client.js,核心代码如下:
const express = require('express'); const passport = require('passport'); const OAuth2Strategy = require('passport-oauth2'); const axios = require('axios'); const session = require('express-session'); const app = express(); // 配置session,用于保存Passport的用户信息 app.use(session({ secret: 'client-secret-key', resave: false, saveUninitialized: false })); app.use(passport.initialize()); app.use(passport.session()); // 配置OAuth2策略:对接认证服务器的端点 passport.use(new OAuth2Strategy({ authorizationURL: 'http://localhost:3000/oauth/authorize', tokenURL: 'http://localhost:3000/oauth/token', clientID: 'dummy-client', clientSecret: 'dummy-secret', callbackURL: 'http://localhost:3001/callback' }, (accessToken, refreshToken, profile, done) => { // 这里可以把令牌和用户信息存到客户端自己的数据库 // 示例里直接返回令牌,方便后续调用接口 return done(null, { accessToken }); })); // Passport序列化/反序列化用户(session存储用) passport.serializeUser((user, done) => done(null, user)); passport.deserializeUser((user, done) => done(null, user)); // 登录路由:跳转到认证服务器进行授权 app.get('/login', passport.authenticate('oauth2')); // 回调路由:处理认证服务器返回的授权码,交换令牌 app.get('/callback', passport.authenticate('oauth2', { failureRedirect: '/login' }), (req, res) => { // 授权成功,跳转到客户端首页 res.redirect('/'); } ); // 客户端首页:调用认证服务器的受保护接口 app.get('/', (req, res) => { if (!req.user) { return res.send('<h1>未登录</h1><a href="/login">点击登录</a>'); } // 使用拿到的accessToken调用服务器的受保护接口 axios.get('http://localhost:3000/api/protected', { headers: { Authorization: `Bearer ${req.user.accessToken}` } }) .then(response => { res.send(` <h1>授权成功!</h1> <p>从服务器获取的信息:</p> <pre>${JSON.stringify(response.data, null, 2)}</pre> `); }) .catch(err => { res.send(` <h1>调用接口失败</h1> <p>错误信息:${err.response.data.message}</p> <a href="/login">重新登录</a> `); }); }); app.listen(3001, () => { console.log('第三方客户端运行在 http://localhost:3001'); });
常见问题排查(解决你可能遇到的异常)
- 授权码无效/令牌获取失败:
- 检查客户端的
redirectURL和认证服务器配置的redirectUris完全一致(包括端口、路径,不能多斜杠) - 确认请求令牌时的
grant_type是authorization_code,并且正确传递了code和redirect_uri参数
- 检查客户端的
- 接口401未授权:
- 确保请求头的
Authorization格式是Bearer {accessToken},没有拼写错误 - 检查accessToken是否过期(可以延长
accessTokenLifetime测试)
- 确保请求头的
- 跨域报错:
- 认证服务器必须添加CORS中间件(示例里已经加了
cors())
- 认证服务器必须添加CORS中间件(示例里已经加了
- 包版本冲突:
- 尽量使用
oauth2-server和passport-oauth2的最新稳定版,避免版本不兼容
- 尽量使用
测试流程
- 启动认证服务器:
node server.js - 启动客户端:
node client.js - 访问
http://localhost:3001,点击登录跳转到认证服务器 - 在授权页面点击「允许」,会自动跳回客户端,就能看到调用服务器受保护接口的结果了
内容的提问来源于stack exchange,提问作者Raj kumar
相关产品推荐
相关产品推荐

