wechatverify
github.com/gonewx/wicket/wechatverify 在后端用授权 code 向微信换取用户标识。App 端通过微信 SDK 完成授权后把 code 发给你的后端,后端调用本包得到跨应用稳定的 unionid(拿不到时回落为 openid)。
联调状态
真实的微信开放平台凭据尚未到位,真实链路联调尚未完成。当前的验证覆盖:针对桩服务(stub)的请求/响应对拍、错误分类、密钥脱敏,以及“未配置凭据即不可用”的语义。上线前请用你自己的凭据做一次端到端验证。
基本信息
| 项 | 值 |
|---|---|
| 调用的接口 | GET https://api.weixin.qq.com/sns/oauth2/access_token(常量 DefaultEndpoint) |
| 适用产品 | 移动应用的微信登录。小程序的登录接口是另一套产品与响应格式,本包不支持 |
| 依赖 | 仅 Go 标准库 |
| 入口 | NewVerifier(Config) (Verifier, error);Verifier.Verify(ctx, code) (*VerifiedIdentity, error) |
示例
以下代码针对本地桩服务编译并运行过(桩服务部分省略):
import (
"context"
"errors"
"net/http"
"os"
"github.com/gonewx/wicket/wechatverify"
)
func newWeChat() (wechatverify.Verifier, error) {
return wechatverify.NewVerifier(wechatverify.Config{
AppID: os.Getenv("WECHAT_APP_ID"),
AppSecret: os.Getenv("WECHAT_APP_SECRET"),
})
}
func handleWeChatLogin(v wechatverify.Verifier, w http.ResponseWriter, r *http.Request) {
id, err := v.Verify(r.Context(), r.FormValue("code"))
switch {
case err == nil:
if id.SubjectSource == wechatverify.SubjectFromOpenID {
// 降级:只拿到了本应用内的 openid,按业务策略决定是否接受
}
_ = id.Subject // 用作外部账号键,查找或创建本地用户
w.WriteHeader(http.StatusOK)
case errors.Is(err, wechatverify.ErrInvalidCredential):
http.Error(w, "invalid code", http.StatusUnauthorized)
case errors.Is(err, wechatverify.ErrRetryable), errors.Is(err, wechatverify.ErrUnavailable):
// 都表示“现在无法判定”;ErrUnavailable 需要告警
http.Error(w, "try again later", http.StatusServiceUnavailable)
case errors.Is(err, context.Canceled):
// 客户端已放弃
default: // ErrMalformedResponse
http.Error(w, "upstream error", http.StatusBadGateway)
}
}NewVerifier 在缺少 AppID 或 AppSecret 时返回 ErrUnavailable。建议启动时就构造:凭据缺失时整个能力保持关闭(对外返回 503),而不是在第一个用户登录时才失败。
配置:Config
| 字段 | 类型 | 说明 |
|---|---|---|
AppID | string | 移动应用的 AppID,必填 |
AppSecret | string | 应用密钥,必填 |
Endpoint | string | 覆盖接口地址(测试桩、代理);留空用 DefaultEndpoint。必须是 http/https、有主机名、不带查询串,否则 ErrUnavailable |
HTTPClient | *http.Client | 自定义客户端;留空时使用内建客户端 |
Timeout | time.Duration | 内建客户端的超时,≤ 0 时为 10 秒;提供了 HTTPClient 时忽略 |
内建客户端不跟随重定向:接口返回 3xx 会被当作异常响应(ErrMalformedResponse)。自带 HTTPClient 时建议同样设置 CheckRedirect 返回 http.ErrUseLastResponse,避免带着密钥的请求被转发到别处。
身份投影与降级标注
type VerifiedIdentity struct {
Subject string // 优先 unionid,缺失时为 openid
SubjectSource SubjectSource // SubjectFromUnionID("unionid")或 SubjectFromOpenID("openid")
UnionID string // 原值,可能为空
OpenID string // 原值,成功时一定非空
Scope string // 用户授权的 scope,原样透传
}- unionid 只有在应用已绑定到微信开放平台账号、且满足微信的下发条件时才会返回。缺少 unionid 是正常情况,不是错误。
- 此时
Subject回落为 openid,SubjectSource为SubjectFromOpenID。这是一个降级标注:openid 只在当前应用内稳定,同一个人在你的另一个应用里会是另一个 openid。若你的账号体系跨多个应用,应据此拒绝登录或引导用户完成绑定,而不是直接建号。 - 成功响应必须带 openid;接口声称成功却没有 openid,按
ErrMalformedResponse拒绝,即使有 unionid 也不签发身份。 - 交换得到的微信
access_token会被解析但不使用、也不对外暴露;是否成功只看errcode是否为 0 以及 openid 是否存在。
错误类别
四个哨兵用 errors.Is 判断;需要微信原始返回码时用 errors.As 取 *VendorError(字段 Code int、Msg string)。
| 哨兵 | 触发条件 |
|---|---|
ErrInvalidCredential | code 为空(不发请求直接拒绝);返回码 40029(code 无效)、40163(code 已使用)、41008(缺少 code)、40013(AppID 无效)、40125(密钥无效)、40001(凭据不匹配);以及任何未识别的非零返回码 |
ErrRetryable | 返回码 -1(系统繁忙)、45011(频率超限);HTTP 5xx、429、408;网络传输失败;读取响应体失败;ctx 到期 |
ErrMalformedResponse | 其他非 2xx 状态(含 3xx);响应体不是合法 JSON;返回码为 0 但没有 openid |
ErrUnavailable | 未配置 AppID / AppSecret;Endpoint 不合法;请求无法构造 |
说明:
- 微信在 HTTP 200 下同时用于成功与失败,只有非零
errcode表示失败,本包已处理,调用方只需看错误。 - 应用侧配置错误(40013、40125、40001)在本包中也归入
ErrInvalidCredential,与用户 code 无效不做区分。如需区分以便告警,可用errors.As取VendorError.Code自行判断。 ctx被取消时,错误满足errors.Is(err, context.Canceled),不满足ErrRetryable;ctx到期时同时满足context.DeadlineExceeded与ErrRetryable。- 响应体最多读取 1 MiB,错误中引用的响应体片段最多 256 字节。
密钥不外泄
密钥是以查询参数随请求发出的,因此一旦网络失败,Go 返回的错误里会带着完整 URL。本包对所有可能到达调用方的文本做了脱敏:
- 错误信息中的密钥替换为
[REDACTED],原文与 URL 编码后的形式都会被匹配。实测传输失败时的错误:... exchange request failed: Get "http://127.0.0.1:1?appid=wx&code=c&grant_type=authorization_code&secret=[REDACTED]": ... connection refused。 VendorError.Msg同样经过脱敏。Config实现了String()、MarshalJSON()与slog.LogValuer:fmt的%v/%+v/%s、json.Marshal、slog输出中密钥均为[REDACTED];反序列化不受影响,仍可从 JSON 读入配置。- 验证器实例的
%v输出同样脱敏。
两个保护范围之外的口子
- v0.1.4 中
%#v格式不在上述脱敏范围内。请不要用%#v输出或记录配置。 - 你自己把
cfg.AppSecret字段单独取出来打印,本包无从保护。
本包自身不写任何日志。
与容器的关系
容器注册了一个空的 *wechatverify.Config 单例(root.Ptr_Config())作为配置扩展点,但不注册 Verifier——空配置构造必然失败。最简单的做法是直接在你的初始化代码里构造验证器。
如果你选择把凭据写进容器里的这个共享指针,请只在装配阶段、开始处理请求之前写入:它被所有作用域共享,请求进行中写入会与读取方产生数据竞争;若此时把某个字段清空,下一次构造会静默地变成 ErrUnavailable。