跳转到内容

行为验证码

适用对象:需要在登录、注册、发送短信、敏感提交等操作中接入行为验证码的公司应用系统。本文按当前对外接口实现编写。

captcha-1captcha-2captcha-3captcha-4

接入完成后,用户在应用页面完成行为验证码,应用后端在执行业务前验证一次性 token,验证通过后继续业务。

参与方需要完成的工作
企业服务平台 / 流控服务提供方提供环境地址、客户端接入信息和适配后的前端 SDK,运行统一验证码服务
接入系统前端展示验证码,提交 SDK 采集的轨迹,将验证码 token 随业务请求提交
接入系统后端获取平台访问令牌,代理生成与校验请求,在业务接口中调用二次验证

验证码类型由企业服务平台从滑块、旋转、拼接、文字点选四种类型中随机选择,允许连续重复;

信息本文中的名称说明
企业服务平台授权地址ESP_TOKEN_URL,正式环境:esp.xkw.cn平台提供的客户端模式授权端点,现有接入使用 /oauth2/token
应用标识及密钥client_id、client_secret由平台发放,保存在接入系统后端
验证码前端资源包平台适配版 Web SDK与后端验证码版本兼容,包含 message → msg 适配
运行参数由维护方确认当前验证码和二次验证 token 默认有效期均为 120 秒,实际以环境配置为准

本文服务路径以 /throttle/api/external/v1/captcha 为前缀

captcha-sequence-diagram

/captcha/gen、/captcha/check 是接入系统代理路由,由接入方实现,可以调整名称;

为了处理跨域问题,浏览器通过接入系统同源代理访问验证码服务。平台 client_secret 和平台访问令牌只保存在接入系统后端。二次验证由业务后端执行,前端“验证成功”的提示或标志不能替代二次验证。

应用后端使用企业服务平台的客户端模式(grant_type=client_credentials)获取 access_token。用户登录令牌、授权码模式令牌不能替代客户端模式令牌。

授权调用示例(参数和授权地址以平台发放信息为准):

POST <ESP_TOKEN_URL>
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&client_id=<应用标识>&client_secret=<应用密钥>

参数需要按表单规则进行 URL 编码。授权返回中的 access_token 用于后续请求,expires_in 表示授权端给出的有效时长;应在后端缓存令牌,并在到期前重新获取,避免每次生成验证码都申请访问令牌。

三个接口都需要以下请求头:

Authorization: Bearer <access_token>

两种 token 的用途不同:

名称来源用途传递位置
平台 access_token企业服务平台授权端证明应用系统有权调用服务三个服务接口的 Authorization 请求头
验证码 token / validToken验证码 /check 成功响应中的 data.token证明一次验证码行为校验通过,一次性消费业务请求参数,随后作为 /verify 的表单或查询参数

validToken 不能放进 Bearer 请求头替代 access_token。

项目约定
方法三个接口均为 POST
编码UTF-8,响应为 JSON
成功判断检查 JSON 中的数字字段 code,成功值为 200
二次验证成功判断同时满足 code === 200 和 data === true
错误提示message,不要通过中文提示文本判断业务分支
排查标识requestId,请求失败时可随问题反馈给平台维护方

统一响应示例:

{
"code": 200,
"message": "成功",
"requestId": "示例请求标识",
"data": {}
}

失败响应可能没有 data,或 data 为 null。接入方应同时检查 HTTP 传输结果和 JSON 业务码,不能仅凭 HTTP 200 判定成功;服务当前可能以 HTTP 200 返回业务错误,网关也可能直接返回 HTTP 错误或非 JSON 内容。

代理原样转发服务响应,保持 code 为数字。不要再包一层 data、转换成字符串或在转发前删除字段。

POST /throttle/api/external/v1/captcha/gen
Authorization: Bearer <access_token>
Content-Type: application/json
{}

无业务入参,按 SDK 默认提交空 JSON 对象即可。当前接口由服务端随机选择类型,不接收前端指定的验证码类型。

成功响应示例(图片和扩展数据仅作结构展示):

{
"code": 200,
"message": "成功",
"requestId": "gen-request-id",
"data": {
"id": "SLIDER_0123456789abcdef0123456789abcdef",
"type": "SLIDER",
"backgroundImage": "data:image/jpeg;base64,<背景图片数据>",
"templateImage": "data:image/png;base64,<模板图片数据>",
"backgroundImageWidth": 600,
"backgroundImageHeight": 360,
"templateImageWidth": 110,
"templateImageHeight": 360,
"data": {}
}
}
data 字段说明
id验证码标识,校验时原样回传,作为不透明字符串处理
typeSLIDER、ROTATE、CONCAT 或 WORD_IMAGE_CLICK
backgroundImage背景图片数据,由 SDK 展示
templateImage模板图片数据,拼接类型可能为 null
图片宽高字段服务端返回的图片尺寸,交给 SDK 处理,不要硬编码
dataSDK 使用的类型相关扩展数据,原样保留

