访问逻辑 - 推心置腹
网址第三题 访问逻辑 推心置腹 - 猿人学爬虫逆向练习平台Network 面板请求都是从哪里发出来的一、HTML 解析阶段浏览器自动发JS 还没运行浏览器解析 HTML 文档读到标签就自动发起网络请求此时 JS 脚本还没执行。link hrefxxx.css类型stylesheet触发HTML 解析器 parser作用下载 css 样式文件script srca.js类型script触发HTML 解析器 parser作用下载 JS 脚本文件只是把文件拿下来还没执行里面代码img src1.jpg、视频、字体、ico 图标类型img/media/font/icon触发HTML 解析器 parser✅共同点写在 HTML 标签里浏览器解析 HTML 就自动请求不是 JS 代码调用才发。二、JS 脚本运行阶段代码执行时发出请求逆向重点JS 代码跑起来之后由脚本主动发起网络请求来源标记都是 script。jQuery$.ajax()/$.get()/$.post()Network 类型XHR就是猿人学页面拿业务数据的接口原生浏览器fetch()Network 类型fetch动态创建标签发起请求const s document.createElement(script); s.src xxx.js; document.body.appendChild(s);类型script区别不是 HTML 写死的 script 标签是 JS 代码运行动态插入标签才去下载 js图片预加载、埋点上报new Image ().srcxxx类型imgJS 代码触发看【发起程序】面板可以看到调用栈追踪到具体是哪个 js 文件、第几行代码触发这个请求。三、其它类型document.cookie不会产生 network 请求只是请求头自动带上是 http 头字段不是独立请求。websocket类型wsJS new WebSocket () 打开长连接。文档主请求页面本身类型document第一次访问页面的 html。触发方式Network 类型是谁发起HTMLlink加载 cssstylesheetHTML 解析器 parserHTMLscript src下载 js 文件scriptHTML 解析器 parserJS 动态创建 script 标签加载 jsscriptJS 脚本运行$.ajax / XMLHttpRequestXHRJS 脚本运行fetch()fetchJS 脚本运行img图片imgHTML 解析器 parserJS new Image () 请求图片imgJS 脚本运行页面 html 本体document浏览器地址栏 / 跳转总结document请求/match/3拿到 HTMLparser 自动下载一批 css (stylesheet)、js (script) 静态资源js 全部下载完毕浏览器按顺序执行 js 代码执行混淆 JS计算出m赋值给document.cookie业务代码调用$.ajax()发出 XHR 接口/api/question/3业务源码script (function ($) { // 全局常量配置 var API/api/question/3, // 真正拿业务数据的接口地址 TP5, // totalPage 总页数固定5页 PS10, // pageSize 每页10条 p1; // 当前页码初始第1页 // jQuery元素缓存提前拿到页面上DOM节点避免反复查询DOM var $r$(#pgxPager), // 分页外层容器 $l$(#pgxList), // 数据列表容器 $pg$(#pgxPages), // 页码按钮容器 $pre$(#pgxPrev), // 上一页按钮 $nxt$(#pgxNext), // 下一页按钮 $kw$(#pgxKw), // 搜索输入框 $s$(#pgxStatus), // 状态文字就绪/请求中/加载完成 $dbg$(#pgxDebug); // debug调试文本框展示完整请求url /** * ui() 更新分页按钮状态 * 控制上一页、下一页是否禁用高亮当前页码 */ function ui(){ // 第一页禁用上一页最后一页禁用下一页 $pre.prop(disabled, p1); $nxt.prop(disabled, pTP); // 遍历页码按钮给当前页加上高亮class $pg.find(.pgx-page).each(function(){ $(this).toggleClass(is-active, $(this).data(page)p); }); } /** * loading() 加载状态开关 * ontrue请求中UI置灰按钮全部禁用防止重复点击 * onfalse结束请求恢复页面交互 */ function loading(on){ $s.text(on?请求中...:就绪); $r.css(opacity, on?0.92:1); $pre.add($nxt).add($pg.find(.pgx-page)).prop(disabled, on); } /** * render() 渲染页面数据 * a接口返回的数字数组 */ function render(a){ a(a||[]).slice(0,PS); // 最多截取PS10条 // 如果有数据循环渲染数字span没有就显示暂无数据 $l.html(div classpgx-line(a.length ? a.map(nspan classpgx-numn/span).join() : span classpgx-empty暂无数据/span)/div); } /** * req(np) 核心请求函数切换页码就执行这个 * npnewPage要跳转的目标页码 */ function req(np){ // 页码边界限制最小1最大TP5 pMath.min(TP,Math.max(1,np)); ui(); // 更新分页UI // window.request() 反爬钩子混淆JS挂载的全局函数用来做前置校验、更新window.match3 window.requestwindow.request(); // 组装GET请求参数 d var d{ page:p, // 当前页码 pageSize:PS, // 每页条数 kw:$.trim($kw.val()||), // 搜索框关键词去除前后空格 m:window.match3 // ⭐重点加密参数来自外部混淆JS赋值的全局变量window.match3 }; loading(true); // 开启加载状态 $dbg$dbg.text(GET API?$.param(d)); // debug框打印完整请求url$.param把对象转url查询字符串 render(Array(10).fill(加载中...)); // 页面先渲染10个“加载中…”占位 // ✅迷惑同步接口重点 $.ajax({url: /api2/3, async: false,}) // ✅真正拿数据接口async:false 同步阻塞ajax浏览器会卡住等待接口返回 $.ajax({ url:API, type:GET, dataType:json, data:d, timeout:1e4, // 超时10000毫秒10秒 async: false, // 同步请求浏览器等待接口返回才往下走逆向重点 success:function(res){ render(resres.data); // 成功把返回data数组渲染页面 $s.text(加载完成); }, error:function(){ failedAlert(接口异常请使用chrome浏览器开启无痕模式若错误持续请联系管理员) $s.text(接口异常请使用chrome浏览器开启无痕模式若错误持续请联系管理员); }, complete:function(){ loading(false); // 无论成功失败结束loading ui(); } }); } // 绑定页面点击事件 $pre.on(click, ()p1req(p-1)); // 上一页页码大于1调用req $nxt.on(click, ()pTPreq(p1)); // 下一页页码小于5调用req $pg.on(click,.pgx-page, function(){ req($(this).data(page)); }); //点击数字页码 $kw.on(keydown, ee.keyEnterreq(1)); //搜索框回车回到第1页查询 ui(); req(1); // 页面初始化直接执行req(1)加载第一页数据 })(window.jQuery);第一次请求会产生cookie第二次请求携带cookie可以请求到正确的数据。执行req()函数先执行第一句同步 ajax/api2/3浏览器发出 GET 请求访问/api2/3服务器收到这个请求在响应头返回Set‑Cookie种下校验 Cookieasync:false同步阻塞JS 代码停在这里必须等待这个请求完整响应、浏览器保存 Cookie 完毕才往下执行/api2/3请求完成浏览器已经把服务端下发的 Cookie 存入浏览器 Cookie 存储。紧接着执行第二个 ajax请求真正业务接口/api/question/3浏览器 HTTP 请求头自动把本机该域名下所有 Cookie 全部带上刚刚 /api2/3 种下的 cookie 就在里面后端校验两件东西 请求头里面由/api2/3种下的 cookie模拟后端源码from flask import Flask, request, make_response, jsonify import hashlib import uuid app Flask(__name__) # 模拟数据库存储 cookie令牌 和前端提交的m值的对应关系 # key:cookie_value, value:前端合法m标记 token_store dict() def md5(s: str) - str: return hashlib.md5(s.encode()).hexdigest() app.route(/api2/3) def api2_3(): 迷惑前置接口 浏览器先请求这个接口后端Set‑Cookie下发校验token async:false同步请求前端不处理返回内容 # 生成随机校验token通过Set‑Cookie给到浏览器 check_token str(uuid.uuid4()) token_store[check_token] {valid: False} resp make_response(, 200) # 后端下发Cookie浏览器自动保存后续请求自动带上 resp.set_cookie(yuanren_check, check_token) return resp app.route(/api/question/3) def question3(): 真正业务接口双重校验 校验1请求头Cookie里面的 yuanren_check 是否存在来自/api2/3下发 校验2url查询参数 m 是否合法模拟window.match3加密校验 # 1.取出浏览器带过来的cookie client_cookie request.cookies.get(yuanren_check) if not client_cookie or client_cookie not in token_store: # 没有调用/api2/3没有拿到后端下发cookie校验失败 return jsonify({data: [接口异常请使用chrome浏览器开启无痕模式...]}) # 2.获取url参数 m m_param request.args.get(m, ) page request.args.get(page, 1) # 模拟校验m真实环境这里是后端复现前端加密逻辑校验m # 真实场景后端拿到时间戳复现同样加密算法比对m这里简化演示 if not m_param: return jsonify({data: [接口异常请使用chrome浏览器开启无痕模式...]}) # ✅双重校验全部通过返回业务数据 mock_data [11111,22222,33333,44444,55555,66666,77777,88888,99999,100000] return jsonify({data: mock_data}) if __name__ __main__: app.run(port5000, debugTrue)import requests session requests.Session() # 第一步访问前置接口session自动接收Set‑Cookie保存 session.get(http://127.0.0.1:5000/api2/3) # 第二步业务请求session自动携带上面拿到的cookie传入m参数 resp session.get( http://127.0.0.1:5000/api/question/3, params{page:1,pageSize:10,m:前端算出来的match3值} ) print(resp.json())set_cookie和cookie模拟流程1. 客户端第一次访问/api2/3服务器返回Set‑Cookie:check_tokenabc888命令客户端存下这条 cookie2. 客户端session自动收下保存这条 cookie3. 再访问业务接口/api/question/3客户端自动在请求头带上Cookie: check_tokenabc8884. 后端读取请求头 Cookie 做校验1服务端代码flaskfrom flask import Flask, request, make_response, jsonify app Flask(__name__) app.route(/api2/3) def api2_3(): 前置接口返回Set‑Cookie种下校验cookie resp make_response(, 200) # Set‑Cookie服务器下发指令让浏览器存cookie resp.set_cookie(check_token, valueabc888, path/) return resp app.route(/api/question/3) def question3(): 业务接口读取请求头里面的Cookie做校验 # request.cookies 拿到【请求头Cookie】是客户端带过来的 check request.cookies.get(check_token) m_arg request.args.get(m) if check ! abc888 or not m_arg: return jsonify({data:[校验失败]}) return jsonify({data:[100,200,300,400]}) if __name__ __main__: app.run(port5000,debugTrue)2客户端 requests 模拟浏览器行为import requests BASE http://127.0.0.1:5000 # 必须同一个Session等价浏览器的cookie仓库 session requests.Session() print(第一次请求 /api2/3对应浏览器async:false同步ajax) r1 session.get(f{BASE}/api2/3) # r1.headers 里面能看到服务端返回的 Set‑Cookie响应头 print(响应头 Set‑Cookie, r1.headers.get(set‑cookie)) # 此时session内部已经自动保存 check_tokenabc888不需要手动处理 print(\n第二次请求业务接口 /api/question/3 ) params {page:1,m:xxx_match3_value} r2 session.get(f{BASE}/api/question/3, paramsparams) # 打印本次发给服务器的 请求头 Cookie print(本次请求发送出去的Cookie, r2.request.headers.get(cookie)) print(业务接口返回, r2.json())ajax请求同步 AJAX// 同步请求 async false function syncApi2_3() { const xhr new XMLHttpRequest(); // 第三个参数 false → 同步阻塞JS代码停在这里不动 xhr.open(GET, /api2/3, false); // 请求头可选 xhr.setRequestHeader(Accept, */*); xhr.send(null); // GET请求 send传null // ⚠️ 同步模式代码执行到send()直接卡住 // 1.浏览器发出http请求 // 2.等待服务器返回响应 // 3.浏览器解析响应头 Set‑Cookie写入浏览器Cookie存储内核自动做JS代码读不到Set‑Cookie // ✅全部完成send()才返回才会执行下面这行 console.log(api2/3 请求结束我们完全丢弃返回内容不使用 xhr.responseText); // 不处理返回结果只利用【http往返 浏览器保存Set‑Cookie】这个副作用 } // -------- 完整业务流程match3真实逻辑 -------- function req() { // 第一步同步前置接口阻塞等待Set‑Cookie落地到浏览器cookie库 syncApi2_3(); // 上面同步请求全部完成后才走到这里 // 此时浏览器已经存好了 /api2/3 返回Set‑Cookie种下的校验cookie // 第二步发起业务接口请求 const bizXhr new XMLHttpRequest(); bizXhr.open(GET, /api/question/3, true); // 业务接口用异步 bizXhr.onload function(){ if(bizXhr.status 200){ console.log(业务返回数据, JSON.parse(bizXhr.responseText)); } } // url参数带上 m对应 window.match3 const params new URLSearchParams({ page:1, pageSize:10, m: window.match3 }) bizXhr.send(params); } // 调用 req();异步 AJAX// 异步请求 async true默认 function asyncDemo(){ const xhr new XMLHttpRequest(); xhr.open(GET,/api2/3, true); // true异步不会阻塞代码 xhr.onload function(){ // 只有接口返回之后才会进入这个回调函数 console.log(异步接口返回了, xhr.responseText); } xhr.send(); // send() 立刻返回不会等待接口代码直接往下执行 console.log(send执行完毕不等接口返回就跑到这里); }jQuery $.ajax 写法// 同步版本 match3源码风格 function req(){ // 前置同步ajax $.ajax({ url:/api2/3, type:GET, async: false, // 重点false 同步阻塞 success:function(res){ // 这里的回调同步模式下也会执行但是代码不拿res直接忽略 } }) // 上面同步请求全部完成才运行下面业务请求 $.ajax({ url:/api/question/3, type:GET, async:true, data:{ page:1, pageSize:10, m: window.match3 }, success:function(resp){ console.log(业务数据, resp) } }) }参数说明open(method, url, async)asyncfalse同步true异步 (默认)send(body)GET 请求传nullPOST 传请求体onload / onreadystatechange异步模式接口回来之后触发回调同步模式回调几乎没用send 完数据已经就绪响应头Set‑CookieJS 代码无法读取浏览器内核自动解析并存入 Cookie 存储请求头Cookie浏览器自动组装该域名下全部本地 Cookieajax 发出请求自动带上JS 不能直接读写这个请求头最终代码import requests import json url https://match.yuanrenxue.cn/ session requests.session() session.headers { sec-ch-ua-platform: \Windows\, x-requested-with: XMLHttpRequest, user-agent: yuanrenxue, accept: application/json, text/javascript, */*; q0.01, sec-ch-ua: \Not/A)Brand\;v\99\, \Chromium\;v\148\, sec-ch-ua-mobile: ?0, sec-fetch-site: same-origin, sec-fetch-mode: cors, sec-fetch-dest: empty, referer: https://match.yuanrenxue.cn/match/3, accept-encoding: gzip, deflate, br, zstd, accept-language: zh-CN,zh;q0.9, cookie: sessionidho9vnm6xj4tu49sls5l2xhghod0ekog8, priority: u0, i } def send1(): url1 url api2/3 res session.get(url1) print(res) def send2(page): url2 url api/question/3 params { page: page, pageSize: 10, kw: } res session.get(url2, paramsparams) # print(res) data json.loads(res.text).get(data) print(i, data) sum 0 for item in data: sum item return sum if __name__ __main__: sum 0 for i in range(1,6): send1() sum send2(i) print(sum)