微信小程序 webview 登录态丢失:我用一次性 ticket 和 Redis 做 H5 静默换 token
从小程序跳 H5 后登录态容易丢,我把长期 token 改成一次性 ticket:Redis 短时效存储、后端核销、前端拦截和失败兜底,让订单和支付页能静默登录。

作者
ZHOUYI
最近遇到一个很典型的问题:用户在小程序里已经登录,但点进 webview 打开 H5 订单页,接口还是 401。页面要么跳登录,要么白屏,支付前最容易流失。一开始我以为是 cookie 没带上,后来发现 webview 里的 storage、cookie 和小程序登录态并不天然互通,尤其跨域名、首次打开时更容易出问题。我们也不敢把长期 token 直接拼在 URL 里,因为链接会被分享、缓存、日志记录,风险太大。
为什么选择一次性 ticket
思路很简单:小程序端先向后端申请一个短时效、只能使用一次的 ticket,再把 ticket 拼进 H5 地址。H5 拿到 ticket 后立刻去后端换 token,换完就作废。ticket 本身不携带用户明文信息,只作为后端查询的临时凭证。这样即使 URL 泄漏,别人也很难二次使用,H5 也不用感知小程序 code。
完整链路怎么做
- 小程序调用后端接口,用已有登录态申请 ticket,并记录来源页面。
- Node.js 生成随机 ticket,写入 Redis,设置 60 秒左右过期。
- webview 打开 H5 时只带 ticket,不带手机号、用户 ID 等敏感信息。
- H5 首次进入时拦截路由,先调用兑换接口,再渲染业务页面。
- 后端校验并删除 ticket,返回短效 token,前端继续请求订单或支付接口。
关键代码与踩坑
const key = `h5:ticket:${ticket}`;
const val = await redis.get(key);
if (!val) throw new Error('ticket 无效或已过期');
// 先删除,防止并发重复兑换
const removed = await redis.del(key);
if (!removed) throw new Error('ticket 已被使用');
const payload = JSON.parse(val);
const token = await signToken({ uid: payload.uid }, '15m');
return { token };前端这边,我在 Nuxt 路由中间件里判断:如果页面需要登录且 URL 有 ticket,就先调兑换接口;成功后把 token 放到内存和短期 storage。后续请求统一加 Authorization。若返回 401,只重走一次静默流程,仍失败就展示“请返回小程序重新进入”,避免用户卡在空白页。
- ticket 必须一次性消费,最好用 Redis 原子删除或 Lua 脚本,避免并发兑换。
- 兑换失败不要无限重试,否则过期 ticket 会被刷出大量异常日志。
- H5 请求层要做 401 拦截:优先静默重新获取,失败再提示用户返回小程序。
- 支付、订单这类页面,入口参数要记录 scene、来源和时间,方便复盘。
“这套方案不复杂,但比直接传 token 稳很多。小程序负责证明“我是谁”,H5 只拿一次性凭证换短效会话,职责清楚,出了问题也好定位。”
最后补一句:不同 webview 环境对 cookie、storage 的行为可能不一致,别把登录态完全寄托在浏览器能力上。上线前最好用真机多测几轮,尤其是支付页和分享回流页。涉及微信能力时,还是以官方文档为准。