网易易盾滑块验证DEMO全流程解析:从初始化到后端二次校验
2026/9/13 12:23:39 网站建设 项目流程

简介:网易易盾滑块验证演示是一套面向网页开发者与验证码技术学习者的源码示例,集中演示了网易易盾滑块验证码的核心机制,涵盖拼图拖动、随机图像生成、滑块起始与最终位置的后端比对等关键步骤,并加入连续尝试次数限制、验证码过期时间等安全策略,帮助理解真实场景中验证码如何抵御自动化工具与机器人攻击。包体极轻量,仅2KB共3个文件,以两个脚本为主体,一个承载滑块渲染、用户交互处理及与服务器的通信逻辑,另一个作为设置文件存放服务器地址、密钥、错误重试次数等可调参数,另有联系方式说明文件。目前已有2252人学习,适合希望快速接入易盾滑块验证或对验证码防破解设计感兴趣的开发者,通过阅读该演示可掌握动态图片生成、坐标计算、服务端校验等实现细节,并能直接修改配置项扩展自定义规则,作为实际项目集成或安全测试的实用参考。

1. 网易易盾滑块验证的DEMO在验证什么

拿到一个网易易盾滑块验证的 demo 程序,多数人的第一反应是研究怎么把背景图上的缺口找准。做过对接就会知道,缺口识别只是整条链路里最不重要的环节:易盾真正在意的不是“你找没找对缺口”,而是“你这一串拖动轨迹像不像真人、你手里有没有拿到合法的验证凭证”。一个能用的 DEMO,价值在于把初始化、用户滑动、前端拿 token、后端二次校验这条链路完整跑通,而不是把一张图抠出来。这套东西适合谁:要在登录页里接入人机校验的前端工程师、需要在数据采集场景里处理登录态验证的脚本工程师,以及想搞懂验证码服务端校验逻辑的测试开发。这篇就用一个最小 DEMO 把整条流水线讲清楚。

2. 滑块验证的最小闭环:初始化、拖动、二次校验

2.1 请求时序:前端只过第一关,后端必须再过一遍

网易易盾滑块验证的完整链路,比表面上看到的“拖一下弹窗消失”要长得多。用户拖完滑块后弹窗关闭,只是通过了前端这一关,业务后端如果只依赖前端回调就放行登录,等于没接验证码。

一次完整的校验分成三段:

第一段是初始化。前端带着易盾后台分配的 captchaId(有些文档里也叫 appId)去请求验证码服务,拿到背景图、滑块图和一个初始 token。这个 token 代表“一次验证会话”,它和用户名、密码没有任何关系,只用来标识这次验证。

第二段是拖动与行为采集。用户从起点拖到缺口附近,前端把坐标轨迹、耗时、加速度等行为数据连同 token 回传给易盾服务端。易盾根据这套轨迹判断“这是人还是机器”,判定通过后返回一个 validate 字段。到这一步,前端工作结束。

第三段是二次校验。业务前端把 token 和 validate 放进登录请求,业务后端拿到后,再通过服务端接口把这些参数发给易盾。易盾用 secretId/secretKey 签名确认请求来源合法,返回本次校验的真伪结果。只有这一段的返回为真,业务系统才能认定这次登录是真人操作。

DEMO 里最常见的错误,是只做了前两段:前端回调成功就把登录表单提交了,后端没调校验接口。这样等于把验证码变成了一个前端装饰品,攻击者直接改写前端回调数据即可绕过。

2.2 token、validate、secretId 各管一段,别混着用

把字段角色理清楚,DEMO 写起来才不会绕晕。下面是这套链路里最核心的几个字段以及它们各自的归属方:

字段产生方使用方作用
captchaId易盾后台申请前端初始化参数标识当前接入的是哪个业务
token易盾服务端返回给前端前端持有,随登录请求传给业务后端标记一次验证会话
validate易盾在滑块通过后返回给前端前端传给业务后端,后端再传给易盾二次校验的凭证
secretId易盾后台申请业务后端持有标识服务端调用方的身份
secretKey易盾后台申请业务后端持有对校验请求做签名

