主题
核心 API 与业务链路
外部世界只通过 11 个 URL 触碰这套系统:
get发题、check交卷、verify由客户后端复核,其余入口都是旁路(配置、采集、短信上行、移动页)。
1. 协议入口清单
所有 Controller 都继承 AbstractController,复用其 JSONP 输出、参数容错、callback/token 白名单校验与域名限制(captcha-server/src/main/java/com/netease/is/nc/server/abst/AbstractController.java:59)。
| Controller | 路径 | 方法 | 用途 | 行号 |
|---|---|---|---|---|
GetControllerV2 | /api/v2/mp/get | GET | 小程序取图,直接返回 JSON(不走 JSONP) | GetControllerV2.java:55 |
GetControllerV2 | /api/v3/get | GET | Web/H5 取图,JSONP 输出 | GetControllerV2.java:95 |
CheckControllerV2 | /api/v3/mp/check | POST | 小程序交卷,JSON | CheckControllerV2.java:50 |
CheckControllerV2 | /api/v3/check | GET | Web 交卷,JSONP | CheckControllerV2.java:80 |
VerifyControllerV2 | /api/v2/verify | POST | 客户后端二次校验 | VerifyControllerV2.java:55 |
GetConfController | /api/v2/mp/getconf | GET | 小程序 SDK 资源与配置 | GetConfController.java:92 |
GetConfController | /api/v2/getconf | GET | Web SDK 资源与配置 | GetConfController.java:129 |
MobileControllerV2 | /api/v2/mobile.html | GET | 移动端接入页(渲染 FreeMarker 视图) | MobileControllerV2.java:36 |
SdkCollectController | /api/v2/mp/collect | POST | 小程序反作弊数据采集 | SdkCollectController.java:51 |
SdkCollectController | /api/v2/collect | GET | Web 反作弊数据采集 | SdkCollectController.java:66 |
UplinkSMSController | /api/v1/sms | GET/POST | 运营商上行短信回调 | UplinkSMSController.java:46 |
路径版本号不统一的坑:取图与校验在 /api/v3/*,配置却是 /api/v2/*,移动页又在 /api/v2/mobile.html。/api/v2/mobile.html 这个字面量不能改——AbstractController.isFromSDK() 用正则 ^https?://[^/]+/api/v2/mobile.html 反推「请求是否来自 SDK」(AbstractController.java:85、AbstractController.java:361),改路径会连带影响 smart 开关与固定验证码类型策略。
2. 取图链路(/api/v3/get)
text
client ──GET /api/v3/get?callback=cb&id=xxx&token=&type=&width=320&runEnv=10&dpr=2──┐
▼
① callback 白名单 isValidCallback ^[a-zA-Z0-9_]{1,64}$ (AbstractController:376)
② captchaId 合法性 appCacheService.isValidBusiness (AppCacheService:125)
③ 参数解析+校验 GetControllerV2.validate(request,false) (GetControllerV2:149)
④ referer 域名段 isDomainAllowAccess(runEnv, referer) (AbstractController:403)
⑤ 子版本解析 parseSubVersion (AbstractController:452)
⑥ GetCaptchaService.getCaptchaImg (GetCaptchaService:98)
├─ 卓望 IP 富化(非短信) enrichZhuoWangIpInfo (:151)
├─ 决定类型 getCaptchaType(switchOver/高频切换/指定) (:269)
├─ License 校验 licenseService.canUseCaptcha(type,enh) (:103)
├─ 按类型取题 getCaptchaImgByType (:219)
├─ token 复用(旧 smart token 仍有效则沿用) (:172)
├─ 写会话 Redis captchaRedisService.save (:122)
├─ jsdk 版本上下文 existSdkVersionContext (:135)
├─ 行为日志 logAction → ES (:464)
└─ 安全模式上下文 saveGetContext(KEY: pri:ctx:{token})(:209)
⑦ convert 拼装返回 token/bg/front/type (GetControllerV2:261)
⑧ Set-Cookie P3P + track cookie,JSONP 回包参数校验的硬条件(GetControllerV2.java:190-217):captchaId 非空、App 存在、app.isVersion2()、token 匹配 ^[a-z0-9]{32}$ 或为空(前端传字面量 undefined 会被归一成空串,GetControllerV2.java:197)、runEnv 合法、audio=true 时必须 License 支持语音且 App 开启无障碍;小程序与 Web 的 runEnv 互斥校验。
2.1 类型决策优先级
getCaptchaType 的判定顺序(GetCaptchaService.java:269):audio → VOICE;前端指定 type 且该产品在白名单(getSpecifiedCaptchaTypePids)且未被系统级智能切换开关覆盖 → 用指定值;否则按 app.switchOver 走 AUTO(调 SwitchOverService)/RANDOM/FIX_*(固定)/FIXED_SMART(先判高频再随机降级)。
2.2 返回结构与取图
get 不返回图片字节,只返回可拼接的密钥与地址(GetControllerV2.java:261):
| 字段 | 含义 | 生成规则 |
|---|---|---|
token | 会话标识,32 位小写十六进制 | 题面生成时产出 |
bg | 背景图地址数组(两元素,兼容旧客户端) | image.download.server + /image/{key} + dpr 后缀(AbstractController.java:347) |
front | 前景信息:拖动=前景图 URL,点击=待点文字 | 同上;AVOID 强制 .png |
type | 服务端最终决定的类型值 | CaptchaType.getValue() |
attrs | 仅增强版拖动返回,角速度标量 | 由答案与真实宽度反算(GetControllerV2.java:284-295) |
地址前缀取 image.download.server,本仓库默认配的是 S3 端点(captcha-base/src/main/resources/base.properties:21 → https://s3.ap-east-1.amazonaws.com/yiduncaptchatest),也可被 sys_conf 中按 captchaId 配置的图片域名覆盖(AbstractController.java:349)。图片字节由 captcha-image 模块(本地磁盘 upload_file_path)或 S3UploadService 承载,与 captcha-server 解耦。
getConf 链路另有一套启动期硬校验:apiServer / splitStaticServers 为空直接抛异常导致启动失败(GetConfController.java:82),并签发/校验设备令牌 dt(TimeExtendSignId.genId / verifyId,GetConfController.java:207-217)。
3. 校验链路(/api/v3/check)
text
GET /api/v3/check?id&token&type&data&width&runEnv&dt&user
① type 合法性(CaptchaType.valueOf) (CheckControllerV2:126)
② trace 解密:iv>=1 走 TraceUtilV2.getTraceIv1 否则 getTrace (CheckControllerV2:246)
③ dt 验签:TimeExtendSignId.verifyId(dt, DEVICE_TOKEN_SALT) (CheckControllerV2:196)
④ referer 域名段校验 (:98)
⑤ CheckCaptchaService.checkCaptcha (CheckCaptchaService:119)
├─ supplyCheckDataFromGetRedis:回填 get 阶段 fp/gp/sce/imgId/cts (:327)
├─ 指纹黑名单 checkFingerPrintFreq → 413 HF_FAILED (:174)
├─ getCheckResult:先跑名单拦截链(见 04 篇),再按类型分派 (:350)
├─ 短信类型:只记成功日志,直接返回 (:133)
├─ 付费版:高频切换统计(只统计,不下发) (:142)
├─ validateContext:安全模式上下文规则链 (:263)
└─ buildFinalResult:答案 && 上下文 双通过才发 validate (:211)
⑥ convert → {token, result, validate} (CheckControllerV2:232)3.1 会话 Redis 结构
token 键为 pri:token:{token}(captcha-base/src/main/java/com/netease/is/nc/base/redis/RedisKeyFormat.java:53),Hash 结构,写入用一条 Lua 合并 HMSET+EXPIRE(CaptchaHARedisService.java:54),读取+删除用另一条 Lua 保证一次性消费(CaptchaHARedisService.java:44)。
| field | 含义 | 写入时机 | 代码 |
|---|---|---|---|
fp | get 阶段浏览器指纹 | get | CaptchaHARedisService.java:82 |
ans | 标准答案(百分比坐标/点位/词序) | get | :83 |
type | 验证码类型值 | get | :84 |
time | 生成时间戳 | get | :85 |
smart | 是否智能无感知 | get | :86 |
env | 运行环境 | get | :87 |
gp / sce | 业务分组/场景(≤32 字符) | get | :88-89 |
imgId | 底图 id | get | :90 |
cts | 校验次数计数,初始 1 | get | :91 |
actoken / irToken | 反作弊/风控令牌 | get | :92-93 |
ver | 二次校验结果("0" 表示 OK) | check 通过 | CheckCaptchaService.java:250 |
ua / ip | 客户端 UA / IP(按产品开关决定是否回传) | check 通过 | :244 |
hitType | 命中类型码(HitType) | check 完成 | :251 |
3.2 有效期与失败处置
- 非短信:
app.getTimeout();短信固定Const.DEFAULT_SMS_EXPIRE_TIME_SEC = 300秒(GetCaptchaService.java:119、Const.java:24)。 - 答案比对:拖动验证码把百分比答案还原成像素后比阈值——安全模式 480px 以下 5px、以上 8px;非安全模式 8px/10px(
captcha-server/src/main/java/com/netease/is/nc/server/service/drag/check/CaptchaCheckService.java:88、:121)。宽度限[220, 1920](Const.java:42)。 - 失败处置:非智能验证码失败即整键删除(不可重试),智能验证码保留键(允许重试)(
CheckCaptchaService.java:222)。成功时先invalidateAnswer删掉ans字段,再写ver,同一 token 无法二次交卷(:218)。 - 失败计数:
cts白名单统计,真正起阈值作用的是高频规则(fq:*桶计数)与设备指纹/IP/用户名单,命中即写回名单库形成联防(CaptchaContextCheckServiceImpl.java:200)。
3.3 上下文校验链结果
命中高频时通过率被压到 SUCCESS_RATE_FREQUENCY(0.0,必拦),hitType 覆写为 HIGH_FREQUENCY(20100),并按规则配置把 IP/用户/指纹写入对应黑名单库(CaptchaContextCheckServiceImpl.java:172-178、:248)。对外错误码最终落到 CONTEXT_VALIDATE_ERROR(420)(CheckCaptchaService.java:226)。
4. 二次校验链路(/api/v2/verify)
| 步骤 | 动作 | 代码 |
|---|---|---|
| 1 | 参数非空校验(captchaId/validate/secretId/nonce/timestamp/version/signature) | VerifyControllerV2.java:295 |
| 2 | validate 先 urlsafe 还原(-→\、_→/、.→+)再 AES 解密,按 :: 切出 validate 与 fp | VerifyControllerV2.java:322、:428 |
| 3 | 签名校验:secretId 必须匹配 App,sign(secretKey, method, params) 比对 | VerifyControllerV2.java:374 |
| 4 | 版本校验:version == "v2" 且 app.isVersion2() | VerifyControllerV2.java:405 |
| 5 | AES 解出 NValidate,比对 captchaId | VerifyCaptchaService.java:71、:101 |
| 6 | getAndDelete 一次性消费 token,ver == "0" 且指纹与 get 阶段一致才通过 | VerifyCaptchaService.java:110、:122、:147 |
| 7 | 可选字段按 sys_conf 开关回传:token/captchaType/clientIp/clientUa/hitType | VerifyControllerV2.java:203 |
小程序指纹为空属正常(VerifyControllerV2.java:361);RunEnv 为小程序类型时跳过指纹比对(VerifyCaptchaService.java:148)。SDK 不可用时有兜底凭证 Const.SDK_UNAVAILABLE_VALIDATE,按 sys_conf 开关直接放行(VerifyCaptchaService.java:232)。
5. 业务参数体系
代码里没有 appId / businessId 这两个字段名,实际对应关系如下,改协议前必须认清:
| 概念 | 实际字段 | 落点 | 说明 |
|---|---|---|---|
| 应用(App) | id 参数 → App.captchaId | App.java:51 | 一应用一密钥对,get/check 的主键 |
| 业务/产品 | App.productId | App.java:46 | 产品级维度,名单同步与统计按它聚合 |
| 密钥对 | secretId / secretKey | App.java:56、:61 | 仅 verify 使用 |
| 类型 | type 参数 | CaptchaType | 2 拖动 / 3 文字点选 / 4 短信 / 5 智能无感知 / 6 障碍躲避 / 7 图标点选 / 8 语序选词 / 9 图片推理 / 10 语序点选 / 11 空间推理 / 12 语音(captcha-common/.../CaptchaType.java:24-37) |
| 难度 | App.typeEnhance | TypeEnhance | 0 默认 / 1 增强(captcha-common/.../TypeEnhance.java:17),拖动增强会下发 attrs 角速度 |
| 适老化 | sizeType 参数 | SizeType | 影响题面尺寸与图片队列 |
| 尺寸 | width / dpr | AbstractController | dpr 归一化到 1~3,非法值按 1 处理(GetControllerV2.java:237) |
| 域名限制 | security.restrict.domains | AbstractController.java:403 | 支持 *.b.com 通配,sys_conf 可覆盖 |
| 超时 | App.timeout | App.java:68 | 会话 TTL 秒数 |
6. 错误码
协议层错误码(captcha-server/src/main/java/com/netease/is/nc/server/abst/ErrorCode.java:9):OK(0)、PARAM_ERROR(100)、SERVER_ERROR(110)、LICENCE_ERROR(111)、INVALID_BUSINESS(432)。
业务错误码(CaptchaError,captcha-common/src/main/java/com/netease/is/nc/common/constant/CaptchaError.java:31)常用项:
| 码 | 枚举 | 触发点 |
|---|---|---|
| 0 | OK | 校验通过 / verify 成功 |
| 100 / 101 | CAP / CREATE_ERROR | 无题可发、生成失败 |
| 200 / 202 | NTERNAL_ERROR / NO_IMAGE_SET | 取图内部异常、图片集缺失 |
| 400 | BAD_REQUEST | 参数结构错误 |
| 401 | KEY_ERROR | 产品 key 错误 |
| 403 | DATA_ERROR | 入参数据非法(verify 默认失败码) |
| 404 / 405 / 406 / 407 | APP_ERROR / NOS_ERROR / RESIZE_ERROR / REDIS_ERROR | 应用不支持、存储、缩放、Redis 故障 |
| 410 | PLACE_FAILED | 拖动位置不达标 |
| 411 | PWD_ERROR | 答案缺失或超时 |
| 412 | NOT_HUMAN | 行为判定非人 |
| 413 | HF_FAILED | 设备指纹黑名单命中 |
| 414 | TRACE_ERROR | 轨迹解析失败 |
| 415 | SIGN_ERROR | verify 签名不匹配 |
| 416 | ID_NOT_MATCHED | validate 内 captchaId 与入参不一致 |
| 417 | SMS_NOT_RECEIVED | 短信未上行 |
| 418 | NOT_ALLOWED | IP / 用户名单命中、短信接口 IP 白名单拒绝 |
| 419 | PARAM_ERROR | check 参数校验失败 |
| 420 | CONTEXT_VALIDATE_ERROR | 上下文规则链拒绝(含高频命中) |
| 421 | VERSION_ERROR | 版本不匹配 |
| 422 | RANDOM_REJECTED | 智能无感知随机拒绝 |
| 435 | SDK_UNAVAILABLE | SDK 不可用兜底 |
| 500 / 600 | EXCEPTION / UNKNOWN | 内部异常 / 未知 |
命中维度另有独立标记 CheckResult.hitBlacklist:0 未命中、1 指纹、2 IP、3 用户(captcha-server/src/main/java/com/netease/is/nc/server/service/meta/CheckResult.java:38),后台展示依赖它而非错误码。
7. Prometheus 指标
只有两个自建指标,由 PrometheusMetricsInterceptor 挂在 /api/** 上采集(captcha-server/src/main/webapp/WEB-INF/captcha-server-servlet.xml:28):
| 指标名 | 类型 | 标签 | 含义 | 代码 |
|---|---|---|---|---|
captcha_requests_total | Counter | path,method,code,server_ip,hostname | 按 URI+方法+HTTP 状态计数,code 是 HTTP 码不是业务码 | PrometheusMetricsInterceptor.java:36 |
captcha_requests_latency_seconds_histogram | Histogram | 同上 | 请求耗时,桶 0.05/0.1/0.2/0.5 秒 | PrometheusMetricsInterceptor.java:46 |
两个指标都带 server_ip + hostname,是为了多副本部署时能按实例下钻;preHandle 只打时间戳,afterCompletion 在响应结束后计数(:56、:63)。业务成功率、类型分布、名单命中率全部落在 ES(captcha_info_yyyy-MM-dd),不在 Prometheus 里,需要走 admin 后台查询。
8. 全链路总图
text
┌──────────── 客户业务后端 ────────────┐
│ POST /api/v2/verify (signature+AES) │
▼ │
┌──────────────────────── captcha-server :8085 ─┴──────────────────────────┐
│ MVC 拦截器(仅 /api/**) │
│ LicenceCheckInterceptor ──无有效证书──▶ 111 LICENCE_ERROR │
│ PrometheusMetricsInterceptor ──▶ 2 个指标 │
│ │
│ GET /api/v2/getconf ─▶ dt 签发/验签 ─▶ resources 按版本选 ─▶ ir 风控配置 │
│ GET /api/v3/get ─▶ 参数校验 ─▶ 类型决策 ─▶ License ─▶ 题面 ─▶ Redis │
│ (S3 图片 URL 回给前端) │
│ GET /api/v3/check ─▶ trace 解密 ─▶ 名单拦截链 ─▶ 类型校验(答案阈值) │
│ ─▶ 上下文规则链 ─▶ 高频 ─▶ 写名单 │
│ ─▶ 通过:ver=0 → validate │
│ GET /api/v1/sms ─▶ IP 白名单 ─▶ 手机号 1 小时限额 ─▶ 标记短信已上行 │
└───────────────┬───────────────────────────────┬──────────────────────────┘
│ │
Redis(maxmemory ▸ pri:token / pri:ctx) ES(captcha_info_*)9. 改动前的自检点
/api/v3与/api/v2的路径分配是 SDK 内置规则,不可在代理层做前缀重写(docs/二次校验接口隔离代理方案.md:60)。callback必须过^[a-zA-Z0-9_]{1,64}$,否则直接 400 纯文本,前端拿不到业务错误码。data与validate都是密文,禁止在 Controller 层直接按明文解析;轨迹长度上限 4000 字节(Const.java:7)。- 新验证码类型必须同时补齐
get/check/fallback三处实现,否则getCaptchaImgByType的default分支会返回空CaptchaImg,前端表现为「白屏但无错误码」。