WeiyanGuard一行 script + 一个 guard。appId 是本站应用标识;接口地址从脚本自身推导,
引擎与版本由 api.php?act=cfg 下发,页面里没有版本号可改。
<script src="//captcha.llua.cn/v4/wcaptcha.js?v=261101.10" data-app-id="demo"></script>
<script>
WeiyanGuard.init(); // 预建实例,首屏就把题备好
document.querySelector('#login').addEventListener('click', function () {
WeiyanGuard.guard(function (cap) {
// cap = { captcha_id, captcha_randstr }
// 一起提交给业务后端,后端带 app_key 调 api.php?act=verify 核销
});
});
</script>
浏览器侧只碰这四个地址:GET api.php?act=cfg 取配置,
POST act=gen 出题,POST act=check 判定,
POST act=abandon 作废(关闭或换题时自动发)。
另有 POST act=tel 是匿名运行环境上报,后台可关。请求/响应是本站自己的
{code,msg,data} 信封。
字段也是本站自己的:题面是 id / bgWidth / sliderWidth / pow / proof,
通过凭证是 {captcha_id, captcha_randstr}。没有 lot_number /
captcha_output / pass_token / gen_time 这一套。
POST api.php?act=verify业务后端拿 captcha_id + captcha_randstr,配 app_key 直接提交。
不需要签名,app_key 本身就是凭据 —— 所以只能放服务端。
POST https://captcha.llua.cn/api.php?act=verify
Content-Type: application/json
{
"appid": "demo",
"app_key": "…",
"id": "SLIDER_…",
"randstr": "…",
"idempotency_key": "order-20261101-0001"
}
成功时:
{
"code": 200, "msg": "OK",
"data": true,
"evil_level": 0,
"captcha_args": {
"lot_number": "SLIDER_…",
"user_ip": "192.168.31.50",
"referer": "https://example.com/",
"used_type": "slide",
"scene": "demo"
}
}
只认 data === true。票据无效或已经用过时,返回的是
{"code":200,"msg":"OK","data":false} —— HTTP 状态码依然是 200,
别拿 HTTP 码判断放行。只有 app_key 不对才是 HTTP 403。
票据一次性,核销成功即作废。
captcha_args 是对账材料,不是判据:user_ip 是过验证那台机器的 IP,
referer 是来源页,used_type 是题型,scene 是应用。
拿它和本次业务请求的 IP 比一比,就能发现「A 机过验证、票据转给 B 机用」。
evil_level 取 0 未发现异常 / 1 可疑 / 2 明确恶意,
不影响 data,只给二次判定用。
// 业务后端(PHP),app_key 只在服务端
$res = json_decode(file_get_contents('https://captcha.llua.cn/api.php?act=verify', false,
stream_context_create(['http' => [
'method' => 'POST',
'header' => "Content-Type: application/json\r\n",
'content' => json_encode([
'appid' => $appId,
'app_key' => $appKey,
'id' => $captchaId,
'randstr' => $randstr,
'idempotency_key' => $orderNo, // 可选,重试时复用同一个
]),
]])), true);
if (($res['data'] ?? false) === true) { /* 放行 */ }
核销请求超时的时候不要直接判失败:带 idempotency_key(8–64 位,
[A-Za-z0-9_-])重试,同一个 key 拿到第一次的结果;换 key 再来必然
data:false。
mode:'inline'不弹层,把题面挂进页面里的容器。通过后不自动收起,由接入方决定何时
close() 或换一道题。需要这种精细控制时用 Captcha4.init ——
它和 WeiyanGuard 在同一个 SDK 文件里,只是给的是完整实例。
<div id="capBox"></div>
<script>
Captcha4.init({ appId: 'demo', mode: 'inline' }, function (cap) {
cap.onReady(function () {})
.onPass(function (p) {
// p = { challenge_id, proof, ticket }
})
.onError(function (e) {
// { code, message, detail }
})
.appendTo('#capBox');
});
</script>
mode 不传就用配置里的 ui.mode。
接入方显式写了就按自己的来。常用方法:
open() / close() / refresh() /
reset() / destroy() / appendTo('#box') /
getProof()。
onReady():组件就绪,可以调用 open()。onPass(p):通过那一刻。结果参数是
{challenge_id, proof, ticket}。onSettled(p):通过动画播完。要留住动画就等它再跳转;
网络请求可以在 onPass 先发。onClose():用户没通过就关了。onError(err):{code, message, detail},加载失败或校验异常。getProof() 随时可取;没通过时返回 null。
浮层形态通过后自动关闭;内嵌形态不自动关。
被风控冷却时不会产生任何成功回调,控件显示「操作过于频繁,请稍后再试」并倒计时,
业务侧拿不到凭证 —— 拿到 getProof() 才意味着验证通过。
var proof = cap.getProof();
// { challenge_id: "SLIDER_…", proof: "…", ticket: "…" }
aidEncrypted)按应用开启,默认关闭。开启后 gen 必须带一个由你的服务端用
app_key 签出来的 aidEncrypted,否则一律回 code 4002。
它挡的是「只拿到 appId 就能白嫖出题」—— appId 写在页面里是公开的,
Origin 是 HTTP 头、curl 一行就能伪造,只有 app_key 是真拿不到的。
plain = appid + "|" + expireAt + "|" + nonce // expireAt 是 unix 秒,nonce 是 16 位十六进制 sig = hex( HMAC-SHA256(key = hex2bin(app_key), msg = plain) ) aidEncrypted = "v2." + base64url(plain) + "." + sig // 去掉 base64 结尾的 =
// PHP,6 行,不用装任何扩展
function tac_aid(string $appid, string $appKey, int $ttl = 300): string {
$exp = time() + $ttl;
$nonce = bin2hex(random_bytes(8));
$plain = $appid . '|' . $exp . '|' . $nonce;
return 'v2.'
. rtrim(strtr(base64_encode($plain), '+/', '-_'), '=')
. '.' . hash_hmac('sha256', $plain, hex2bin($appKey));
}
有效期在后台「凭证有效期」里配(1–86400 秒,默认 300)。一个凭证整页复用,直接传给控件:
Captcha4.init({ appId: 'x', aidEncrypted: '…' }, cb);
// 或者每次出题前现取:
Captcha4.init({
appId: 'x',
aidProvider: function () { return fetch('/你的后端/签aid').then(r => r.text()); }
}, cb);
不想自己写签发,可以在你的服务端用 app_key 按上面的方式生成 aid; 这个签名只能放服务端做,别把 app_key 写进页面。 凭证无效或过期时控件显示「验证服务异常,请刷新页面」,不会自动重试。
控件不只有鼠标/触屏一条路。手柄本身是 role="slider" + tabindex="0",
落在弹层的第一个 Tab 位上,接入方不需要做任何事:
Tab 焦点进到弹层,落在手柄上 ← / ↓ 退一格(量程的 1%) → / ↑ 进一格 Home / End 回到最左 / 推到最右 Enter / 空格 确认提交(和松开鼠标等价)
步长取量程的 1%,保证任何答案位置都至少有两三档落在容差(显示宽的 2%)里。
手柄拿到键盘焦点时有一圈主题色描边(:focus-visible,鼠标点不会出现),
滑轨提示会从「按住滑块,拖动完成拼图」换成「← → 调整位置,回车确认」。
读屏软件靠 aria-valuenow / aria-valuetext 播报进度,
校验结论走滑轨内那句 role="status" 的文本。