ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

从刷课脚本失效看浏览器自动化的攻防演变与技术边界

从刷课脚本失效看浏览器自动化的攻防演变与技术边界 我的一个练手项目“学习通浏览器刷课脚本已失效”已经在仓库里闲置了大半年最近翻出来整理旧代码时顺手给标题加上了“已失效”三个字。趁着模块化回忆还热乎我想把整个项目从能用、到失效、再到彻底放弃的完整过程复盘一遍也算是对自己踩过的坑做一个交代。先说清楚这篇文章不是一份“刷课教程”我也不会贴一些能直接拿去跑、绕过平台检测的代码。我更想聊的是一个浏览器端的自动化脚本是怎么工作的它后来为什么会失效平台的反制机制到底升级了哪些东西如果你也写过用户脚本Tampermonkey、暴力猴这类或者正在做前端、自动化测试、爬虫相关的方向这篇复盘应该能给你一些实打实的启发。1. 先搞明白这类脚本当初是怎么跑起来的1.1 刷课脚本到底在解决什么问题学习通是国内高校相当常用的在线学习平台课程视频有学时要求看完才能拿平时分。问题在于一门课动辄几十个视频每个十几分钟到几十分钟不等而且平台不允许直接拖动进度条也不允许倍速过快。手动一集一集点开播放消耗时间不说还极度枯燥。这种情况下“刷课脚本”这个需求就出现了。它的核心目标很简单让浏览器自动完成“打开视频 → 播放 → 等待进度走完 → 切换到下一集”的重复劳动。早期版本甚至不需要多复杂的技术一个定时器加几行DOM操作就能跑通。我当时写这个脚本的想法也很朴素用用户脚本管理器注入一段JS在课程页面里自动定位未完成的视频节点触发播放再定期上报播放状态。平台页面本身是常规的前后端分离架构视频播放器也是常见的HTML5视频播放器这种脚本的基本假设是“我模拟的本来就是真实用户在浏览器里的操作平台没理由区分我和真人”。在那一阶段这个假设是成立的。因为早期的学习通页面JS代码几乎没有做任何保护所有按钮、章节节点都有稳定的class名和id接口参数也都是明文浏览器里能看到什么脚本就能操作什么。1.2 脚本的三种常见形态随着需求变多“刷课”的实现方式也分化出了几种不同的形态。我周围有人用过Python加Selenium也有人写浏览器扩展但最常见、成本最低的还是用户脚本。下面这张表可以清楚看出它们的区别实现方式技术门槛分发成本隐蔽性维护难度用户脚本Tampermonkey等低只需懂JS和DOM极低一个脚本链接就能用一般运行在页面里可被检测改动快跟着页面走浏览器扩展Chrome Extension中需要了解扩展API较高需要打包或上架较高权限独立页面改动时也要同步改Python Selenium中高需要掌握自动化框架较高需要装Python环境较低浏览器环境异常容易被识别依赖浏览器驱动维护成本高用户脚本能成为主流选择核心原因在于“成本”二字。Tampermonkey这类脚本管理器已经把注入、权限管理、资源加载这些问题都封装好了写一个刷课脚本本质上就是写一个网页上的辅助脚本和给某个网站写一个“自动签到助手”的难度差不多。而浏览器扩展虽然权限更大、更稳定但开发、分发都要麻烦得多。2. 核心原理拆解播放器自动化到底做了什么2.1 播放器自动化的三个基本动作从技术角度看刷课脚本的操作对象就是页面里的HTML元素和浏览器原生API。我当时把脚本拆成了三个核心动作自动点击和遍历章节。学习通的课程章节列表长什么样本质是一个嵌套的树形DOM结构每个章节节点挂在对应的层级下面已完成和未完成的状态通常体现在class名或子节点上。脚本要做的就是用querySelectorAll把所有待学习的章节节点捞出来找到第一个没有打上“已完成”标记的节点执行click()触发播放。控制播放器行为。视频元素加载出来之后脚本需要拿到video标签然后处理自动播放、音量、倍速这些问题。浏览器有自动播放策略带声音的视频经常被拦截所以脚本一般会把muted设为true再调用play()。倍速方面playbackRate可以调到1.5或者2但有些课程会校验倍速太快反而容易被标记。监听播放进度和切换视频。一个视频播完播放器会触发ended事件脚本监听这个事件然后去点击下一个章节节点。如果平台有“上一集下一集”的按钮也可以直接模拟点击。一段极简的老版思路大概长这样// 早期版本中最朴素的逻辑现在早就被风控拦死了 setInterval(() { const video document.querySelector(video); if (video video.paused) { video.muted true; video.play().catch(() {}); } // 让进度条走快一点平台监控不严的时候能用 if (video video.currentTime 5 video.duration) { video.currentTime 5; } }, 3000);这段代码放到现在的学习通页面上基本没有意义因为接口校验和行为检测早就把这类操作识别出来了。但它很能说明问题刷课脚本的“自动化”本质上不是攻击某个深层的漏洞而是把真实用户在页面上会做的操作用一批JS语句机械化地复现出来而已。2.2 进度上报真正难缠的部分很多人以为刷课脚本的难点在于“让视频播放”其实不是。真正麻烦的是平台的进度上报机制。学习通不会只看浏览器端播放器的状态就给你计入学时它有一套向后端上报的机制通常是一个持续的心跳请求。播放器每播几秒就会向后端发送一条记录包含当前视频的ID、当前播放位置、播放状态、设备信息等。后端根据这些心跳数据累加时长最终决定你这节课算不算“学完”。早期这个接口的参数非常暴露// 早期心跳接口POST /api/video/heartbeat { courseId: xxx, videoId: yyy, progress: 30, duration: 60, playedTime: 35 }这种接口的设计默认是信任前端传上来的数字。所以我当时的脚本里可以直接修改playedTime的值让一次心跳“吃掉”更多进度从而实现比真实播放快得多的时间积累。平台后来显然意识到了这个问题接口参数开始增加签名和时间戳校验例如// 后期心跳接口多了一堆签名和风控字段 { courseId: xxx, videoId: yyy, progress: 30, duration: 60, playedTime: 35, platform: 1, devInfo: 浏览器指纹相关数据, ts: 1691234567, sign: 一串由多个参数拼接后生成的哈希值 }签名算法的加入让“篡改进度”这件事的成本突然高了很多。因为服务端不再接受裸参数所有关键字段必须按照特定规则拼接、加密、生成签名脚本必须先弄清楚签名算法才能构造出合法请求。与此同时平台也会对心跳间隔做合理性校验——如果每次心跳上报的时间间隔完全一致、且长期没有任何鼠标和键盘操作痕迹这套模式就很可疑。2.3 为什么脚本都爱挂在浏览器里既然刷课脚本本质是页面操作那挂在浏览器里就是最自然的选择。用户脚本管理器提供了一个很关键的机制你可以通过match规则声明脚本只在某个域名下运行这样平时浏览其他网站时脚本完全不会生效也就降低了对日常上网的干扰。另一个优势是调试方便。开发者工具是现成的控制台、Network面板、元素面板全都能直接用。脚本出问题的时候我不需要像调Selenium那样反复重启浏览器只要刷新页面、打个断点就能看到哪一步挂了。对于这种需要频繁跟着平台页面变化做调整的脚本来说这个开发体验至关重要。当然用户脚本也有它的致命弱点运行在页面上下文中就意味着它的行为是有迹可循的。页面里的JS完全可以通过一些标志性特征来判断“当前环境是否被脚本注入”比如检测某些DOM节点的变化频率、检查MutationObserver的监听结果、或者干脆在页面里埋陷阱诱导脚本暴露自己。3. 脚本失效的真相学习通反制机制的三层升级3.1 第一层页面代码从“裸奔”到打包混淆我的脚本第一次失效原因是页面DOM结构变了。早期的学习通课程页面章节节点有很清晰的class名和层级脚本里的选择器写起来非常舒服比如.chapter-list .chapter-item.finish。但在某次平台改版之后页面变成了webpack打包的大文件所有的类名、变量名被压缩或混淆成了随机字母。更麻烦的是很多原来静态写在HTML里的节点变成了动态渲染加载时机不同DOM结构也不同。脚本作者唯一的应对办法就是不断打开元素面板重新定位节点更新选择器。这个过程非常枯燥而且每次平台改动脚本都得跟着改一轮。我当时还遇到过一种更恶心的情况同一个class名在不同课程页面里含义完全不同导致脚本某个课程能用、换个课程就报错。这一层反制其实不算“智能”它只是提高了脚本维护的体力成本。3.2 第二层接口层加入签名与风控参数当平台发现光改前端无法阻止脚本时重心就从浏览器端转移到了服务端。接口层的变化是最直观的。之前的心跳接口随便传点参数就能通过后来开始加sign、ts、devInfo这些字段。sign的算法是从一个比较长的字符串拼接结果做哈希得到的ts是当前时间戳devInfo则是浏览器指纹相关的信息比如User-Agent、canvas指纹、WebGL渲染器信息、屏幕分辨率等。服务端拿到请求后会先校验签名是否正确、时间戳是否在合理范围、设备指纹是否在短时间内出现过多次异常。如果这三项任意一项不对服务端就可能静默拒绝这条心跳或者返回一个错误码但页面上照样显示“播放中”让用户和脚本都毫无察觉最后等你要拿学时的时候才发现进度一分没涨。这种“静默失败”的模式非常讨厌。因为脚本没有任何报错播放器也在正常转但后端就是不认账。如果只看页面表现你可能永远不知道问题出在哪儿。3.3 第三层从行为层面识别“非真人”页面混淆提升的是脚本编写成本接口签名提升的是请求伪造门槛但真正让刷课脚本“凉透”的是平台开始采集和分析用户行为数据。学习通这类平台在之后的版本里陆续加入了事件埋点会记录用户在页面内的鼠标移动轨迹、点击坐标、滚动频率、键盘输入甚至页面获得焦点和失去焦点的时间。如果服务端发现一个账号在观看视频期间没有任何鼠标轨迹或者滚动行为非常规律就会把这个账号标记为“异常学习行为”。在做技术复盘的时候我觉得这个设计思路是很有代表性的签名校验是静态的规则固定下来就能被逆向。但行为特征是动态的、连续的真人操作不可能像脚本那样精确重复。平台只需要收集足够多的行为样本然后用规则或简单的模型计算出“这是一个真实在屏幕前学习的人”的概率就足以拦截绝大多数机械化的刷课脚本。再往后平台还加入了人脸识别和随机的弹窗验证。这些机制一出来刷课脚本的处境就变得非常尴尬你没法模拟一张真实的脸也很难在随机时间点保证自己正好有空去点验证码。很多脚本作者在这个阶段选择放弃我的脚本也是在那时彻底跑不动了。4. 现场排查实录我是怎么确认它彻底失效的4.1 第一步确认脚本有没有注入成功既然要复盘一次失效过程就得走一遍实际排查的流程。我当时是从GitHub上拉了一个还在更新的刷课脚本打算观察它到底是怎么“死”的。没想到从第一步就卡住了。打开课程页面后我先去Tampermonkey的弹窗里确认脚本有没有加载发现脚本图标正常亮着说明match规则匹配成功。接着打开控制台准备看脚本有没有打日志结果控制台里干干净净连一条GM_log的输出都没有。这就很反常因为正常情况下至少会有初始化日志。后来我把脚本暂停再启用刷新页面时才看到控制台里报了一大片跨域错误和未捕获的异常。跨域错误指向的是某个第三方接口未捕获异常则来自页面内部的某个加密模块。这说明脚本虽然注入了页面但它依赖的某个后端接口已经在域名或协议上做了调整脚本里写死的接口地址已经失效了。这个步骤最重要的启示是排查脚本失效问题先分清“脚本没跑起来”和“脚本跑起来但被拒绝”是两码事。如果是前者去看控制台报错和网络请求如果是后者大概率是风控或签名校验的问题。4.2 第二步顺着Network面板找断点控制台没有有效报错但脚本就是不行的时候就该打开Network面板看网络请求。刷新页面找到播放器的心跳请求。我当时观察到一个非常核心的现象点击播放以后点击播放的按钮事件能正常发出播放器也确实进入了播放状态但心跳接口的请求频率异常的稀疏而且第一次请求就返回了403 Forbidden。这个状态码说明请求到了服务端但在服务端的网关层就被拦了下来。查看请求头以后发现脚本并没有带上页面运行环境里生成的某些加密Header而正常播放器发出的请求是携带这些Header的。脚本作者可能还没更新到最新的加密逻辑所以构造出的请求在服务端看来就是一个“来历不明”的请求。顺便提一个容易忽略的细节有些时候请求返回的是200但响应的JSON里藏着一个code: 40001之类的业务错误码。所以排查的时候不要只看HTTP状态码响应体里的业务逻辑也需要扫一眼。我之前就遇到过好几次这种“假成功、真失败”的情况。4.3 第三步对比代码和平台端的差异到了这一步排查就进入逆向阶段了。我会在播放器的JS文件里搜索heartbeat、sign、ts这些关键词通过调用栈找到签名生成函数的大致位置。这一步不复杂但很费时间因为代码是混淆过的你只能一点一点读逻辑。对比完差异以后我大致确认了三个主要失效点失效点原因影响页面DOM节点变化平台改版class名和节点结构完全变了脚本拿不到正确的播放器元素心跳接口新增签名参数签名算法未知无法构造合法请求服务端拒绝心跳进度不上涨行为检测升级鼠标轨迹、焦点事件等被风控模型分析即使请求合法账号也被标记异常这三个失效点叠加在一起意味着如果要继续更新脚本我不仅要逆向新的DOM结构还要逆向一套混淆后的签名算法同时还得想办法模拟真人行为特征。这个工作量已经不是“周末顺手改改”能解决的了。4.4 最后的结论不是不能修是成本太高我见过不少技术不错的开发者确实靠逆向签名和模拟行为把脚本恢复过一段时间。但问题是平台方有全职的安全团队可以随时调整参数算法、修改加密逻辑而脚本作者只能利用业余时间被动跟进。今天修好的接口到下个版本可能又以新的方式失效了。刷课脚本的维护本质上是一场“无限游戏”平台只要一直更新你就得一直追而且双方投入的时间和精力完全不对等。所以在那个节点我认为这个项目已经不值得继续投入直接归档并标注“已失效”是性价比最高的选择。5. 攻防之外普通开发者能从中学到什么5.1 前端加密的边界在哪里这段攻防经历最大的收获是对“前端加密”这件事有了更清醒的认识。前端代码跑在用户的浏览器里无论你怎么混淆、加密、加壳只要运行环境是开放的就必然可以被分析和复制。前端加密能提升攻击者的成本但无法提供绝对的安全性。真正的安全边界在服务端。学习通最后的反制手段也不是靠JS加密而是靠行为风控、人脸识别这类“需要真实用户参与”的机制。这个思路对做Web开发的很有参考价值不要把你的业务规则和信任依据全部放在前端重要的校验必须下沉到服务端并且要叠加一些服务端才能判断的维度。另一个值得学的地方是风控系统的设计逻辑。它不会只因为单个异常就封死你而是通过多个维度的信号综合打分。某个请求签名不对可能只是忽略但如果请求频率异常、设备指纹频繁变化、行为轨迹又过于规律各项加权算下来就会触发更严厉的处置。这种“多因子评分”的思路在很多业务场景里都能复用到。5.2 浏览器自动化的能力与局限写刷课脚本的经验和现在做E2E自动化测试、写爬虫的经验其实有不少相通之处。Puppeteer和Playwright这类无头浏览器工具的核心能力同样是模拟用户操作、控制页面行为。但它们在反爬能力强的站点面前同样容易暴露。无头浏览器会被识别的点非常多navigator.webdriver属性、窗口尺寸异常、浏览器启动参数中未被处理的痕迹、canvas或WebGL渲染结果与真实浏览器不一致、字体列表缺失等。之前热搜词里还出现过“浏览器指纹”和“设备指纹”相关的内容本质上都是同一类技术问题——你在客户端留下的任何可观测特征都可能成为被识别的依据。所以如果你要写一个长期稳定运行的自动化程序不能把希望全部寄托在“模仿用户操作”上还需要考虑更底层的环境伪造和更上层的策略调度。但这已经远远超出刷课脚本的范畴了。5.3 用户脚本管理器的安全隐患这个项目让我对用户脚本管理器也有了新的认识。Tampermonkey这类工具本身没有问题但用户脚本社区里鱼龙混杂很多脚本看起来是帮你刷课、抢课、签到背后可能藏了恶意代码。用户脚本的API权限是很高的比如GM_xmlhttpRequest可以发起跨域请求GM_setValue和GM_getValue可以在脚本域内存储数据某些脚本甚至可以通过grant声明访问页面所有资源。如果你装了一个来源不明的脚本它完全可以读取你的cookie、监听你输入的密码、把你账号信息发到某个第三方服务器。所以在安装任何用户脚本之前一定要看一眼脚本的权限声明确认它只用到了完成任务所需的最少API。这个习惯比很多安全工具都管用。6. 避坑与反思刷课这件事的账要算清楚6.1 那些容易被忽略的隐性风险聊完技术我想再认真提醒一点风险。刷课脚本本身不是一个“零成本”的操作它的隐性代价被很多人低估了。第一是账号安全。学习通账号通常绑定学号和实名信息一旦被判定为异常学习轻则课程成绩作废重则影响账号在平台上的正常使用。这个损失比脚本省下的那几十个小时大得多。第二是个人信息风险。很多刷课脚本是个人开发者在维护脚本会抓取哪些数据、存到哪里、会不会泄露用户根本无从知晓。为了刷几节课把自己的账号密码甚至身份证信息暴露给第三方这笔账怎么算都不划算。第三是平台协议风险。学习通的使用协议明确禁止非官方的自动化操作刷课脚本本质上是在绕过平台的正常学习记录机制。虽然这在很多学生眼里不算什么严重问题但从契约角度看它确实违反了平台规则。6.2 比刷课更划算的“偷懒”方案说句实在话网课学习的核心目的不是“挂机凑时长”而是掌握课程内容。但如果你实在觉得某个课程没必要投入那么多精力我建议先看看平台有没有官方倍速播放功能。学习通近年来已经支持倍速播放这个功能是合法的不容易触发风控只是不能跳进度。如果想要快速完成学习合理的做法是把观看时间拆分成多个小任务穿插在一天的空闲时段里后台播放着课程顺手做点别的事情听个响也算有了基本的印象。这个方案虽然不如脚本“爽”但胜在零风险。如果你对自动化的技术本身感兴趣我推荐换一个更合规的练手方向例如写一个自动备份网盘文件的小工具、做一个自动整理下载目录的脚本或者研究一下浏览器扩展开发。这些方向同样能学到DOM操作、事件监听、网络请求分析这些能力而且不用冒着账号风险和道德风险去做。我在徒手排查Network面板、逆向签名参数的过程中学到的东西比刷完十门网课都值。如果你眼前也有一个类似的“捷径”我的建议是把研究它的时间用来研究它背后的原理亏不了。
返回列表