注意 secretKey 绝对不能出现在前端代码里。DEMO 阶段经常有人图省事,把 secretKey 写进一个 config.js 供前端调用,这相当于把服务端校验的通行证贴在了门外面。标准做法是:后端单独保存,通过环境变量或配置中心注入。

token 和 validate 这两个字段还有一个容易混淆的点:token 是验证会话的编号,validate 是本次会话通过后颁发的凭证,两者是成对出现的。后端校验时如果漏传了 validate,或者把 token 当成 validate 传进去,服务端会直接拒绝。

2.3 后端校验接口的最小实现:Flask 版

后端二次校验是 DEMO 中最容易偷工减料的部分。这里给一个 Flask 实现,把签名、参数组包、远端校验都放在一个接口里:

import hmac import hashlib import time import requests from flask import Flask, request, jsonify app = Flask(__name__) SECRET_ID = "your_secret_id" # 易盾后台申请,放环境变量里 SECRET_KEY = "your_secret_key" # 只允许后端持有 CHECK_URL = "https://open.dun.163.com/api/check" # 实际地址以易盾接入文档为准 def make_sign(params: dict, secret_key: str) -> str: items = sorted(params.items()) raw = "&".join(f"{k}={v}" for k, v in items if v not in ("", None)) return hmac.new(secret_key.encode(), raw.encode(), hashlib.sha256).hexdigest() @app.route("/auth/captcha-verify", methods=["POST"]) def captcha_verify(): payload = request.json or {} token = payload.get("token") validate = payload.get("validate") if not token or not validate: return jsonify({"pass": False, "reason": "token or validate missing"}), 400 params = { "secretId": SECRET_ID, "captchaId": payload.get("captchaId"), "token": token, "validate": validate, "timestamp": int(time.time() * 1000), "nonce": payload.get("nonce", ""), "data": payload.get("userData", ""), } params["signature"] = make_sign(params, SECRET_KEY) try: resp = requests.post(CHECK_URL, data=params, timeout=5).json() except requests.exceptions.Timeout: return jsonify({"pass": False, "reason": "upstream timeout"}), 504 return jsonify({"pass": resp.get("result") is True, "detail": resp.get("error", "")}) if __name__ == "__main__": app.run(port=5001)

这段代码里需要注意三件事。第一,签名算法做了参数名升序排序,然后拼成 key=value 的串再做 HMAC-SHA256,这是服务端校验接口最常见的签名套路;实际参数名和拼接规则要以易盾服务端文档为准。第二,timeout=5是必须的,远端校验接口如果响应慢,不能无限占着登录线程。第三,result为 true 才放行,其他情况都要进入失败分支,包括接口返回错误码、返回空值、网络超时三种情况。

2.4 前端 DEMO 的最小页面:initCaptcha 回调拿 validate

前端部分用一个静态 HTML 就能讲清楚。使用易盾标准的加载脚本,在页面里指定一个容器,初始化时把 captchaId 传进去:

<!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"> <title>网易易盾滑块验证 DEMO</title> </head> <body> <form id="login-form"> <input name="username" placeholder="账号"> <input name="password" type="password" placeholder="密码"> <div id="captcha-box"></div> <input type="hidden" name="token" id="token-field"> <input type="hidden" name="validate" id="validate-field"> <button type="submit">登录</button> </form> <script src="https://cstaticdun.126.net/load.min.js"></script> <script> initCaptcha({ captchaId: "你的captchaId", element: "#captcha-box", mode: "slide", enableIce: false, protocol: "https", width: 320, success: function (data) { document.getElementById("token-field").value = data.token; document.getElementById("validate-field").value = data.validate; } }); </script> </body> </html>

mode: "slide"指定渲染为滑块验证,enableIce: false表示关闭无感验证模式,这样 DEMO 里每次都会走完整的滑动流程,方便观察数据流转。success回调是所有环节的入口,只有用户完成有效拖动后才会触发。把 data.token 和 data.validate 同时写进两个隐藏字段,是为了让后面的表单提交逻辑不用关心验证码是怎么工作的,只需要从隐藏字段里取值。

