文章目录
学车学到科目三,要我们在无忧乐行平台上看满 9 小时课程之后才能预约考试。这个平台非常恶心,不仅 UI 丑得没法看,而且在看视频的时候每隔一定时间会弹出验证码窗口,必须在指定时间之内完成验证;把网站标签页或窗口切到后台就会停止记录学时,必须保证前台观看。
魔高一尺,道高一丈,既然都是前端的小把戏,我们自然可以找到前端的解决方法。
TL;DR
最终解决方案非常简单,就三行代码……
window.inspect = function(){};
$(document).off('hide');
$(document).off('visibilitychange');
估计这是最简单的方案了。看到这里的你是不是有点智商被侮辱的感觉 😅……
已经上传 Greasy Fork 啦:无忧乐行刷学时。虽然这么简单可能没啥必要上传……
以下记录一下对这个网页的探索过程。
现存其他方案
最开始在 Greasy Fork 上找了一下,比较有用的是这个脚本:【刷学时】杭州科目驾驶培训网络课程。它的功能是「在任意切换窗口、浏览其他页面时会自动计时;如果有验证弹窗,发送桌面通知」。
然而在我使用的时候,时不时的弹窗验证通知还是让人非常不爽,而且这个脚本屏蔽「切换窗口停止计时」的手段好像是高频率地检测,这样会导致记录的学时少于实际的时间。总之不太理想。
于是晚上就开始研究了一下这个网站……
网站技术初探
这个网站后端是 JSP,估计是很 low 的外包公司,主要业务逻辑的 JS 代码都内联在 HTML 里。还有诸如 FuckInternetExplorer
这样的暴躁函数名(这搁 NOI 不得被禁赛几年)(虽然 IE 确实 👎),甚至锁进混用 Tab 和空格!
前端用了 jQuery,代码可以说非常杂乱(好在留了注释)。我以为这垃圾网站估计是十年前的,没想到代码里记录的最近更新日期是 2021-6-1……
探索「模拟请求」的方案
这种方法的探索失败了(或者说由于过于麻烦而放弃了)。可以跳过不看。
由于大量的业务逻辑放在前端,一开始我尝试研究了一下直接用前端发请求计学时的可能性。因为之前用过超星学习通小助手-视频专版这个脚本,这种直接发请求挂机的方式给我留下了深刻印象。
//签退 num为1时 强制 //随机验证
var todayEnd;
function signOut(type){
//判断是否在自动上传,如果在,则延后0.5S,防止同时进行后台自动屏蔽学时
if(!automaticUpload) {
//失败则跳转到课件列表页面
$(".tt_p").focus();
isInspect =true;
todayEnd=type;
suspend();
if(courseware.ruleDto.automatic == 1) {
validate(1,3);
}else {
interfaceForStu(3); //签退记录
}
}else {
setTimeout("signOut("+type+")", 500);
}
}
//视频验证接口 num: 1-签到 2-中间验证 3-签退
function interfaceForStu(num){
//获取验证方式
styleType = num;
// 20200728:视频验证是通过配置来选择走的海景还是云从,故这边代码无需改造
if(courseware.ruleDto.autoVerifyStyle==1){ //海景
//...
}else if(courseware.ruleDto.autoVerifyStyle==6){//2017.09.26 云从人脸识别
//...
}else if(courseware.ruleDto.autoVerifyStyle==3){ //回答问题
window.scrollTo(0,0);//滚动到顶部
page=$.layer({
//...
iframe : {
src : 'https://5u5u5u5u.com:443/learning/popup/inspect_ques2.jsp?type='+num+'&frequency='+courseware.ruleDto.verifyTimes+'&time='+courseware.ruleDto.verifyValidTime+'&verifyNotPassStyle='+courseware.ruleDto.verifyNotPassStyle+'&timeoutStyle='+courseware.ruleDto.timeoutStyle+'&resType='+courseware.resType
}
});
}else if(courseware.ruleDto.autoVerifyStyle==5) { //不用了
//...
}else if(courseware.ruleDto.autoVerifyStyle == 4) { //拍照
//...
}
}
interfaceForStu
这个函数就是弹出点击验证码的。可以看到点击签退时,弹出了一个浮层,尝试构造一个 src
去看看,比如 https://5u5u5u5u.com:443/learning/popup/inspect_ques2.jsp?type=2&frequency=3&time=3&verifyNotPassStyle=1&timeoutStyle=1&resType=1
。可以看到这个 iframe 内部的验证码页面:
形式是向后端请求一个图片,要求用户在指定时间内按照顺序用鼠标点击四个汉字的位置。
F12 审阅这个页面的代码。还是一样的风格,JS 内联在 HTML 里……(看第五行和第八行,一个 type 字段写了两遍,单双引号、空格风格还不统一…… 🤮)
//后台验证
function InpectvalidateCode(result){
$.ajax({
url:"/learning_json/inpectvalidateCode.action", //url需重定义
type : "post",
data : "result="+result,
async:false,
type:'post',
success:function(json){
if(json.msg !="success"){
nextInpect();
}else{
countFlag=false;
parent.validate(1,validateType,null, json.code);
try {
window.parent.closeCur();
}catch(e) {
}
}
}
});
}
//获取点击位置
function getSelPic() {
//...
}
$(function(){
//...
$(".queding_btn").click(function(){
countFlag = false;
var result = getSelPic();
InpectvalidateCode(result);
});
//...
});
点击「确定」按钮后,向后端发送了用户点击的坐标,然后拿到了后端返回的一个 code,传给 parent
(也就是 iframe 外层的 document)的 validate
函数,这个函数应该就是用来增加有效学时的。
回到视频播放页面:
//验证回调方法 validateType:1-签到 2-随机 3-中间验证
function validate(result,validateType,image, code){
//...
if(validateType==1){
//...
}else if(validateType==3){
layer.closeAll();
var valid=getValidTime();
var newVideoTime=getVideoTime();
//...
$.ajax({
url:"/learning_json/signOut.action?ttm=" + (new Date()).getTime(),
async:false,
data:"id="+courseware.id+"&result="+result+"&videoTime="+newVideoTime+"&valid="+valid+"&type="+todayEnd+"&lecturePage="+pageNo+"&verifyType="+courseware.ruleDto.autoVerifyStyle+"&stageId="+stageId+"&photoUrl="+image+"&stageEnd="+stageEnd+"&tag="+tag+"&code="+code+"&videoid="+videoid,
type:"post",
success:function(json){
if(result==-1){
faultMessage();
return ;
}
if(json=="success"){
endSucAlert();
}else if(json ){
//入库失败
//...
}else {
toLessonList();
}
}
});
}else if(validateType==2){ //随机验证
var _flag = true;
var valid=getValidTime();
var newVideoTime=getVideoTime();
//同步方法 async:false,
$.ajax({
url:"/learning_json/inspect.action?ttm=" + (new Date()).getTime(),
async:false,
data:"id="+courseware.id+"&result="+result+"&videoTime="+newVideoTime+"&valid="+valid+"&lecturePage="+pageNo+"&verifyType="+courseware.ruleDto.autoVerifyStyle+"&stageId="+stageId+"&photoUrl="+image+"&stageEnd="+stageEnd+"&tag="+tag+"&code="+code+"&videoid="+videoid,
type:"post",
success:function(json){
if(sucRedirect) {
endSucAlert();
return;
}
if(result==-1){
faultMessage();
return ;
}
if(json && json !="success"){
//...
}
}
});
if(result==1 && !sucRedirect && _flag){//验证结果返回成功
curState = cast;
validTime=validTime+valid;//随机验证已经有效的学习时间
setRandomTime();
goon();
}
}
//...
}
尝试修改 cast 变量,会发现右上角的计时器时间也会更改;假设改为 2 小时,点击签退,验证之后会发现弹窗显示「有效学时 2 小时」,然而进入课程列表刷新会发现实际更新的时间并不是 2 小时,而是实际进入页面的时间。
尝试了多种方式,用 BS 修改请求啥的,结果记录的时间永远是我实际打开视频页面的时间……看来后端有某种不太好搞的验证方式。暂时懒得研究了。
屏蔽「随机验证码」
接下来还是关注一下比较简单的前端 tricks 了。
首先是这个不定时弹出的「随机验证码」,可以找到这段代码:
//随机验证
function inspect(){
if(!automaticUpload) {
$(".tt_p").focus();
inspectUpload = true;
isInspect =true;
suspend();
interfaceForStu(2); //视频验证
}else {
setTimeout("inspect()", 500);
}
}
//显示计时时间
function startCount(){
//只有讲义
if(courseware.type==2){
//...
}
//非讲义
//alert(flag);
if(flag){
if(courseware.type!=2){
//按消耗时间计时
if(courseware.recordType==3){
operation(cast);
setStuTime(cast);
if(courseware.ruleDto.autoVerify==1)
if(randomTime==cast&&flag&&!endFlag){
inspect();
}else if(randomTime&&randomTime<cast){
setRandomTime();
}
if(courseware.ruleDto.autoVerify==3){
if(randomTime==cast&&flag&&!endFlag&&!validone){
inspect();
validone=true;
}else if(randomTime&&randomTime<cast){
setRandomTime();
}
}
if(courseware.ruleDto.autoVerify==2){
if(validonetime>73 && !validone){
inspect();
validone=true;
}else {
validonetime++;
}
}
}else if(courseware.recordType==2){
//...
}else if(courseware.recordType==1){
//...
}
}
}
//...
ds = window.setTimeout(function(){startCount();},1000);
}
看来这是个周期函数。如果 cast
(学时)达到 randomTime
就调用 inspect()
进行验证。经过实测 flag
变量如果为 false
则不会再计时……
那么,我们直接把 inspect()
函数弄成空,让它什么都不做不就好了。也就是:
window.inspect = function(){};
(足够简单粗暴……)
屏蔽「切后台检测」
继续看代码,发现这一段:
//笔记 评价 窗口最小化暂停视频播放
$(function(){
$('.xubox_close').css('display','none');
//最小化窗口暂停视频播放
$(document).on('hide', function() {
//...
layer.alert('您已离开当前页面,计时停止.', 8,function(index){
//...
});
});
});
很简单,只要把绑定在 document 上 hide
事件的监听器取消掉就好了。
然而其实还有另一个监听器(虽然没找到代码在哪里):visibilitychange
,通过 Chrome 的 F12 可以找到:
用 jQuery 的 off()
可以解绑这俩监听器:
$(document).off('hide');
$(document).off('visibilitychange');
大功告成!
遗留问题
好像如果把页面放在后台,这个学习时间的计时会慢一点,过了 15min 它好像只记了 9min。猜测可能和 Chrome 的后台节省内存的机制有关?