Selenium、代理IP与原生POST融合:抢购脚本的工程化架构
2026/9/16 20:54:49 网站建设 项目流程

简介:一套面向有Java基础开发者的抢购辅助脚本,聚焦ibox二级科技场景,通过代理IP与Selenium自动化,解决接口数据加密、手动提交耗时等问题。源码包共10个文件,含4个Java核心代码、2个XML配置、1个readme文档、1个IP池文本、1个manifest清单及chromedriver.exe驱动,整体仅5.99MB,小巧易部署。已有512人学习下载。脚本实现getip文件缓存以节省代理费用,借助Selenium调用网站内js解密方法获取商品列表,并预留机器人滑动验证模块(当前未完成)。对研究Java模拟请求、逆向解密逻辑或自动化抢购的开发者,这份代码提供了可直接运行的骨架和排错思路,便于继续补全验证流程并接入实际业务。

1. 抢购 ibox 二级科技脚本为什么把 Selenium、代理 IP 和原生 POST 拼在一起

在 ibox 二级科技的抢购场景里,最大的坑不是手速,而是前端 JS 把请求加密了。直接从控制台复制 cURL 会发现请求体是密文,改了商品 ID 却带不过去,因为加密令牌绑定了时间戳和页面会话。用 Selenium 一路模拟点击虽然能过加密,但每个动作都走浏览器渲染,延迟高、IP 容易暴露,多账号操作马上触发风控。这个项目拆了一个更务实的架构:用 Selenium 只做“解密窗口”和“登录态”,用 Java 原生 POST 做商品列表轮询和下单提交,再配合代理 IP 池轮换出口 IP。对有一定 Java 基础的人,这种“浏览器解耦”模式比纯 Selenium 自动化更容易控制节奏,也给未完成的滑动验证留了人工兜底的位置。

2. 用 Selenium 执行 window.de:让浏览器替你解密

2.1 为什么必须依赖浏览器而不是直接调 JS

拿到 ibox 前端的加密入口后,很多人的第一反应是把 JS 复制到本地,用 Java 的 GraalJS 或 Node 去调。两周后你会发现,页面改版时混淆逻辑变了,解密函数内部还会读取当前页面上下文里的 cookie、localStorage,甚至某些 DOM 元素,脱离浏览器后要么直接报错,要么解密出来是空字符串。

所以我更建议把“解密”这件事留在页面里:用 Selenium 打开真实页面,等待 window.de 被注入,然后通过 JavascriptExecutor 传参调用。这样解密函数的运行环境与线上一致,Token 和时间戳都是从页面上下文自动获取的,脚本这边只关心传什么密文、拿什么明文。

2.2 解密入口探测与最小 Java 实现

先用typeof window.de确认入口是否存在,再调用它。注入 JavaScript 时尽量避免把密文字符串直接拼进脚本字符串,因为密文里常有引号、反斜杠,拼进去很容易破坏 JS 语法。用arguments[0]传参就不会有这个问题。

// decryption_demo.java 片段 JavascriptExecutor js = (JavascriptExecutor) driver; // 第一步:确认 window.de 是否已经挂载 String deType = (String) js.executeScript("return typeof window.de;"); if (!"function".equals(deType) && !"object".equals(deType)) { throw new IllegalStateException("window.de 未注入,检查页面是否加载完成"); } // 第二步:根据不同类型调用解密入口 String plain = (String) js.executeScript( "var fn = window.de; " + "if (typeof fn === 'function') { return fn(arguments[0]); } " + "if (fn && typeof fn.decrypt === 'function') { return fn.decrypt(arguments[0]); } " + "return '';", encryptedPayload );

这段代码做了三件事:先探测入口类型,再按照“函数”或“对象方法”两种形式调用,最后在调用失败时返回空字符串而不是抛异常。如果你第一次跑就返回 null,优先检查页面是不是被 iframe 包裹,window.de 是否挂载在 iframe 而不是主页面。若是 iframe,先driver.switchTo().frame("frame_id")再执行 JS。