需要注意,加载脚本地址和 captchaId 都需要替换成自己开通服务后拿到的真实值。这个最小页面没有处理验证码初始化失败的情况,生产环境里还要监听error回调,比如网络被拦截或页面协议与加载地址不一致时给出提示。

3. 在本地把 demo 程序跑通:模拟登录一条链路

3.1 本地工程的目录结构与启动顺序

这个 demo 程序在本地需要同时起两个服务:前端静态页和一个 Flask 后端。目录结构保持简单:

captcha-demo/ ├── server/ │ ├── app.py # Flask 后端,负责登录接口和二次校验 │ └── requirements.txt └── web/ ├── index.html # 登录页 └── captcha.js # 易盾初始化与表单提交逻辑

启动后端:

cd captcha-demo/server pip install flask requests flask-cors python app.py

再开一个终端,用 Python 自带的静态文件服务承载前端页面:

cd captcha-demo/web python3 -m http.server 8000

浏览器访问http://localhost:8000就能看到登录页。这里有一个绕不开的问题:前端页面跑在 8000 端口,后端校验接口跑在 5001 端口,属于跨域请求。在 app.py 里加 CORS 支持,限制只允许本地页面访问:

from flask_cors import CORS CORS(app, resources={ r"/auth/*": {"origins": "http://localhost:8000"} })

origins没有写成*,是因为服务端校验接口不应该对任意站点开放,否则别人直接拿你的接口当验证码代理用了。本地调试按域名精确放行,部署后换成正式域名或者直接改成同域部署。

3.2 把验证结果绑进登录表单:模拟带滑块验证的登录页

很多登录页并不是表单直接提交,而是前端拦截 submit 事件后改成异步请求。这里的关键在于:提交动作发生前要检查 validate 字段有没有值,如果没有值,说明用户根本没完成滑块验证,直接提交会被后端拒绝。