默认有效期为生成后 120 秒。刷新或失败重试时重新调用 /gen,使用新返回的 id 和图片数据。

POST <THROTTLE_BASE_URL>/api/external/v1/captcha/check
Authorization: Bearer <access_token>
Content-Type: application/json

请求体:

{
"id": "SLIDER_0123456789abcdef0123456789abcdef",
"data": {
"bgImageWidth": 300,
"bgImageHeight": 180,
"templateImageWidth": 55,
"templateImageHeight": 180,
"startTime": 1792512000000,
"stopTime": 1792512001200,
"trackList": [
{"x": 0, "y": 0, "t": 0, "type": "down"},
{"x": 100, "y": 0, "t": 1200, "type": "up"}
],
"data": {}
}
}

以上仅展示滑块请求结构,不能作为可通过校验的测试数据。实际请求必须使用 SDK 采集的完整轨迹,代理不应自行构造、筛选或重新计算字段。不同类型的字段含义可能不同,例如旋转类型会由 SDK 特殊处理 bgImageWidth。

请求字段必填说明
id是非空字符串,最长 128 字符,来自生成响应
data是SDK 的完整轨迹对象,包含时间、坐标和类型相关数据

校验成功:

{
"code": 200,
"message": "成功",
"requestId": "check-request-id",
"data": {
"id": "SLIDER_0123456789abcdef0123456789abcdef",
"token": "0123456789abcdef0123456789abcdef"
}
}

data.token 是 32 位小写十六进制字符串。前端将它作为 validToken 随业务请求提交给本系统后端。

轨迹校验失败示例:

{
"code": 4001,
"message": "基础校验失败",
"requestId": "check-failed-request-id"
}

验证码进入核心校验后,其缓存会被取出并删除,成功或答案错误都会消费本次验证码。校验失败、重复校验或校验超时后应重新生成,不能反复提交同一 id。参数错误发生在进入核心校验之前时,不保证已消费;接入方统一重新生成即可。

8. 二次验证:业务后端必须调用

Section titled “8. 二次验证:业务后端必须调用”
POST /throttle/api/external/v1/captcha/verify
Authorization: Bearer <access_token>
Content-Type: application/x-www-form-urlencoded
validToken=0123456789abcdef0123456789abcdef
参数必填说明
validToken是/check 成功返回的 token,格式为 [a-f0-9]{32}

validToken 也可通过查询参数传递,推荐使用表单,避免出现在 URL 中。此接口通过 @RequestParam 接收参数,不能仅发送 {"validToken":"…"} JSON 请求体。

成功响应:

{
"code": 200,
"message": "成功",
"requestId": "verify-request-id",
"data": true
}

过期、不存在或已消费:

{
"code": 4000,
"message": "已失效",
"requestId": "verify-failed-request-id"
}

接入系统的业务接口按以下顺序处理,示意流程与编程语言无关:

收到业务参数及 validToken
→ 检查本系统会话/业务权限、参数及重复提交状态
→ 后端携带平台 access_token 调用 /verify,表单提交 validToken
→ HTTP 请求失败、无法解析响应、code 非 200 或 data 不为 true:拒绝执行业务
→ code 为 200 且 data 为 true:执行登录、发送短信、提交等业务
→ 返回本系统业务结果

行为验证规则如下:

  • token 默认在 /check 成功签发后 120 秒内有效,与验证码本身的有效期分别计算。
  • /verify 会原子消费 token,同一 token 的正常并发验证只允许一次成功。
  • 当前 token 不绑定 clientId、用户会话或业务场景;具备接口调用资格且持有有效 token 的应用可消费它。需要会话或场景绑定时,由接入系统维护关联并检查。
  • 不要先调用一次 /verify“试验是否有效”,随后在业务中再次调用;第一次成功后 token 已失效。
  • /verify 超时后,服务端可能已经消费 token;不要把超时视为通过,也不要自动重复消费后直接放行业务。
  • 验证码消费与接入系统的业务事务不在同一事务中,业务失败不会恢复 token。结合业务幂等机制处理重试;需要再次验证时重新完成验证码。

前端负责展示验证码、提交 SDK 采集的操作轨迹,并将校验成功返回的 data.token 随业务请求交给本系统后端。业务后端再按第 8 节执行二次验证;浏览器显示“验证成功”不代表业务可以直接放行。