2.3 解密结果的传递:长 JSON 别直接 return

解密结果通常是一个 JSON 对象,直接return会被 Selenium 自动序列化成 JSON,但它一旦包含 undefined、循环引用或大量转义符,驱动端就可能拿到失败结果。常见做法是先在页面里JSON.stringify,再以字符串方式返回,Java 侧再解析。

解密方式动态参数获取维护成本延迟适用阶段
浏览器控制台手工复制手动补参数人工接口调试
Selenium 动态执行页面环境自动补充完整链路
Java 反混淆重写算法需维护各种密钥和算法迭代长期高频请求

这三种方式里,Selenium 动态执行是成本和成功率最平衡的。手工复制适合一开始分析字段,Java 重写适合后续优化速度,但项目开发初期不建议碰。

// 对象转字符串返回,避免序列化边界问题 String json = (String) js.executeScript( "var data = window.de(arguments[0]); " + "if (data === null || data === undefined) return ''; " + "return typeof data === 'string' ? data : JSON.stringify(data);", encryptedPayload );

注意这里encryptedPayload是外部传入的密文,通过arguments[0]映射到页面内的arguments[0]。不像拼字符串那样容易造成引号冲突。拿到json后,后期会结合商品列表和下单参数一起使用,这部分在第 4 章展开。

3. 代理 IP 池与 ip.txt 文件缓存:把取号成本压下来

3.1 从代理接口取号并写入 ip.txt

项目里getip的用途很直接:从代理商拿一批 IP 写进ip.txt,Selenium 和 HttpClient 每次请求都从这份文件里取。之所以做文件缓存,是因为代理商按次计费,高频轮询商品列表时如果每次都重新取号,几十个请求就会把一个月的额度刷完。

常见代理商会返回类似下面这种结构的 JSON,不过字段名可能有差异,以你的代理商文档为准:

{ "code": 200, "data": { "proxy_list": ["1.2.3.4:8080", "5.6.7.8:9090"], "expire_time": 1710000000 } }

把取号结果写入本地缓存的逻辑可以做成这样:

