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) |
示例
以下代码针对本地桩服务编译并运行过(桩服务部分省略):
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 那一侧的认证方案。仍然要求必填,是为了让“身份配置不完整”的部署在启动时就被整体拒绝,而不是在某个恰好被校验到的环节才失败。
成功结果
type VerifiedPhone struct {
Mobile string // 手机号,成功时非空;原样透传,不做格式转换
RequestID string // 阿里云请求 ID,用于对账和工单排查
}错误类别
四个哨兵用 errors.Is 判断;厂商的原始返回码、消息、请求 ID 与 HTTP 状态用 errors.As 取 *VendorError(字段 Code、Message、RequestID、HTTPStatus),这些字段都已脱敏并限长。
| 哨兵 | 触发条件 |
|---|---|
ErrInvalidCredential | token 为空或纯空白(不发请求);返回码 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。