跳到主要内容

微信小程序扫码登录:从零实现一个完整的登录系统

前言

微信小程序扫码登录是当下 Web 应用中非常常见的登录方式。用户在浏览器上看到一个二维码,用微信扫一扫,小程序弹出确认页面,点击确认后 Web 端自动完成登录——整个体验流畅且安全。

本文将基于一个基于 Hono 框架的实际项目,从架构设计和实现逻辑的角度,完整梳理扫码登录的核心流程与关键技术决策。

整体架构

项目采用经典的分层架构,基于 Hono 框架构建后端 API 服务:

  • 配置层:通过 Zod Schema 校验环境变量,确保配置的类型安全
  • 类型层:定义登录状态机、错误码、请求/响应接口等核心类型
  • 服务层:封装会话管理、JWT 签发、微信 API 交互、用户管理等业务逻辑
  • 路由层:定义 API 端点,串联服务层完成请求处理
  • 中间件层:统一响应格式,将所有 API 响应包装为 { code, msg, data } 结构

项目还集成了 hono-openapi 和 Scalar,自动生成 OpenAPI 文档并提供交互式 API 文档界面,方便前端联调和测试。

核心流程:一个五态状态机

整个扫码登录的本质是一个有限状态机。每个登录会话(Session)都有唯一的状态,状态之间只能按预定义的路径转移。

状态定义

登录会话定义了五种状态:

状态含义
pending会话已创建,等待用户扫码
scanned用户已扫码,等待确认
confirmed用户已确认,登录成功
expired会话超时失效
cancelled用户主动取消

合法状态转移

状态之间的转移被严格约束,非法转移会被拒绝:

pending  --> scanned / expired / cancelled
scanned --> confirmed / expired / cancelled
confirmed / expired / cancelled --> 终态,不可再转移

这个状态机设计保证了登录流程的安全性。例如,一个已经确认登录的会话不能被重复确认,防止重放攻击;一个已取消的会话也不能被恢复。

完整时序

整个登录流程涉及三个参与方:Web 浏览器后端服务微信小程序

┌──────────┐          ┌──────────┐          ┌──────────┐
│ Web端 │ │ 后端服务 │ │ 小程序 │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
│ POST /api/qrcode │ │
│─────────────────────>│ │
│ │── 创建Session(pending)│
│ │── 调用微信API生成小程序码│
│ 返回sessionId+二维码 │ │
│<─────────────────────│ │
│ │ │
│ 开始轮询GET /api/status/:sessionId │
│─────────────────────>│ │
│ 返回status=pending │ │
│<─────────────────────│ │
│ │ │
│ │ 用户扫码,打开小程序 │
│ │ 小程序获取scene参数 │
│ │ 调用wx.login获取code │
│ │ │
│ │ POST /api/scan │
│ │ {sessionId, code} │
│ │<─────────────────────│
│ │── code2Session换取openid│
│ │── 更新Session(scanned) │
│ │ │
│ GET /api/status │ │
│─────────────────────>│ │
│ 返回status=scanned │ │
│<─────────────────────│ │
│ │ │
│ │ POST /api/confirm │
│ │ {sessionId, code} │
│ │<─────────────────────│
│ │── 再次code2Session │
│ │── 验证openid一致性 │
│ │── 签发JWT Token │
│ │── 更新Session(confirmed)│
│ │ │
│ GET /api/status │ │
│─────────────────────>│ │
│ 返回status=confirmed│ │
│ + userInfo + token │ │
│<─────────────────────│ │
│ │ │
│ 登录完成 │ │
└──────────────────────┘ │

关键设计细节

1. 二维码生成:小程序码而非普通二维码

项目使用微信的 getwxacodeunlimit 接口生成小程序码,而非普通的二维码图片。小程序码的优势在于:

  • 用户扫码后直接打开小程序的指定页面
  • 可以通过 scene 参数传递会话 ID
  • 支持指定小程序版本(开发版/体验版/正式版),方便开发调试

后端在生成小程序码时,会将 sessionId 去除连字符后作为 scene 参数传递给微信 API。小程序打开后会从 scene 参数中解析出 sessionId,从而建立起与 Web 端登录会话的关联。

2. Access Token 管理