private void refreshIpCache() throws IOException { String api = "http://你的代理商接口/getip?num=20&format=json"; String jsonBody = doHttpGet(api); JSONObject obj = JSON.parseObject(jsonBody); JSONArray list = obj.getJSONObject("data").getJSONArray("proxy_list"); // 在每行末尾追加过期时间,避免下次读的时候猜不清是否失效 List<String> lines = new ArrayList<>(); for (int i = 0; i < list.size(); i++) { lines.add(list.getString(i) + "#" + obj.getJSONObject("data").getLongValue("expire_time")); } Files.write(Paths.get("ip.txt"), lines, StandardCharsets.UTF_8); }

doHttpGet可以用 Apache HttpClient 实现,也可以用URL.openConnection(),关键是别自己拼 socket。上面这段把expire_time跟着 IP 一起写进文件,之后读取时就能直接判断是否过期。如果你用的是多个代理组合,expire_time 不同,可以把取号接口返回的每个代理单独带上过期时间。

3.2 带过期时间读取缓存与轮换队列

ip.txt的每一行格式我建议定为ip:port#unix_timestamp,字段说明如下:

字段示例说明
IP 地址和端口1.2.3.4:8080代理请求入口
过期时间1710000000Unix 秒,超过即失效

读取并构建可用队列的代码:

// ProxyPool.java Queue<String> availableProxies = new ConcurrentLinkedQueue<>(); public void loadProxies() throws IOException { long now = System.currentTimeMillis() / 1000L; List<String> lines = Files.readAllLines(Paths.get("ip.txt"), StandardCharsets.UTF_8); for (String line : lines) { String[] parts = line.split("#"); if (parts.length < 2) continue; long expire = Long.parseLong(parts[1]); if (expire > now) { availableProxies.offer(parts[0]); } } }

这里用了ConcurrentLinkedQueue,因为抢购脚本通常有多个线程并发取号。队列的poll()取头一个代理,offer()放回可用队列。如果某个代理连续失败三次,就不要放回,而是从队列中移除,避免一个坏 IP 反复重试。

3.3 把可用代理接进 Selenium 和 HttpClient

代理池不只是给 HttpClient 用的,Selenium 的浏览器访问也需要切换 IP。Selenium 启动时可以通过ChromeOptions设置代理:

ChromeOptions options = new ChromeOptions(); options.addArguments("--proxy-server=http://" + proxyHost); WebDriver driver = new ChromeDriver(options);

HttpClient 侧设置代理更灵活,因为可以每次请求前从队列拿一个 IP:

HttpHost proxy = new HttpHost(ip, Integer.parseInt(port)); RequestConfig config = RequestConfig.custom() .setProxy(proxy) .setConnectTimeout(1500) .setSocketTimeout(3000) .build();

连接超时设置短一点是有意的。代理 IP 不可用时,1500 毫秒内直接失败,然后立刻换下一个代理。如果超时设成 10 秒,抢购窗口早过去了。Socket 超时 3 秒,是防止某个代理响应缓慢把请求阻塞住。这块直接决定脚本的稳定程度,建议在正式跑之前先用一个小的地址探测脚本验证代理连通率。

4. 商品列表解析与 POST 下单:混合会话链路

4.1 解密后的商品列表怎么映射成 Java 对象

第 2 章拿到的json字符串就是商品列表明文,字段一般是goodsIdskuIdpricestock之类。用 fastjson 或 Jackson 解析成 Java 对象即可。这里直接操作 JSONObject,避免为核心数据写一个完整 POJO:

// goods_list_demo.java JSONArray goodsList = JSON.parseArray(plainJson); for (int i = 0; i < goodsList.size(); i++) { JSONObject item = goodsList.getJSONObject(i); String goodsId = item.getString("goodsId"); int stock = item.getIntValue("stock"); if (stock <= 0) continue; // 只把有库存的商品 ID 放入待抢队列 pendingGoods.offer(goodsId); }

pendingGoods是一个阻塞队列,后续下单线程从这个队列取商品 ID,不需要在列表解析线程里直接发起请求。这样能实现“一边轮询列表一边提交订单”,响应延迟能降低不少。实际解析时字段名可能不是驼峰,注意看解密后的原始 key。

4.2 为什么提交必须走 POST:get 和 post 习惯的差别

很多人在拿到抢购接口后,习惯性把参数拼到 URL 后面,这是 GET 思维。GET 适合查询,参数会出现在 URL 和访问日志里;POST 适合提交订单,参数放在 Request Body 中,并且需要Content-Type与请求体格式匹配。抢购 ibox 二级科技这类操作会改变库存状态,只能走 POST。

GETPOST
参数位置URL Query StringRequest Body
幂等性是,多个请求一般不会改变状态否,会创建或更新订单
调试难度浏览器直接访问即可需要检查 Body 和响应状态
抢购场景拉取列表可以用下单必须用

用 Apache HttpClient 发送 POST 请求的代码:

// submit_order_demo.java HttpPost post = new HttpPost(ORDER_URL); post.setConfig(config); post.setHeader("Content-Type", "application/json;charset=utf-8"); post.setHeader("Referer", "https://目标页面"); JSONObject body = new JSONObject(); body.put("goodsId", goodsId); body.put("count", 1); body.put("sign", encryptedSign); post.setEntity(new StringEntity(body.toJSONString(), StandardCharsets.UTF_8)); try (CloseableHttpResponse resp = httpClient.execute(post)) { int status = resp.getStatusLine().getStatusCode(); String result = EntityUtils.toString(resp.getEntity(), StandardCharsets.UTF_8); // 返回状态非 200 时打印响应,便于分析是否被风控 }

encryptedSign不能直接写明文,它需要在页面上下文里通过 window.de 生成。一种做法是下单线程拿到goodsId后,调用js.executeScriptgoodsId和时间戳交给window.de,拿到返回值再拼接订单 body。这样就能保证签名算法每次都能命中。

4.3 会话打通:把 Selenium 的 Cookie 迁移到 HttpClient

实际跑单时,Selenium 负责登录和解密,HttpClient 负责高频 POST。两者必须共享同一个登录会话。最容易出错的地方就是把 Selenium 里的 Cookie 直接塞给 HttpClient,但忘了设置 domain 和 path,导致请求出去只带着空 cookie。

正确做法是从 Selenium 读取 Cookie,再逐个复制到 Apache HttpClient 的 CookieStore:

// cookie_sync_demo.java CookieStore cookieStore = new BasicCookieStore(); for (Cookie ck : driver.manage().getCookies()) { BasicClientCookie clientCookie = new BasicClientCookie(ck.getName(), ck.getValue()); clientCookie.setDomain(ck.getDomain()); clientCookie.setPath(ck.getPath()); clientCookie.setExpiryDate(ck.getExpiry()); cookieStore.addCookie(clientCookie); } CloseableHttpClient client = HttpClients.custom() .setDefaultCookieStore(cookieStore) .setDefaultRequestConfig(config) .build();

cookieStoreconfig都是之前构建好的对象。迁移以后,HttpClient 发出的请求就能带上 Selenium 页面里生成的登录态。要注意,cookie 里的 HttpOnly 标记不会影响这里,因为BasicClientCookie不关心原始标记,只有 domain 匹配才会被使用。如果你发现请求返回未登录,第一件事就是确认 domain 是不是丢了点前缀,比如backend.xxx.com.xxx.com的匹配范围完全不同。

5. 滑动验证未完成时的工程化兜底:人工滑块与 Cookie 回传

5.1 把“未完成”变成“人工介入”

项目里写着“通过机器人滑动验证(未完成)”,我认为这个结论是对的。滑动验证的核心不只是拖一条轨迹,后端还会校验浏览器指纹、缺口识别结果和时间曲线。与其在未完成的算法上死磕,不如把它当成暂停信号。

我一般这么设计:HttpClient 下单时如果返回某个错误码,表示滑块风控触发,就把当前订单请求挂起,同时调起 Selenium 窗口,让浏览器停留在需要滑块的页面,然后人工拖一下。Java 端轮询滑块元素是否消失,消失后再继续后续请求。

5.2 检测滑块出现并等待人工完成的代码

// captcha_wait_demo.java JavascriptExecutor js = (JavascriptExecutor) driver; boolean appeared = false; try { WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(5)); WebElement captcha = wait.until( ExpectedConditions.visibilityOfElementLocated( By.cssSelector(".captcha, .slide-verify, .geetest_slider") ) ); appeared = true; } catch (TimeoutException e) { // 当前没有滑块,继续提交即可 } if (appeared) { // 提示人工拖拽,阻塞等待验证码消失 System.out.println("出现滑动验证,请手动处理"); WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(60)); wait.until(ExpectedConditions.stalenessOf( driver.findElement(By.cssSelector(".captcha, .slide-verify, .geetest_slider")) )); }

这个方案不追求机器模拟,而是保证抢购流程不中断。检测到滑块后,把浏览器窗口保持在前台,人工处理完,代码检测到元素被移除或重新渲染后,再从 Selenium 读取最新的 Cookie 回填到 HttpClient。滑块通过后,验证服务一般会生成新的 Token 并写入 Cookie,不回填的话,下一次 POST 还是会失败。

回填代码就是把第 4.3 节那段再执行一次。要注意,验证出现时不能把旧 Cookie 清掉,否则人工拖完滑块后浏览器已经要重新登录,反而更慢。实际场景里,等滑块消失后,立刻执行driver.manage().getCookies(),然后重建 CookieStore,重发刚才失败的 POST。这样整套脚本就从“未完成滑块机器人”变成了“人工兜底 + 半自动抢购”,抢购窗口来临时,至少不会卡死在验证码这步。

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

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

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

立即咨询