
网页端小程序码登录实战
背景与目标
项目原本使用短信验证码登录,但短信通道出现故障后,继续依赖单一供应商并不是最稳妥的选择。考虑到产品已经有认证过的微信小程序,并且小程序具备手机号快速验证能力,我们决定增加小程序码登录。
这里使用的不是微信开放平台的网站应用二维码,而是微信小程序码。用户在电脑打开登录页后,用微信扫码进入小程序,授权手机号并确认,网页随后自动完成登录。
最终目标包括:
- 网页不再依赖短信验证码;
- 继续保留密码登录;
- 小程序负责验证微信身份和手机号;
- 网页沿用原有 JWT 登录态;
- 二维码过期、重复扫码和并发换票都必须安全失败。
图中的二维码属于短时登录会话,发布文章时已经失效。
整体方案
整个流程由网页、小程序、业务后端、Redis 和微信接口共同完成:
- 网页向后端创建短时登录会话。
- 后端返回公开的
sessionId和仅当前浏览器持有的pollToken。 - 后端把
sessionId写入小程序码的scene。 - 用户扫码进入小程序确认页。
- 小程序等待用户主动授权手机号。
- 小程序提交
sessionId、wx.logincode 和手机号动态 code。 - 后端向微信校验凭证并关联业务用户。
- 网页轮询到确认状态,用
pollToken一次性换取 JWT。 - 登录会话立即失效,不能再次换票。
这套设计的核心是:小程序只确认用户身份,网页 JWT 只交给最初创建会话的浏览器。
双票据安全模型
二维码是一种公开信息,可能被拍照、截图或转发,因此不能在二维码中放 JWT、手机号、用户 ID 或权限信息。
最终使用两份随机票据:
1 | 二维码中 sessionId |
sessionId 只负责定位会话。pollToken 不进入二维码,小程序也不会收到它。即使其他人拿到了二维码,也无法查询完整状态或领取 JWT。
服务端只保存 pollToken 的哈希,避免 Redis 数据意外暴露时直接泄漏换票凭据。
Redis 状态机
扫码登录是一段跨设备的异步流程,不能用一次普通请求完成。登录会话保存在 Redis 中,并设置 3 分钟 TTL。
| 状态 | 含义 |
|---|---|
WAITING |
网页等待扫码 |
SCANNED |
小程序已打开并等待确认 |
CONFIRMED |
身份确认成功并等待网页换票 |
CONSUMED |
网页已经领取 JWT |
EXPIRED |
会话已经过期 |
Redis 数据大致如下:
1 | key: auth:miniapp-code:{sessionId} |
确认和换票使用 Lua 脚本完成原子状态转换。只有 CONFIRMED 状态可以进入 CONSUMED,所以并发请求中最多只有一个能领取登录结果。
使用 Redis 而不是 Java 进程内 Map,还能避免应用重启丢失会话,并为以后扩展多实例部署保留空间。
接口设计
后端提供六类接口:
| 调用方 | 方法 | 路径 | 作用 |
|---|---|---|---|
| 网页 | POST |
/api/auth/miniapp-code/sessions |
创建会话 |
| 网页 | GET |
/api/auth/miniapp-code/sessions/{id}/image |
获取小程序码 |
| 小程序 | POST |
/api/auth/miniapp-code/sessions/{id}/scanned |
标记已扫码 |
| 小程序 | POST |
/api/auth/miniapp-code/sessions/{id}/confirm |
确认身份 |
| 网页 | GET |
/api/auth/miniapp-code/sessions/{id}/status |
查询状态 |
| 网页 | POST |
/api/auth/miniapp-code/sessions/{id}/exchange |
换取 JWT |
创建会话返回的数据结构如下:
1 | { |
网页查询状态和换票时必须通过 X-Login-Poll-Token 请求头提交私密票据。小程序端的扫码和确认接口不使用这个值。
后端实现
后端主要负责四件事:
创建登录会话
sessionId 和 pollToken 都使用安全随机数生成。后端把会话写入 Redis,并设置独立的接口限流,避免恶意请求持续消耗小程序码接口额度。
生成小程序码
通过 getwxacodeunlimit 生成不限量小程序码:
1 | { |
登录码不能像普通分享码一样长期保存,图片接口设置 Cache-Control: no-store。微信 access_token 则可以缓存在 Redis 中,避免每次生成二维码都重新获取。
校验微信身份
小程序确认时,后端分别校验 wx.login code 和手机号动态 code,得到 openid 与手机号,再查找或创建已有业务用户。
这里不能为了兼容旧接口,在微信换号失败时相信客户端自己提交的手机号。否则客户端可以直接伪造身份。
一次性换票
小程序确认后只把 userId 写入会话,不向小程序返回网页 JWT。网页带着正确的 pollToken 换票时,后端才签发 JWT,并立即把状态改为 CONSUMED。
小程序端实现
扫码登录使用独立主包页面:
1 | pages/scan-login/index |
不要直接复用业务首页。原有首页可能已经处理职位分享等其他 scene 参数,登录票据容易被旧逻辑误判。
扫码页面从 scene 中解析会话 ID:
1 | const pair = scene.split('&').find(item => item.startsWith('l=')) |
手机号不能静默获取,必须由用户主动点击授权按钮:
1 | <button |
成功回调中的手机号动态 code 与 wx.login code 是两种不同凭证,不能混用。本项目同时提交两者,因为现有用户体系既需要手机号,也需要维持 openid 关联。
小程序只展示确认结果,不接收、不保存网页 JWT。
网页端实现
网页封装一个可复用的扫码登录组件,统一处理:
- 会话创建和二维码加载;
- 每 2 秒轮询状态;
- 二维码倒计时;
- 扫码后的确认状态;
- JWT 换票和登录跳转;
- 过期或失败后的刷新操作。
页面切到后台时暂停轮询,重新可见后立即同步状态。这样可以减少多标签页和长时间后台页面产生的无效请求。
扫码登录和密码登录共用同一套用户信息及 token 存储逻辑,退出登录也同时清理两份状态,避免请求头残留旧 JWT。
项目原来存在主登录页、动态弹窗和历史内嵌登录框。实现时必须梳理所有入口并复用同一个组件,否则只替换主页面后,用户仍可能从其他入口进入旧短信登录流程。
环境切换
小程序码的 env_version 必须和当前联调阶段一致:
| 阶段 | 值 |
|---|---|
| 开发版 | develop |
| 体验版 | trial |
| 正式版 | release |
体验版联调时,先上传小程序体验版,再部署使用 trial 的网页构建。小程序正式发布后,网页也要重新构建为 release,否则普通用户无法打开对应页面。
两个实际问题
图片类型与响应头不一致
最初图片接口固定声明:
1 | Content-Type: image/png |
真实调用微信接口后发现,小程序码字节以 FF D8 FF 开头,实际是 JPEG。浏览器虽然可能正常显示,但错误媒体类型经过 CDN 或严格客户端时可能产生问题。
最终根据文件签名返回类型:
1 | if (isPng(payload)) return MediaType.IMAGE_PNG; |
部署脚本误判进程
旧管理脚本在 PID 文件缺失时通过命令行搜索 JAR 名称。如果远程部署命令本身也包含 JAR 文件名,脚本可能把部署 shell 误认为 Java 进程。
部署动作因此拆成独立步骤:
- 上传 incoming 文件;
- 校验两端 SHA-256;
- 备份当前版本;
- 单独停止服务;
- 切换 JAR;
- 单独启动服务;
- 检查端口和业务接口;
- 失败时恢复备份。
网页静态文件也先上传到临时目录,校验后再原子切换 dist,并保留带时间戳的回滚目录。
安全检查
- 二维码不包含手机号、用户 ID、JWT 或权限;
pollToken不进入二维码,服务端只保存哈希;- 会话短时有效且只能消费一次;
- 确认与换票操作具备原子性;
- 创建、轮询、确认和换票接口分别限流;
- 二维码图片禁止缓存;
- 拒绝手机号授权时网页不会登录;
- 登录响应不返回密码、
sessionKey等敏感字段; - 日志不记录微信动态 code、
access_token、完整手机号和 JWT。
验收清单
- 网页可以正常显示小程序码。
- 扫码后进入正确的小程序确认页。
- 仅扫码但未授权时网页不会登录。
- 拒绝手机号授权后网页保持未登录。
- 同意授权后网页自动完成登录。
- 刷新网页后登录态仍然有效。
- 同一个二维码不能重复换取 JWT。
- 过期二维码不能继续确认或换票。
- 没有
pollToken时不能领取登录结果。 - 全流程不调用短信发送接口。
- 生产日志中没有敏感凭证。
- 正式发布时网页使用
release环境。
总结
小程序码登录的重点并不是生成二维码,而是建立一套安全的跨设备确认协议。
sessionId 相当于公开的会话地址,pollToken 相当于浏览器的取件凭证。小程序确认用户身份,Redis 管理状态与过期时间,Lua 保证一次性消费,最终只有最初的浏览器能够取得 JWT。
如果项目已经具备认证小程序、手机号授权、统一用户表和 Redis,这套方案可以在较小改造范围内替代网页短信验证码登录,同时保留原有密码登录作为回退方式。