调用微信 API 生成小程序码需要 access_token。项目实现了一个 Token 存储抽象层,支持两种实现:

  • 内存存储:用于开发和测试环境,简单直接
  • 数据库存储:用于生产环境,支持多实例部署

Token 缓存采用了"提前过期"策略——在微信返回的过期时间基础上减去 300 秒作为实际缓存时间,避免在 Token 真正过期和请求新 Token 之间的空窗期出现问题。

3. 双重身份验证:为什么扫码和确认要各自调用一次 code2Session

这是整个流程中一个容易被忽视但至关重要的安全设计。

当用户在小程序中扫码后,小程序调用 wx.login() 获取一个 code,后端通过 code2Session 接口换取用户的 openid。在确认登录时,小程序会再次调用 wx.login() 获取一个新的 code,后端再次调用 code2Session 换取 openid,并与扫码时记录的 openid 进行比对。

这个设计的目的是:

  • 确保是同一用户操作:防止扫码用户 A 和确认用户 B 不一致的情况
  • 微信 code 的一次性:每个 code 只能使用一次,两次操作使用不同的 code 增加了安全性

4. 会话存储:内存与数据库的双模式

项目通过适配器模式,将内存存储和数据库存储统一为相同的接口。这使得上层路由代码无需关心底层存储实现:

  • 开发/测试环境:使用内存 Map 存储,零依赖,启动即用
  • 生产环境:使用 PostgreSQL + Drizzle ORM,支持持久化和多实例水平扩展

统一接口通过 UnifiedSessionStore 实现,内部对内存存储(同步操作)和数据库存储(异步操作)进行了适配包装。

5. JWT Token 签发

登录确认成功后,后端使用 jose 库签发 JWT Token。选择 jose 而非 jsonwebtoken 的原因是它对 Edge Runtime 和 Cloudflare Workers 等环境有更好的兼容性。

Token 的载荷包含 openidunionid,有效期默认为 7 天。Web 端轮询到 confirmed 状态时,会同时获得用户信息和 Token,后续请求携带 Token 即可完成身份认证。

6. 多环境支持

项目通过 NODE_ENV 区分三种运行环境,每种环境有不同的服务组合:

环境会话存储微信服务Token 存储
test内存真实(mock fetch)内存
development内存真实微信 API内存
productionPostgreSQL真实微信 APIPostgreSQL

测试环境中,全局 fetch 函数会被 mock 拦截,这样不需要真正调用微信 API 就能测试完整的业务逻辑。

API 设计概览

后端共暴露 6 个核心端点:

Web 端使用的接口:

  • POST /api/qrcode — 创建登录会话并生成小程序码
  • GET /api/status/:sessionId — 轮询查询登录状态

小程序端使用的接口:

  • POST /api/scan — 扫码绑定用户身份
  • POST /api/confirm — 确认登录
  • POST /api/cancel — 取消登录

系统接口:

  • GET /health — 健康检查

所有接口的请求和响应都通过 Zod Schema 进行校验,配合 hono-openapi 自动生成文档。统一的响应格式让前端可以用一致的方式处理成功和错误。

错误处理

项目定义了一套结构化的错误码体系,覆盖会话错误、微信 API 错误和通用错误三大类。微信 API 返回的原始错误码会被映射为业务错误码,前端无需关心微信侧的具体错误含义,只需要处理预定义的业务错误类型即可。

总结

微信小程序扫码登录看似简单——生成二维码、扫码、确认——但其背后涉及会话管理、状态机、Token 缓存、身份验证、JWT 签发等多个技术环节。本项目通过清晰的分层架构、严格的类型定义和统一的接口抽象,将复杂的登录流程拆解为可维护、可测试的独立模块。

几个值得借鉴的设计决策:

  1. 状态机驱动:用明确的状态转移规则保证流程的安全性
  2. 适配器模式:统一内存和数据库两种存储,简化上层逻辑
  3. 双重身份验证:扫码和确认分别换取 openid 并比对,防止身份不一致
  4. Token 提前过期:在缓存中提前淘汰快过期的 Token,避免边界问题
  5. 环境隔离:通过配置驱动实现开发、测试、生产环境的灵活切换

这些设计不仅适用于扫码登录场景,对于其他类似的"多端协作 + 状态流转"类功能同样有参考价值。