Skip to content

phoneverify ​

github.com/gonewx/wicket/phoneverify 在后端用号码认证 token 向阿里云换取本机手机号。App 端通过号码认证移动端 SDK 完成“一键登录”,把得到的 token 发给你的后端,后端调用本包得到手机号。

联调状态

阿里云号码认证的真实凭据尚未到位,真实链路联调尚未完成。当前的验证覆盖:针对桩服务(stub)的请求/响应对拍、错误分类、机密脱敏、并发安全,以及“未配置凭据即不可用”的语义。

另有一项待确认:阿里云文档示例中的手机号是掩码形态,真实返回是完整号码还是掩码尚未核实。本包对号码不做任何转换,原样透传,请在你的联调中确认后再决定是否能直接用作账号键。

基本信息 ​

项值
调用的接口号码认证服务 GetMobile 操作,默认端点 https://dypnsapi.aliyuncs.com(常量 DefaultEndpoint)
签名地域默认 cn-hangzhou(常量 DefaultRegionID)
依赖阿里云 Go SDK(github.com/aliyun/alibaba-cloud-sdk-go 的号码认证服务与错误包)
入口NewVerifier(PhoneAuthConfig) (Verifier, error);Verifier.Verify(ctx, token) (*VerifiedPhone, error)

示例 ​

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

go
import (
	"context"
	"errors"
	"log/slog"
	"net/http"
	"os"

	"github.com/gonewx/wicket/phoneverify"
)

func newPhoneAuth() (phoneverify.Verifier, error) {
	return phoneverify.NewVerifier(phoneverify.PhoneAuthConfig{
		AppKey:          os.Getenv("PHONE_AUTH_APP_KEY"),
		AccessKeyID:     os.Getenv("PHONE_AUTH_ACCESS_KEY_ID"),
		AccessKeySecret: os.Getenv("PHONE_AUTH_ACCESS_KEY_SECRET"),
	})
}

func handleOneTapLogin(v phoneverify.Verifier, logger *slog.Logger, w http.ResponseWriter, r *http.Request) {
	phone, err := v.Verify(r.Context(), r.FormValue("token"))

	var vendorErr *phoneverify.VendorError
	if errors.As(err, &vendorErr) {
		// 已脱敏、已限长,可以直接记录,便于对账和提工单
		logger.Warn("phone auth rejected", "code", vendorErr.Code,
			"request_id", vendorErr.RequestID, "status", vendorErr.HTTPStatus)
	}

	switch {
	case err == nil:
		_ = phone.Mobile // 按手机号查找或创建本地用户
		w.WriteHeader(http.StatusOK)
	case errors.Is(err, phoneverify.ErrInvalidCredential):
		http.Error(w, "invalid token", http.StatusUnauthorized)
	case errors.Is(err, phoneverify.ErrRetryable), errors.Is(err, phoneverify.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)
	}
}

配置:PhoneAuthConfig ​

字段说明
AppKey控制台中认证方案的标识。必填
AccessKeyID访问密钥 ID。必填
AccessKeySecret访问密钥。必填
RegionID签名所用地域;留空为 cn-hangzhou
Endpoint完整 URL 形式的端点覆盖(测试桩、专有网络);留空为 DefaultEndpoint。只能是 http(s)://主机[:端口],带路径、查询串、片段或用户信息都会被拒绝(ErrUnavailable)
Timeout单次交换的读与连接超时;≤ 0 时为 10 秒

三个必填项任一为空(含纯空白),NewVerifier 返回 ErrUnavailable。

AppKey 实际上不会随请求发出——GetMobile 操作没有这个参数,它标识的是移动端 SDK 那一侧的认证方案。仍然要求必填,是为了让“身份配置不完整”的部署在启动时就被整体拒绝,而不是在某个恰好被校验到的环节才失败。

成功结果 ​

go
type VerifiedPhone struct {
	Mobile    string // 手机号,成功时非空;原样透传,不做格式转换
	RequestID string // 阿里云请求 ID,用于对账和工单排查
}

错误类别 ​

四个哨兵用 errors.Is 判断;厂商的原始返回码、消息、请求 ID 与 HTTP 状态用 errors.As 取 *VendorError(字段 Code、Message、RequestID、HTTPStatus),这些字段都已脱敏并限长。

