今日头条sign-node环境
爬取目标
今日头条首页内容推荐内容(也就是下面红线的部分)
也有爬取个人空间与头条新闻的实现,见末尾。
相关接口
可以观察到今日头条首页的第一页请求与之后的请求的url是不大一样的。
第一次请求
https://www.toutiao.com/api/pc/feed/?min_behot_time=0&category=__all__&utm_source=toutiao&widen=1&tadrequire=true&as=A1D52F17E1F6715&cp=5F71C6D781553E1&_signature=_signature
// 为了方便观察,已经缩短了_signature参数的长度。
第二次请求及以后的请求
https://www.toutiao.com/api/pc/feed/?max_behot_time=1601263428&category=__all__&utm_source=toutiao&widen=1&tadrequire=true&as=A1254F17E13671F&cp=5F715637211FEE1&_signature=_signature
// 为了方便观察,已经缩短了_signature参数的长度。
变动的query参数有max_behot_time,as,cp以及signature。
关于参数逆向
一: max_behot_time
max_behot_time很容易观察出来,这个参数来自与上一次请求的数据中的max_behot_time。
如果是第一次请求的话,这个参数就变成了min_behot_time, 并且其值是定值。
好了,这个就这么搞定了
二: signature
第二个便是看signature是怎么生成的了。
我们通过发起者(initator)找到一个调用函数(如下所示,Reqwest是随机选择的 )
在此函数的内部下一个断点,然后页面向下滑动下。
很显然,这个页面要生成的参数到这里都已经生成好了。这时候我们需要通过call stack看看这个函数的调用者们
很快就发现了生成_signature参数的地方了
653行的o便是生成的_signature参数了。
就也是如下面的代码所示。
var url = "https://www.toutiao.com/toutiao/api/pc/feed/?max_behot_time=1601241738&category=__all__&utm_source=toutiao&widen=1&tadrequire=true&as=A1E57F277116DED&cp=5F71169DAE7D3E1";
var signature = window.byted_acrawler.sign({url: url});
貌似我们只要把 window.byted_acrawler.sign 的算法弄下来,就可以生成对应的_signature参数了。
我们进入window.byted_acrawler.sign内部看一看
明显的代码混淆。逆向并不是要将他的代码都读懂,如果你想这么做的话,那么你就中了他的圈套了。
正确的做法应该是是拷贝相关的代码,先尝试在浏览器上运行,如果没有报错,那么说明这个代码没有明显的环境依赖。
如果报错了,就要尝试补环境了,缺哪补哪。
如果浏览器上运行正常,可以拿到node环境进行运行了,node环境要尽量模拟浏览器环境即可。
首先我们复制下acrawler.js的所有的代码。
开一个新的标签页。(建议地址栏输入 about:blank, 这样便可以得到完全的空白页了。这样就不会受其他网页的干扰了)
粘贴下之前复制的acrawler的js代码到console中并执行,执行完毕后并没有报错,并且window.byted_acrawler.sign已经有了
做个试验,看看生成的_signature对不对。
var url = "https://www.toutiao.com/toutiao/api/pc/feed/?max_behot_time=1601241738&category=__all__&utm_source=toutiao&widen=1&tadrequire=true&as=A1E57F277116DED&cp=5F71169DAE7D3E1"; var signature = window.byted_acrawler.sign({url: url});
生成倒是生成了,倒是长度有些短。长度短的原因是因为没有cookie的缘故。
可以在本地开一个服务器,然后通过document.cookie 获取到今日头条网页的cookie。
使用
"你复制到的cookie".forEach((ele)=>{document.cookie = ele});
便可以将今日头条下所有的cookie复制到本地的网页中了。
这时候生成的_signature参数便是正常的长度了。
浏览器试验完成,现在如果这个js可以在node环境运行的话,那就完美了。
如何在node环境中运行呢?首先这个js是肯定会检测浏览器环境的,包括检测canvas,创建元素啥的。
自己一个个写?太麻烦了。
这里要用到的一个第三方库,那便是jsdom。这是一个利用来模拟浏览器环境的node第三方库。
const jsdom = require("jsdom"); const { JSDOM } = jsdom; // // // node_modules/jsdom@16.4.0/api.js // const options = { url: "https://www.toutiao.com/", // to config userAgent, source code must be altered // "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36" } const { window } = new JSDOM("", options); var glb; // window.glb = glb; // var global = undefined; var process = undefined; var module = undefined; var exports = undefined; var { StyleSheet, MediaList, CSSStyleSheet, CSSRule, CSSStyleRule, CSSMediaRule, CSSImportRule, CSSStyleDeclaration, XPathException, XPathExpression, XPathResult, XPathEvaluator, onafterprint, onbeforeprint, onbeforeunload, onhashchange, onlanguagechange, onmessage, onmessageerror, onoffline, ononline, onpagehide, onpageshow, onpopstate, onrejectionhandled, onstorage, onunhandledrejection, onunload, onblur, onerror, onfocus, onload, onresize, onscroll, onabort, onautocomplete, onautocompleteerror, oncancel, oncanplay, oncanplaythrough, onchange, onclick, onclose, oncontextmenu, oncuechange, ondblclick, ondrag, ondragend, ondragenter, ondragexit, ondragleave, ondragover, ondragstart, ondrop, ondurationchange, onemptied, onended, oninput, oninvalid, onkeydown, onkeypress, onkeyup, onloadeddata, onloadedmetadata, onloadstart, onmousedown, onmouseenter, onmouseleave, onmousemove, onmouseout, onmouseover, onmouseup, onwheel, onpause, onplay, onplaying, onprogress, onratechange, onreset, onsecuritypolicyviolation, onseeked, onseeking, onselect, onsort, onstalled, onsubmit, onsuspend, ontimeupdate, ontoggle, onvolumechange, onwaiting, _registeredHandlers, _eventHandlers, _globalObject, _resourceLoader, _globalProxy, _document, _origin, _sessionHistory, _virtualConsole, _runScripts, _top, _parent, _frameElement, _length, _pretendToBeVisual, _storageQuota, _commonForOrigin, _currentOriginData, _localStorage, _sessionStorage, _selection, getSelection, length, frameElement, frames, self, parent, top, document, external, location, history, navigator, locationbar, menubar, personalbar, scrollbars, statusbar, toolbar, performance, screen, origin, localStorage, sessionStorage, customElements, setTimeout, setInterval, clearTimeout, clearInterval, postMessage, atob, btoa, stop, close, getComputedStyle, captureEvents, releaseEvents, console, name, status, devicePixelRatio, innerWidth, innerHeight, outerWidth, outerHeight, pageXOffset, pageYOffset, screenX, screenLeft, screenY, screenTop, scrollX, scrollY, alert, blur, confirm, focus, moveBy, moveTo, open, print, prompt, resizeBy, resizeTo, scroll, scrollBy, scrollTo, glb } = window;
上面的写法骗过今日头条还是挺容易的。
三:as cp参数的生成。
可以看到as cp参数其实是变化的
as cp生成的位置与_signature参数的位置还是挺近的(648行)
this._setParams方法除了对max_behot_time参数的设置,还有对as,cp参数的更新
上面的a函数便可以生成as,cp参数,粘贴到编辑器上,缺啥补啥即可。(约200行,就不粘贴到这里)
四:JSDOM如何修改user-agent?
首先说说为什么要修改user-agent?
这是因为_signature中会使用user-agent,在发送请求时,headers中的user-agent要与_signature中的保持一致。
const jsdom = require("jsdom"); const {JSDOM} = jsdom; const resourceLoader = new jsdom.ResourceLoader({ strictSSL: false, userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36", }); const { window } = new JSDOM(``, { resources: resourceLoader, url: "https://www.toutiao.com/" }); console.log(window.navigator.userAgent); console.log(window.location.href);
相关源码
https://gitee.com/re_is_good/js_reverse/tree/master/%E4%BB%8A%E6%97%A5%E5%A4%B4%E6%9D%A1/1-%E8%8E%B7%E5%8F%96%E4%B8%AA%E4%BA%BA%E9%A6%96%E9%A1%B5%E4%BD%9C%E5%93%81%E9%93%BE%E6%8E%A5