接入前,前后端需要明确:

  • 何时验证:可以在登录、发送短信、敏感提交前要求验证,也可以由业务后端根据风险判断要求补充验证。选择哪种方式由接入系统决定,平台不规定业务响应码或弹窗触发条件。
  • 浏览器访问的路由:由本系统提供生成、校验代理,以下示例使用 POST /captcha/gen 和 POST /captcha/check,对应第 10 节。它们不是平台固定提供的浏览器地址。
  • 凭证传递方式:业务请求使用哪个字段或请求头携带验证码 token,由本系统前后端约定。以下使用业务请求体中的 validToken,不要求采用特定请求头。
  • 失败与取消行为:未获得有效 token 或用户取消时停止本次业务提交;后端要求重新验证时重新获取验证码,不能无限自动重试业务。

平台 client_secret 和客户端模式 access_token 只保存在后端。浏览器代理请求使用本系统规定的会话、CSRF 等保护机制,不向浏览器配置平台访问令牌。不要将本系统用户登录令牌与平台访问令牌混用。

向平台维护方获取配套 Web SDK,将脚本、样式和图片整体部署到本系统静态资源服务,保留包内目录结构和相对路径。例如,将资源映射为:

/captcha/tac/
js/tac.min.js
css/tac.css
images/…
<link rel="stylesheet" href="/captcha/tac/css/tac.css">
<script src="/captcha/tac/js/tac.min.js"></script>
<div id="captcha-container"></div>

示例路径按实际部署位置调整,应用部署在子路径时也需包含对应前缀。资源请求必须返回正确的 JS、CSS 和图片,而不是前端路由回退页面。脚本加载成功后可通过 window.TAC 创建实例;创建前需确保样式已加载、挂载元素已存在。

也可以在需要验证时动态加载资源。此时应设置超时、合并并发加载,失败后允许重新加载,并提供明确的重试或取消入口。

平台响应使用数字 code: 200 表示成功,错误信息在 message,校验凭证在 data.token。生成请求提交 {},校验请求提交 SDK 提供的完整 { id, data };详见第 6、7 节。

为沿用本系统的请求鉴权、错误处理和取消机制,可以在创建实例后、调用 init() 前覆盖 instance.config.requestCaptchaData 和 instance.config.validCaptcha。下面展示配套 SDK 的请求适配方式,使用原生 JavaScript,不依赖组件库。

onVerified 由接入方提供,用于接收 token;onCancel 用于结束等待;showError 用于展示错误及重试入口。页面上的刷新、重试按钮可调用返回对象的 refresh() 并保存返回的新控制对象,关闭按钮调用 cancel()。

function mountCaptcha({ onVerified, onCancel, showError }) {
const host = document.querySelector("#captcha-container");
if (!host || !window.TAC) throw new Error("验证码资源或容器尚未就绪");
const controller = new AbortController();
let active = true;
async function postJson(url, body) {
const response = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "same-origin",
signal: controller.signal,
body: JSON.stringify(body)
});
const result = await response.json();
if (!response.ok || result.code !== 200) {
throw new Error(result.message || "验证码请求失败");
}
return result;
}
function dispose() {
if (!active) return;
active = false;
controller.abort();
instance.destroyWindow();
host.replaceChildren();
}
function cancel() {
if (!active) return;
dispose();
onCancel();
}
const instance = new window.TAC({
bindEl: "#captcha-container",
requestCaptchaDataUrl: "/captcha/gen",
validCaptchaUrl: "/captcha/check",
btnCloseFun: cancel
});
instance.config.requestCaptchaData = async () => {
try {
const result = await postJson("/captcha/gen", {});
// 保留图片、类型和扩展字段,将完整响应交给 SDK。
return result;
} catch (error) {
if (active) showError(error.message || "验证码加载失败,请重试");
// 配套 SDK 不捕获生成请求的 rejection,以失败响应结束加载。
return { code: -1, message: "验证码不可用" };
}
};
instance.config.validCaptcha = async (id, data, view) => {
try {
const result = await postJson("/captcha/check", { id, data });
if (!active) return;
const token = result.data && result.data.token;
if (typeof token !== "string" || !/^[a-f0-9]{32}$/.test(token)) {
throw new Error("校验接口未返回有效 token");
}
view.showTips("验证成功", 1, () => {
if (!active) return;
dispose();
onVerified(token);
});
} catch (error) {
if (!active) return;
view.showTips(error.message || "验证码校验失败", 0, () => {
if (active) instance.reloadCaptcha();
});
}
};
instance.init();
return {
cancel,
refresh() {
if (!active) return;
dispose();
return mountCaptcha({ onVerified, onCancel, showError });
}
};
}