哨兵触发条件
ErrInvalidCredentialtoken 为空或纯空白(不发请求);返回码 isv.TOKEN_INVALID、isv.TOKEN_UNAUTHORIZED_USED、InvalidBizToken.Expired、InvalidBizToken.Check、GET_MOBILE_FAILED;任何未识别的返回码(例外见下)
ErrRetryable返回码 isp.QPS_LIMIT、isv.BUSINESS_LIMIT_CONTROL、isp.SYSTEM_ERROR、isp.UNKNOWN;未识别返回码随 HTTP 5xx / 429 / 408 到达;网络传输失败;SDK 超时;ctx 到期
ErrMalformedResponse响应无法解码(包括 SDK 解码时 panic,已被捕获);没有返回码;返回码为 OK 但手机号为空
ErrUnavailable未配置必填项;Endpoint 不合法;SDK 拒绝构造或发出请求;以及本方配置类返回码:isv.RAM_PERMISSION_DENY、isv.FORBIDDEN_ACTION、isv.PRODUCT_UNSUBSCRIBE、isv.VERIFY_SCHEME_NOT_EXIST、SignatureDoesNotMatch、IncompleteSignature

几条设计取舍:

  • 配置类返回码归 ErrUnavailable。权限不足、产品未开通、签名不匹配说的是“本方凭据或开通状态有问题”,与“用户的 token 无效”是两回事。若归为无效凭据,你会对一个完全正常的用户返回 401,把运维故障藏在用户身上。归为不可用依然不放行,但原因指对了。(wechatverify 没有做这一区分,因为微信侧缺少同样明确的码表。)
  • 返回码按字符串全等匹配,不按 isv. 等前缀归类:同一前缀下同时存在凭据类与配置类的码。
  • 未识别的码默认收紧为 ErrInvalidCredential;唯一放宽是它随 5xx / 429 / 408 到达时按 ErrRetryable 处理,因为此时状态码表达的是厂商侧故障。
  • 业务失败以 HTTP 200 送达。阿里云用 200 + 非 OK 返回码表示失败,本包检查返回码后才给出结果;如果你绕过本包直接调用 SDK,“没有 error 就是成功”是最容易踩的坑。
  • 厂商的明确答复优先于上下文:拿到了厂商的错误响应后,即使 deadline 恰好在此刻到期,也按厂商答复分类,避免把一个已经用掉的 token 当成可重试而重复提交。没有厂商答复时,ctx 取消满足 errors.Is(err, context.Canceled) 且不可重试;ctx 到期同时满足 context.DeadlineExceeded 与 ErrRetryable。调用 Verify 时 ctx 已经结束的,直接按上述规则返回,不做任何工作。

并发与超时 ​

  • Verifier 可被任意多个 goroutine 并发使用。
  • SDK 的客户端会在每次请求时改写自身的超时与传输层,共享一个客户端在并发下会产生数据竞争。因此本包每次调用新建一个客户端,用完即关闭;这一开销远小于随后的网络往返。
  • SDK 的 GetMobile 不接受 context。本包通过自定义传输层把调用方的 ctx 接入请求,使取消能真正中断在途的请求,而不是等到传输超时。
  • 超时由两层共同约束:PhoneAuthConfig.Timeout(默认 10 秒)与调用方 ctx 的 deadline,先到者生效。

机密不外泄 ​

请求签名是 HMAC,AccessKeySecret 本身不会上网;但签名后的请求 URL 含有 AccessKeyId、用户的 token 与签名值,而传输失败时 SDK 的错误会带上完整 URL。本包的处理:

  • 错误文本中的 AccessKeySecret、AccessKeyID 与当次 token 一律替换为 [REDACTED],同时匹配原文、URL 编码一次、编码两次(签名回显)三种形态;逐请求变化的 Signature、SignatureNonce 按参数名剥除。脱敏总是在截断之前进行,避免机密被截成匹配不上的残片。
  • PhoneAuthConfig 实现了 String()、GoString()、MarshalJSON() 与 slog.LogValuer:%v、%+v、%#v、json.Marshal、slog 输出中 AccessKeySecret 均为 [REDACTED]。反序列化不受影响。
  • 口径上的刻意不对称:配置的字符串形式保留 AccessKeyID 明文(它是控制台可见的账号标识,便于运维辨认);错误文本中则剥除它,因为在那里它与签名同处于可重放的 URL 中。
  • 本包不写日志,并对每个 SDK 客户端关闭了链路追踪——SDK 检测到进程内全局 tracer 时会用完整请求 URI 命名 span,那会绕过上述脱敏。

保护范围之外

  • SDK 自带的 DEBUG=sdk 环境变量会把完整请求打印到标准输出,生产环境不要设置。
  • 你自己单独取出 AccessKeySecret 字段打印,本包无从保护。

与容器的关系 ​

容器注册了一个空的 *phoneverify.PhoneAuthConfig 单例(root.Ptr_PhoneAuthConfig())作为配置扩展点,但不注册 Verifier。直接在初始化代码里构造验证器即可。若要使用这个共享指针,只能在装配阶段、处理请求之前写入;请求进行中写入会产生数据竞争,清空字段会让后续构造静默变成 ErrUnavailable。

相关阅读 ​

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