实现面向个人客户的 7 个认证接口(A1-A7),覆盖资产验证、 微信公众号/小程序登录、手机号绑定/换绑、退出登录完整流程。 主要变更: - 新增 PersonalCustomerOpenID 模型,支持多 AppID 多 OpenID 管理 - 实现有状态 JWT(JWT + Redis 双重校验),支持服务端主动失效 - 扩展微信 SDK:小程序 Code2Session + 3 个 DB 动态工厂函数 - 实现 A1 资产验证 IP 限流(30/min)和 A4 三层验证码限流 - 新增 7 个错误码(1180-1186)和 6 个 Redis Key 函数 - 注册 /api/c/v1/auth/* 下 7 个端点并更新 OpenAPI 文档 - 数据库迁移 000083:新建 tb_personal_customer_openid 表
8.4 KiB
8.4 KiB
client-auth-system 设计文档
Context
当前个人客户认证状态如下:
- 个人客户当前使用纯 JWT(
pkg/auth/jwt.go),未做 Redis token 状态存储,服务端无法主动失效。 - 微信配置已在数据库
tb_wechat_config中存在(WechatConfig),但现有能力中仍存在 YAML 静态配置依赖。 - 个人客户相关模型已存在:
PersonalCustomer、PersonalCustomerPhone、PersonalCustomerDevice。 - 现有
/api/c/v1路由将被本次完整认证体系替换为新的/api/c/v1/auth/*七个端点。
Goals / Non-Goals
Goals
- 交付完整 C 端认证系统,覆盖 A1~A7 七个接口:资产验证、微信登录(公众号/小程序)、验证码发送、手机号绑定、手机号换绑、退出登录。
- 建立有状态 JWT(JWT + Redis)机制,支持服务端主动失效。
- 建立 OpenID 多记录模型,兼容公众号与小程序不同 AppID 场景。
Non-Goals
- 不实现业务域 API(如充值、套餐、订单等)。
- 不包含兑换系统(exchange)相关设计与实现。
Decisions
1) asset_token 设计
- 方案:
asset_token使用短时效 JWT(5 分钟),payload 仅包含asset_type+asset_id,并使用独立于主登录 JWT 的签名密钥。 - Why:A1 是无认证接口,若直接暴露内部
asset_id会造成可枚举风险;短时效 + 独立密钥可降低 token 泄露影响范围。
2) Stateful JWT with Redis
- 方案:登录成功签发 JWT 后,将 token 状态写入 Redis 并设置 TTL;每次请求在中间件同时校验 JWT 与 Redis 状态。
- Redis Key:
RedisPersonalCustomerTokenKey(customerID)。 - Why:纯 JWT 无法服务端撤销;Redis 状态可支持封禁、强制下线、单点退出等主动失效场景。
3) OpenID multi-record strategy
- 方案:新增
PersonalCustomerOpenID表,约束UNIQUE(app_id, open_id)(软删条件下唯一);客户查找逻辑采用:- 先按
(app_id, open_id)精确命中; - 未命中时按
unionid回查并合并; - 仍未命中则创建新客户。
- 先按
- Why:公众号与小程序可能不在同一开放平台,需支持“一客户多 OpenID 记录”。
4) WechatConfig dynamic loading + SDK 实例工厂
- 方案:登录时动态读取
tb_wechat_config WHERE is_active=true,使用工厂函数按需创建 SDK 实例:- OfficialAccount 使用
oa_app_id + oa_app_secret - Miniapp 使用
miniapp_app_id + miniapp_app_secret - Payment 使用
wx_mch_id + wx_api_v3_key + wx_cert_content + wx_key_content + wx_serial_no
- OfficialAccount 使用
- Why:避免 YAML 静态配置导致多环境切换和配置漂移,支持运营侧动态切换。
现有 SDK 能力盘点(pkg/wechat/):
| 文件 | 已有能力 | 客户端接口需要 |
|---|---|---|
official_account.go |
GetUserInfo(code)(snsapi_base)、GetUserInfoDetailed(code)(snsapi_userinfo)、GetUserInfoByToken() |
A2 公众号登录 ✅ 直接复用 GetUserInfoDetailed |
payment.go |
CreateJSAPIOrder()、CreateH5Order()、QueryOrder()、CloseOrder()、HandlePaymentNotify() |
提案 2 支付 ✅ |
config.go |
NewOfficialAccountApp(cfg) — 仅从 YAML 创建 |
❌ 需新增 DB 动态工厂 |
缺失 miniapp.go |
无 | ❌ A3 小程序登录需要 |
需要新增的 SDK 代码:
pkg/wechat/miniapp.go— 小程序服务封装:
// MiniAppService 微信小程序服务实现
type MiniAppService struct {
appID string
appSecret string
logger *zap.Logger
}
// MiniAppServiceInterface 微信小程序服务接口
type MiniAppServiceInterface interface {
Code2Session(ctx context.Context, code string) (openID, unionID, sessionKey string, err error)
}
// Code2Session 通过小程序 login code 换取 openid + session_key
// 调用微信 https://api.weixin.qq.com/sns/jscode2session 接口
// 注意: 小程序无法通过 code 直接获取用户信息(昵称/头像由前端授权后传入)
func (s *MiniAppService) Code2Session(ctx context.Context, code string) (openID, unionID, sessionKey string, err error)
pkg/wechat/config.go— 新增 DB 动态工厂函数:
// NewOfficialAccountAppFromConfig 从 WechatConfig DB 记录创建公众号应用实例
func NewOfficialAccountAppFromConfig(wechatConfig *model.WechatConfig, cache kernel.CacheInterface, logger *zap.Logger) (*officialAccount.OfficialAccount, error)
// NewPaymentAppFromConfig 从 WechatConfig DB 记录创建支付应用实例
// appID 参数决定支付关联的应用:公众号传 oa_app_id,小程序传 miniapp_app_id
func NewPaymentAppFromConfig(wechatConfig *model.WechatConfig, appID string, cache kernel.CacheInterface, logger *zap.Logger) (*payment.Payment, error)
// NewMiniAppServiceFromConfig 从 WechatConfig DB 记录创建小程序服务实例
func NewMiniAppServiceFromConfig(wechatConfig *model.WechatConfig, logger *zap.Logger) (*MiniAppService, error)
pkg/wechat/wechat.go— 新增MiniAppServiceInterface接口定义和编译时类型检查。
A2/A3 登录时 SDK 调用链路:
A2 公众号登录:
client_auth.Service
→ wechatConfigService.GetActiveConfig() // 从 DB/Redis 缓存获取配置
→ wechat.NewOfficialAccountAppFromConfig(config) // 动态创建公众号实例
→ wechat.NewOfficialAccountService(app) // 包装为 Service
→ officialAccountService.GetUserInfoDetailed(code) // 现有方法,直接复用
→ 返回 openID + unionID + nickname + avatar
A3 小程序登录:
client_auth.Service
→ wechatConfigService.GetActiveConfig()
→ wechat.NewMiniAppServiceFromConfig(config) // 新增方法
→ miniAppService.Code2Session(code) // 新增方法
→ 返回 openID + unionID + sessionKey
→ nickname/avatar 从请求体获取(前端授权后传入)
关键约束:小程序 Code2Session 不调用 PowerWeChat SDK(该 SDK 主要封装公众号和支付),而是直接 HTTP 请求微信 jscode2session 接口。这更简单可控。
5) Phone binding config
- 方案:手机号绑定策略使用 Viper 配置项
client.require_phone_binding(bool),在登录时实时读取,不新增 DB 配置表。 - Why:该策略属于部署级开关,配置中心化更轻量,减少数据库复杂度。
6) Asset binding on login
- 方案:每次登录都创建
PersonalCustomerDevice绑定记录;同一资产允许被多个客户绑定,不做覆盖写入。 - Why:业务上存在转手、共用、历史归属追踪需求,强唯一会丢失使用关系。
7) Rate limiting strategy
- A1:IP 级限频
30/min。 - A4:手机号维度
60s冷却 + IP 维度20/hour+ 手机号维度10/day。 - Why:A1 主要防资产暴力枚举;A4 主要防短信轰炸与资源滥用,采用多维限流降低绕过概率。
Risks / Trade-offs
-
Redis 强依赖风险
- 风险:Redis 异常会导致 token 校验失败、登录态不可用。
- 缓解:中间件区分“无效 token”与“Redis 不可用”并记录告警;部署 Redis 高可用;关键路径加入超时与重试上限。
-
OpenID 合并误关联风险
- 风险:若第三方返回异常 unionid,可能出现错误合并。
- 缓解:仅在 unionid 非空且满足格式校验时启用回退合并;记录合并审计日志(customer_id、app_id、openid、unionid)。
-
资产多人绑定带来的业务歧义
- 风险:后续业务查询若默认“单资产单用户”,可能读取歧义。
- 缓解:规范下游以“当前登录 customer_id + asset”联合查询;在文档中明确“资产可多客户绑定”语义。
-
动态微信配置切换风险
- 风险:运营误切换
is_active导致登录瞬时失败。 - 缓解:限制仅单条激活、增加配置健康检查与缓存短 TTL、错误回退到最近一次可用配置。
- 风险:运营误切换
Migration Plan
-
数据库迁移
- 新增
tb_personal_customer_openid表(含customer_id/app_id/open_id/union_id等字段)。 - 创建唯一索引:
UNIQUE(app_id, open_id) WHERE deleted_at IS NULL。
- 新增
-
配置更新
- 在
pkg/config/defaults/config.yaml增加:client.require_phone_binding: true|false
- 在
-
灰度切换顺序
- 先上线迁移与新配置;
- 再上线新认证接口与中间件增强;
- 最后切换前端调用到
/api/c/v1/auth/*。