上述代码展示验证码挂载和请求适配,不包含弹窗、按钮及业务页面实现。调用方应保存控制对象;刷新时将其更新为 control.refresh() 的返回值,以便后续关闭或刷新作用于新实例。刷新只更换验证码,不调用 onCancel,也不提交业务;用户关闭时才结束本次验证等待。框架组件卸载时也应取消仍在进行的验证。

请求适配需注意:

  • postJson 应补充本系统需要的鉴权、CSRF 和异常处理,不得注入平台客户端令牌。HTTP 错误、非 JSON 响应及网络异常均按失败处理。
  • 错误提示直接读取 message。使用上述覆盖方式时不需要再给响应补 msg;仅给 SDK 默认请求添加兼容字段,不能保证所有失败分支都展示后端原因。
  • 完整保留生成响应,完整提交 SDK 给出的轨迹对象,不手工构造坐标、时间或类型相关字段,也不要缩放图片后自行推算轨迹。
  • 校验返回成功码但缺少有效 token 时不能提交业务,不得以验证码 ID、登录令牌或本地成功标志兜底。
  • 若业务请求层带有自动弹出验证码的逻辑,验证码代理请求应绕过该逻辑,避免递归触发。
  • 覆盖的 config 方法依赖配套 SDK 版本,升级时需同时检查脚本、样式和回调兼容性。

获得 token 后,按本系统约定发送业务请求。例如,业务后端约定在 JSON 中接收 validToken:

async function submitBusiness(businessData, token) {
const response = await fetch("/api/submit", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "same-origin",
body: JSON.stringify({ ...businessData, validToken: token })
});
// 根据本系统的 HTTP 状态及业务响应协议处理结果。
return response;
}

/api/submit 只是本系统业务路由示例。业务后端收到 validToken 后,使用后端持有的平台访问令牌调用 /verify,以表单提交 validToken,仅在二次验证通过后执行业务。前端不能自行调用 /verify 来代替这一步。

token 只用于当前业务提交,不写入 localStorage、不设为全局默认请求参数、不与其他请求共享。业务失败、网络超时或 token 已消费时,不能假定它仍可使用,应根据后端结果引导重新验证。

若采用“业务请求被要求补充验证后再次提交”的方式,应另外保证:

  • 识别条件及 token 传递位置由本系统协议明确,不依赖平台提供业务风控响应码。
  • 首次响应要求验证时,后端尚未执行业务;再次提交保留原 URL、方法、参数、请求体和鉴权,写操作结合幂等机制处理。
  • FormData 不手动设置 Content-Type,由浏览器生成 boundary;流等不可重复消费的请求体需先设计可重放方式。
  • 再次提交仍要求验证时结束本次自动处理并给出提示,避免循环弹窗或重复执行业务。

验证码可嵌入页面或放入弹窗,样式由接入系统实现。自定义刷新、关闭等控件时应保留明确的操作指引、错误提示及重试入口。通过局部样式覆盖调整外观,不改变 SDK 用于轨迹计算的图片尺寸和拖动坐标关系;应验证平台启用的每种验证码类型均能完整显示和提交。

配套 SDK 内部使用固定 DOM ID,不要同时挂载多个实例。存在并发验证需求时应排队展示,每轮独立获取 token,不共享一次性凭证。

关闭、刷新或离开页面时,中止生成和校验请求,调用 destroyWindow() 并清空容器;忽略旧实例迟到的响应和提示回调。弹窗关闭有动画时,等旧内容完成卸载后再挂载下一实例。使用 Promise 封装时,用户取消也必须完成等待状态,不能让业务调用一直挂起。

接入系统路由示例服务端转发目标转发要求
POST /captcha/genPOST /throttle/api/external/v1/captcha/gen提交 {},附加后端取得的平台 Bearer 令牌
POST /captcha/checkPOST /throttle/api/external/v1/captcha/check原样转发 {id,data} JSON,附加平台 Bearer 令牌
POST /api/login 等业务接口内部调用 POST /throttle/api/external/v1/captcha/verify用表单提交 validToken,验证成功后执行业务

代理地址固定指向平台提供的服务。平台访问令牌由代理自行注入,浏览器的用户身份头不能直接当作平台客户端令牌。根据业务需要允许未登录用户访问验证码代理,例如登录页面;用户权限与代理入口的频率控制由本系统处理。

正常 JSON 响应原样转发,避免重复包装。网络中断、网关错误或非 JSON 响应按本系统统一异常流程处理,不能伪造成功响应。建议禁用验证码接口的响应缓存,避免多个用户复用同一验证码。

