Skip to content

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)

示例 ​

以下代码针对本地桩服务编译并运行过(桩服务部分省略):

go
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 ​

字段类型说明
AppIDstring移动应用的 AppID,必填
AppSecretstring应用密钥,必填
Endpointstring覆盖接口地址(测试桩、代理);留空用 DefaultEndpoint。必须是 http/https、有主机名、不带查询串,否则 ErrUnavailable
HTTPClient*http.Client自定义客户端;留空时使用内建客户端
Timeouttime.Duration内建客户端的超时,≤ 0 时为 10 秒;提供了 HTTPClient 时忽略

内建客户端不跟随重定向:接口返回 3xx 会被当作异常响应(ErrMalformedResponse)。自带 HTTPClient 时建议同样设置 CheckRedirect 返回 http.ErrUseLastResponse,避免带着密钥的请求被转发到别处。

身份投影与降级标注 ​

go
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)。

哨兵触发条件
ErrInvalidCredentialcode 为空(不发请求直接拒绝);返回码 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。

相关阅读 ​

以 Apache License 2.0 发布 · 文档内容 © 2026 Wicket 作者