微信网页授权接入协议
本项目是服务号网页授权的前端回调中转站,负责接收并传递 code 和 state。服务号 AppSecret、换取 access_token、验证 state 和登录会话均在调用方后端完成。当前没有实现后端登录,也不会把“收到 code”显示为“登录成功”。
回调入口
| 场景 | 推荐回调路径 | 必填业务参数 | 接收方式 |
|---|---|---|---|
| 微信内网页 | /callback/web | scene=1、origin | location.replace 返回完整原网页 URL |
| 小程序 web-view | /callback/mini | scene=2、minipath | wx.miniProgram.redirectTo 返回原生小程序页面 |
为兼容只传 scene 的调用方式,根路径 /?scene=1...、/?scene=2... 同样支持。独立路径与 scene 不一致会报错。微信追加的 code 和调用方原样返回的非空 state 也必须存在;重复参数、不支持的 scene、缺少 code 等情况展示无标题错误弹窗,不自动重新授权。
回调域名默认为 webauth.wx.zhang520.cn。VITE_AUTH_CALLBACK_ORIGIN 是构造链接的公开配置;回调接收按实际访问域名运行。更换域名时,同时更新调用方构造链接、部署站点、服务号网页授权域名和小程序业务域名。
场景一:返回原网页
origin 虽然名为 origin,实际是完整 URL,例如 https://demo.example/home/index?foo=bar#section,必须使用 HTTPS。每一层查询参数仅编码一次:
import { buildAuthorizeUrl } from "@/auth/protocol";
import { callbackOrigin } from "@/auth/config";
// 在原网站使用时,可复制 protocol.ts。state 由原网站后端生成、保存并绑定用户会话。
const authorizationUrl = buildAuthorizeUrl({
appId: "wx你的服务号AppID",
state: stateFromBackend,
scope: "snsapi_base",
callbackOrigin,
destination: {
scene: "1",
origin: "https://demo.example/home/index?foo=bar#section",
},
});
window.location.assign(authorizationUrl);构造出的授权 URL 参数顺序遵循官方示例,包含 #wechat_redirect。snsapi_userinfo 应由用户点击按钮发起,避免页面加载时直接拉起该授权。
微信返回中转站后,结果示意为:
https://demo.example/home/index?foo=bar&code=CODE&state=STATE#section原 URL 的其他查询参数和 hash 保留,旧的 code/state 全部替换。hash 路由项目也从 window.location.search 读取结果,而非从 hash 查询中读取。调用方拿到结果后提交给后端验证 state、一次性换取授权信息,再用 history.replaceState 清除地址栏中的 code/state。
禁止 HTTP、相对地址、javascript URL、含账号密码的地址和指向中转站自身的地址。可通过 VITE_ALLOWED_WEB_ORIGINS 设置精确允许列表(例如 https://demo.example,https://app.example:8443);空值按需求允许任意外部 HTTPS 原网页。生产环境建议填写实际接入网站,以限制 code 被转发的目的地;前端允许列表不能替代后端 state 与业务目标的绑定校验。
场景二:可靠返回小程序
传输与时序
- 专用授权页加载 web-view,完成微信网页授权,回调到
/callback/mini?scene=2&minipath=...&code=...&state=...。 - H5 保留参数在内存中并清除自身地址栏的 code/state;加载微信官方 JS-SDK 1.6.0,等待
WeixinJSBridgeReady,通过getEnv确认小程序环境。 - H5 调用
postMessage({ data: { type: 'WECHAT_WEB_AUTH_RESULT', version: 1, code, state } }),作为补充通知。 - 不等待 message 事件,立即调用
redirectTo({ url: '/pages/auth-result/index?code=...&state=...' })。 - 目标小程序页从
onLoad(options)接收参数。旧专用授权页被替换销毁,无需占用页面栈或等待额外确认。
官方 postMessage 在后退、组件销毁、分享、复制链接等特定时机才触发 bindmessage,且 event.detail.data 是消息数组。它不是实时通信接口。不能假定它先于目标页 onLoad,更不能在 bindmessage 中再次跳转、重复交换 code。本方案让导航 URL 携带完整结果,因此即使消息延迟或缺失,目标页也能独立接收。收到消息、桥接 API 回调和页面隐藏均不等于业务后端登录成功。
SDK/桥接/环境检测/跳转等待均有超时提示;失败只允许用户手动重试,不循环导航。跳转 API 未报错但 10 秒后当前页仍未离开,也显示重试提示。真实网络、用户取消、微信限制或错误路径仍可能导致失败,不能以此保证所有设备无条件成功。
minipath 约定
- 必须是当前小程序内已注册的非 tabBar 页面,使用绝对路径,例如
/pages/auth-result/index或/package-auth/pages/result/index?from=home。 - 不允许 URL、相对路径、hash、路径穿越,不应指向专用 web-view 授权页自身。
- 原查询参数保留,旧 code/state 替换。原生小程序目标页将 options 中的结果
decodeURIComponent一次;不要对已解码的 postMessage 数据再次解码。 - 目标页应使用
onLoad接收,不使用onShow再次交换 code。tabBar 业务需先经过一个普通接收页,验证并处理结果后再switchTab;后者不支持通过 URL 查询参数传递结果。 - 可通过
VITE_ALLOWED_MINI_PATHS设置精确路由列表(不含查询参数)。
可复制的小程序示例
仓库 examples/miniprogram/ 提供原生小程序示例,未修改其他项目:
utils/auth-session.js state 关联、消息补充接收、单次消费
pages/web-auth/index.* 专用 web-view 授权页
pages/auth-result/index.* 从 onLoad 接收结果的普通页面- 将 utils 和 pages 文件复制到小程序相应目录,按实际目录调整 require 路径。示例 package.json 仅用于仓库中的 Node 测试,不需要复制。
- 在小程序
app.json的 pages 注册pages/web-auth/index和pages/auth-result/index,后者不要加入 tabBar。 - 修改 web-auth/index.js 中的
SERVICE_APP_ID、CALLBACK_ORIGIN、MINI_PATH。必须使用已认证服务号 AppID,不是小程序 AppID。 - 原调用页取得后端随机 state 后,先建立本地关联,再打开授权页:
const { beginAuth } = require("../../utils/auth-session");
// stateFromBackend 来自你自己的后端授权事务接口,不能使用固定值或 Math.random。
beginAuth(stateFromBackend);
wx.navigateTo({
url: "/pages/web-auth/index?state=" + encodeURIComponent(stateFromBackend),
});auth-result/index.js已取得result.code和result.state,在标注位置接入业务后端。后端必须校验 state 对应当前用户会话与目标业务,并对授权事务实现幂等处理;网络超时应查询该事务状态,不盲目重复兑换 code。
示例使用内存事务表,五分钟过期、只消费一次;小程序进程重启或 state 不匹配会要求重新授权。bindmessage 只保存补充通知,不写持久存储、不触发登录、不干扰结果页。示例没有虚构后端地址,接收参数后的业务登录逻辑需由实际项目对接。
部署
pnpm install --frozen-lockfile,配置.env.local或部署环境变量后执行pnpm test、pnpm build。VITE_配置在构建时注入,修改后需重新构建。- 将 dist 部署至回调域名的 HTTPS 站点根目录。配置 SPA 路由回退,保证直接访问
/callback/web、/callback/mini能返回 index.html。 - 服务号后台「网页授权域名」填写
webauth.wx.zhang520.cn,不带协议或路径;按微信要求放置校验文件。 - 小程序配置该站点的「业务域名」并部署校验文件,检查账号资质、授权链路涉及的域名及真机访问能力。仅关闭开发者工具域名校验不能代替正式配置。
- 回调响应使用
Cache-Control: no-store、Referrer-Policy: no-referrer,避免在访问日志中记录查询字符串,不注入统计或第三方追踪脚本。
Nginx 路由与缓存完整配置见阿里云 ESA 加速与缓存。回调和带参数的根路径继续 no-store;无参数首页及构建哈希资源可缓存。需要根据原始 request_uri 区分,避免内部重写到 index.html 后丢失回调的禁缓存策略。
正式部署还应检查反向代理/CDN 的日志与缓存配置。微信 code 单次使用且五分钟过期,AppSecret 和 access_token 仅保留在后端。本项目不校验来自另一个 origin 的用户会话,调用方后端验证 state 是必需步骤。
真机验收
- 微信内浏览器:完整原 URL、既有查询参数、中文参数、hash、旧 code/state 替换;验证后端 state 与 code 兑换。
- iOS / Android 微信小程序:授权完成后目标页收到准确参数,专用 web-view 页从页面栈销毁;分别检查消息早到、晚到、未到时只处理一次。
- 用户拒绝授权、缺参、未知 scene、无效目标、SDK 加载失败、桥接超时、目标未注册、tabBar 目标、刷新、返回和重复打开。
- 将结果提交后端后模拟网络超时,验证事务幂等和过期后的重新授权。
自动化测试覆盖编码与协议校验、桥接就绪/超时/失败、小程序消息时序和单次消费;不能替代微信真机、后台域名配置和业务后端联调。
官方依据:微信网页授权、web-view、redirectTo。