以下为 Bash 风格命令,运行前通过本系统的配置或终端环境设置 THROTTLE_BASE_URL、ACCESS_TOKEN 和 VALID_TOKEN。示例不包含真实凭证;Windows PowerShell 可使用 curl.exe 并调整续行方式。

(生产)THROTTLE_BASE_URL:https://esp.xkw.cn/throttle

(测试)THROTTLE_BASE_URL:https://test-esp.xkw.cn/throttle

生成验证码:

Terminal window
curl --request POST "${THROTTLE_BASE_URL}/api/external/v1/captcha/gen" \
--header "Authorization: Bearer ${ACCESS_TOKEN}" \
--header 'Content-Type: application/json' \
--data '{}'

校验验证码:仅在需要单独调试 /check 时,暂停代理转发,截取 SDK 本次产生但尚未提交给流控服务的真实 {id,data} 请求体,保存为 captcha-check.json。浏览器已经发送成功的 /check 请求不能用 curl 重放,因为验证码已被消费。不要使用第 7 节的结构示例冒充真实轨迹。

Terminal window
curl --request POST "${THROTTLE_BASE_URL}/api/external/v1/captcha/check" \
--header "Authorization: Bearer ${ACCESS_TOKEN}" \
--header 'Content-Type: application/json' \
--data-binary '@captcha-check.json'

二次验证:VALID_TOKEN 取自上一请求成功返回的 data.token。常规联调可直接在页面完成验证码,从 /check 的成功响应取得 token,跳过 curl 校验步骤。这次联调会消费 token,同一个 token 不再用于实际业务。

Terminal window
curl --request POST "${THROTTLE_BASE_URL}/api/external/v1/captcha/verify" \
--header "Authorization: Bearer ${ACCESS_TOKEN}" \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode "validToken=${VALID_TOKEN}"

以下为响应 JSON 中的业务码,不等同于 HTTP 状态码。参数和系统异常由平台全局处理器统一返回,接入方对未知非 200 业务码也应按失败处理。

业务码含义或常见原因接入方处理
200本次接口调用成功/verify 还需检查 data === true
4000验证码或 token 不存在、已过期、已消费重新生成并完成验证码
4001轨迹 / 答案校验失败使用新验证码重试,检查代理是否修改轨迹
401平台访问令牌缺失、过期、无效或不是客户端模式检查 Bearer 请求头及客户端授权,不要使用用户登录令牌
300211常见的参数校验或请求体解析错误检查必填字段、JSON 格式和 Content-Type;具体以响应为准
403核心无效参数,或平台禁止访问结合 message 和 requestId 排查,不能统一认定为答案错误
429平台请求过于频繁按限流策略降低频率,避免连续自动重试
500服务端或依赖异常拒绝继续业务,记录 requestId 并反馈维护方
-1 / 其他非 200平台业务失败或其他错误按失败处理,保留错误信息用于排查
现象排查方向
/gen 返回方法不支持当前接口使用 POST,检查代理是否改成 GET
/verify 提示缺少 validToken检查是否误发 JSON,应发送表单或查询参数
HTTP 200 但页面失败检查 JSON code,以及是否被重复包装为 data.data
页面一直显示同一种类型短时间随机重复是正常情况;检查代理是否缓存 /gen 或前端固定类型
图片和控件没有展示检查整个 /tac 静态目录是否部署、资源路径是否可访问、挂载元素是否存在
SDK 回调读不到消息确认加载的是平台适配版 SDK,服务消息字段为 message
第二次校验同一验证码失败核心校验会消费验证码,需要重新生成
第一次业务通过,第二次失败token 为一次性,不应重复提交或先做预验证
请求超时后重试提示已失效第一次请求可能已经消费验证码或 token,需重新验证

上线前应完成以下联调,不只验证验证码弹窗能够显示:

  • 四种验证码类型都能展示并完成校验,代理完整保留图片、扩展数据及轨迹字段。
  • 正确操作能取得 token,业务后端 /verify 成功后才执行业务。
  • 不提交 token、伪造 token、过期 token、重复 token 均不能继续受保护业务。
  • 同一 token 并发提交时,不会重复触发业务;业务自身具备必要的幂等处理。
  • 轨迹错误后可以刷新验证码重试,避免复用已经消费的 id。
  • 平台访问令牌过期、验证码服务异常、请求超时等情况下,业务不会因为前端显示成功而直接放行。
  • 浏览器及静态资源中不包含平台 client_secret 或平台访问令牌。
  • 已确认目标环境地址、有效期、SDK 版本及代理路由。故障反馈包含发生时间、接口、业务码和 requestId。