const form = document.getElementById("login-form"); form.addEventListener("submit", async (e) => { e.preventDefault(); const validate = document.getElementById("validate-field").value; if (!validate) { alert("请先完成滑块验证"); return; } const resp = await fetch("/auth/redirect-login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username: form.username.value, password: form.password.value, captchaId: "你的captchaId", token: document.getElementById("token-field").value, validate: validate }) }); const result = await resp.json(); console.log("登录结果", result); });

注意fetch的路径是相对路径,本地调试时页面在 8000 端口,请求会发到 8000 端口的/auth/redirect-login。要让这条链路在本地完整跑通,要么在静态服务里配置代理转发,要么直接把/auth/redirect-login的完整地址改成http://localhost:5001/auth/redirect-login。更推荐前者,因为改完整地址意味着前端代码里写死了环境信息,换到测试环境就又要改一遍。

这个页面模拟的就是“带滑块验证的登录页面如何模拟登录”里最关键的一个习惯:把验证码结果作为登录请求的普通参数一起提交,而不是让验证码逻辑独立于登录流程之外。

3.3 用 requests 重放登录链路:浏览器完成滑块,脚本完成登录

如果目的是写一个自动化的登录脚本,最常见的坑是尝试完全用 requests 去模拟易盾的前端接口。实际做下来会发现,易盾的加载接口本身带有风控策略,直接脚本模拟很容易触发二次验证甚至拿不到可用的验证码数据。

一个更可靠的 DEMO 方案是:浏览器里完成滑块,脚本负责登录。操作步骤是先在控制台注入一段监听代码:

window.__captchaData = null; initCaptcha({ captchaId: "你的captchaId", element: "#captcha-box", mode: "slide", success: function (data) { window.__captchaData = data; } });

手动拖完滑块后,在控制台执行copy(window.__captchaData),拿到 token 和 validate。然后用 requests 脚本继续跑登录:

import requests session = requests.Session() # 1. 先访问登录页,种下 cookie session.get("http://localhost:8000") # 2. 从浏览器控制台粘贴上一步拿到的数据 token = "从控制台copy出来的token" validate = "从控制台copy出来的validate" # 3. 调业务后端登录接口,后端内部会再调易盾二次校验 resp = session.post( "http://localhost:5001/auth/redirect-login", json={ "username": "tester", "password": "123456", "captchaId": "你的captchaId", "token": token, "validate": validate, }, timeout=5, ) print(resp.json())

这套“浏览器半自动 + 脚本请求重放”的方式,本质上是把易盾前端的行为采集和风控判断交给真实浏览器环境,自己只负责拿到最终凭证后继续业务请求。它适合所有需要登录态的场景,比如联调环境里自动化跑接口测试,或者验证后端二次校验逻辑是否正常工作。不要试图用 requests 去逆向易盾的轨迹上报协议,那是一条投入产出比极低的路,而且一旦触发风控会把你整个出口 IP 都拉黑。

4. 轨迹、缺口与回调:滑块验证 DEMO 的三个调试点

4.1 拖动轨迹要带“人类特征”,匀速直线最容易露馅

易盾对拖动轨迹的判断重点不在于起点和终点是否对齐缺口,而在于中间过程的时序特征。真实人类拖动鼠标时,手部会有微小的抖动,轨迹不是一条完美直线,速度曲线呈现“加速—匀速—减速—校正”的形态。匀速直线运动的轨迹在行为数据里极其显眼。

如果 DEMO 里需要生成一段用于联调的测试轨迹,可以用下面这个简单的生成器:

import random def human_track(distance, speed=0.8): track = [] cur = 0.0 t = 0.0 # 起始迟疑:先停顿一小段 t += random.uniform(0.1, 0.3) # 前 20% 距离加速 while cur < distance * 0.2: step = random.uniform(1, 3) * speed cur = min(cur + step, distance * 0.2) track.append((round(cur, 1), round(t, 3))) t += random.uniform(0.015, 0.03) # 中段保持速度,但每一步有小幅变化 while cur < distance * 0.75: step = random.uniform(2, 5) * speed cur = min(cur + step, distance * 0.75) track.append((round(cur, 1), round(t, 3))) t += random.uniform(0.01, 0.02) # 末端减速并做位置校正 while cur < distance: step = random.uniform(0.3, 1.2) * speed cur = min(cur + step, distance) track.append((round(cur, 1), round(t, 3))) t += random.uniform(0.03, 0.06) return track

speed控制整体拖得快还是慢,0.8 是一个比较接近人工操作的经验值。返回的列表是 (x, timestamp) 的二元组,实战中的上报数据通常还需要带上 y 轴坐标和压力值,这里只做示意。时间戳的单位是秒,易盾上报时一般要求毫秒级精度,接入时注意换算。

这条轨迹生成方法只用于联调环境验证自己的登录链路,生产环境请直接使用用户的真实拖动数据。把轨迹生成写进生产代码企图绕过风控,既不道德也不稳定,而且易盾的模型会随攻击样本持续更新。

4.2 缺口识别:用像素差定位,别急着上模板匹配

DEMO 里如果需要自动找到背景图上的缺口位置,最简单的做法是拿滑块图去背景图上做滑动对比,找出差异最大的横坐标。用 PIL 的像素差就能实现:

from PIL import Image, ImageChops def find_gap(background_path, slider_path, step=3): bg = Image.open(background_path).convert("RGB") sl = Image.open(slider_path).convert("RGB") bg_w, bg_h = bg.size sl_w, sl_h = sl.size best_x, best_diff = 0, 0 for x in range(0, bg_w - sl_w, step): region = bg.crop((x, 0, x + sl_w, sl_h)) diff = ImageChops.difference(region, sl) diff_val = sum(diff.getdata()) if diff_val > best_diff: best_diff = diff_val best_x = x return best_x

这段代码先按步长 3 粗筛,定位到差异最大的区域后,可以再在附近做步长为 1 的细筛。步长越大速度越快但精度越低,DEMO 场景下图片普遍在 300 像素左右,粗筛加细筛的组合基本能在一秒内出结果。

模板匹配的库用起来更省事,但易盾的背景图经常带干扰纹理,缺口附近的阴影和装饰物会让模板匹配误判。像素差方案虽然笨,但对 DEMO 来说更直观,出问题也好排查。真正生产级的缺口识别需要考虑光照变化和图片压缩噪声,那已经是另一个量级的问题,不建议在 DEMO 阶段投入。

4.3 二次校验失败的排查顺序:先看回调,再看签名,再看超时

DEMO 里最让人头疼的不是写代码,而是滑块明明拖过了,登录却还是被拒。下面是这套链路上最常出现的几个情况:

现象优先排查点
滑块拖完了但登录请求里没有 validate检查 success 回调是否真的执行,隐藏字段有没有被赋值
后端收到请求但校验接口返回参数错误确认 captchaId、token、validate 三个字段都传了且没有串位
校验接口返回签名错误检查参数排序规则、空值过滤和 secretKey 是否与后台一致
滑块通过、签名正确、接口也返回了,但登录还是失败看后端有没有拿到 result 之后再走 login 流程,还是直接忽略了返回值

排查时建议在后端加一条请求日志,分别记录“前端回调拿到 validate 的时间”和“后端调用易盾校验接口的响应时间”。如果两者间隔超过几秒,多半是前端把 validate 塞隐藏字段的动作放在了异步回调里,用户已经点了登录按钮但字段还没赋值。

还有一个常见的低级错误:后端写校验逻辑时只判断 HTTP 状态码是 200 就放行,没有解析result字段的实际值。HTTP 200 只代表接口通了,不代表校验通过。这个坑在 DEMO 阶段就要打好标记,避免带病上线。

5. 把 DEMO 抽成可替换的验证码接入层

5.1 用适配器接口隔离开业务代码与验证码厂商

DEMO 跑通之后,下一步是把它从“能跑的页面”变成“能维护的模块”。常见的做法是抽象一个验证码适配器接口,把易盾相关的逻辑全部封装在实现类里,业务代码只依赖接口。这样将来换别家验证码,或者同一个项目里不同环境用不同厂商,都不需要动登录路由。

class CaptchaAdapter: def verify(self, token: str, validate: str, **kwargs) -> bool: raise NotImplementedError class YidunCaptchaAdapter(CaptchaAdapter): def __init__(self, secret_id: str, secret_key: str, captcha_id: str): self.secret_id = secret_id self.secret_key = secret_key self.captcha_id = captcha_id def verify(self, token: str, validate: str, user_data: str = "") -> bool: params = { "secretId": self.secret_id, "captchaId": self.captcha_id, "token": token, "validate": validate, "timestamp": int(time.time() * 1000), "data": user_data, } params["signature"] = make_sign(params, self.secret_key) resp = requests.post(CHECK_URL, data=params, timeout=5).json() return resp.get("result") is True def get_captcha_adapter() -> CaptchaAdapter: # 从配置中心读取厂商类型,按需实例化 if config.captcha_provider == "yidun": return YidunCaptchaAdapter( secret_id=config.yidun_secret_id, secret_key=config.yidun_secret_key, captcha_id=config.yidun_captcha_id, ) raise ValueError(f"unknown captcha provider: {config.captcha_provider}")

接口的verify方法只暴露 token 和 validate 两个业务语义明确的参数,调用方不需要知道签名怎么算、接口地址是什么。get_captcha_adapter这个工厂函数负责根据配置决定用哪家实现,换厂商时新增一个类,改一行配置。

最后补一个自检脚本的验证技巧:写两个测试用例,一个用空字符串 validate 调用 verify 断言返回 False,另一个把浏览器里手工滑出来的 token 和 validate 喂进去断言返回 True。每次改动签名逻辑或者参数组包规则后,跑一遍这两个用例就能确认链路没有断。把密钥从代码里抽到环境变量的最早时间点,就是在写这个适配器的时候。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询