React+Node集成QuickBooks OAuth2时重定向后access_token丢失问题
问题根因
- 异步逻辑竞态:
/callback接口中createToken是异步操作,你没有等待token生成完成就直接跳转前端页面,大部分情况下前端发起/getCompanyInfo请求时,后端还没完成token存储,直接抛出缺失access_token的错误,偶尔成功只是网络延迟低时的巧合。 - token存储方式错误:你将
oauthClient实例和token存在服务端全局变量中,这类临时内存变量会在服务端重启、多请求覆盖、内存回收时丢失,刷新页面后没有重新走授权流程的话,全局变量没有有效值,自然调用失败。同时这种写法仅支持单用户使用,多用户访问会出现token串用的安全问题。 - 前端缺少兜底逻辑:React组件中info初始值为空字符串,直接读取
info.CompanyInfo.CompanyName会在数据未返回时抛出未定义错误,直接导致组件崩溃。
修复方案
1. 修复回调接口异步顺序
调整 /callback 逻辑,等待token生成完成后再执行跳转:
app.get("/callback", async function (req, res) { try { const authResponse = await oauthClient.createToken(req.url) // 这里后续可以改成存储到session/数据库,暂时先保留全局变量验证逻辑 oauth2_token_json = JSON.stringify(authResponse.getJson(), null, 2); res.redirect("http://localhost:3000/companyInfo"); } catch (e) { console.error(e); res.status(500).send("授权失败,请重试"); } });
2. 替换临时全局存储为session存储(推荐)
安装 express-session 依赖,将token和用户会话绑定,刷新页面也不会丢失:
首先执行安装命令:npm install express-session
在服务端代码中初始化session:
const session = require('express-session'); // 放在所有路由之前初始化 app.use(session({ secret: '自定义任意加密字符串', resave: false, saveUninitialized: false, cookie: { secure: false } // 生产环境部署为https时改为true }))
调整授权逻辑存储token到session:
// /callback 接口调整 app.get("/callback", async function (req, res) { try { const authResponse = await oauthClient.createToken(req.url) const tokenJson = authResponse.getJson(); req.session.oauthToken = tokenJson; req.session.realmId = tokenJson.realmId; res.redirect("http://localhost:3000/companyInfo"); } catch (e) { console.error(e); res.status(500).send("授权失败,请重试"); } });
调整 /getCompanyInfo 从session取token调用接口:
app.get("/getCompanyInfo", async (req, res) => { // 没有token时返回错误,前端可以引导重新授权 if (!req.session.oauthToken || !req.session.realmId) { return res.status(401).send("未授权,请先登录"); } // 给oauthClient赋值已存储的token oauthClient.setToken(req.session.oauthToken); const companyID = req.session.realmId; const url = oauthClient.environment == "sandbox" ? OAuthClient.environment.sandbox : OAuthClient.environment.production; try { const authResponse = await oauthClient.makeApiCall({ url: url + "v3/company/" + companyID + "/companyinfo/" + companyID, }); console.log("The response for API call is :" + JSON.stringify(authResponse)); res.send(JSON.parse(authResponse.text())); } catch (e) { console.error(e); res.status(500).send("接口调用失败"); } });
3. 修复React组件兜底逻辑
给属性读取加可选链和兜底文本,避免未加载完成时崩溃:
import React, { useState, useEffect } from 'react' import axios from 'axios' const CompanyInfo = () => { const [ info, setInfo ] = useState({}) // 初始值改为空对象更合理 useEffect(() => { axios.get('/getCompanyInfo') .then(res => setInfo(res.data)) .catch(err => { // 可以在这里判断401错误,跳转到授权页面 console.error(err) }) },[]) return( <div> <p> {info?.CompanyInfo?.CompanyName || '数据加载中...'} </p> </div> ) } export default CompanyInfo
内容的提问来源于stack exchange,提问作者Josh Simon
相关产品推荐
相关产品推荐

