Chrome扩展中sendMessage延迟导致userAgent伪造失效如何解决?
这个问题我之前碰过好几次,核心就是异步请求的延迟和网页脚本执行顺序的冲突——你用chrome.runtime.sendMessage是异步操作,得等后台返回结果才能改UA,但网页的脚本(哪怕是inline的)会在异步等待的间隙先跑起来,自然第一次console.log拿到的是默认UA,等你改完UA之后,后续输出才会生效。
给你两个针对性的解决方案,按需选:
方案1:直接同步设置固定UA(最简单,适合UA不变的场景)
如果你的自定义UA是固定的,不需要后台动态生成,那直接在document_start的内容脚本里覆盖navigator.userAgent的 getter 就行,完全没异步延迟问题:
// content.js(注意manifest里要设置run_at: document_start) Object.defineProperty(navigator, 'userAgent', { get: () => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 CustomUA/1.0', configurable: true, // 允许后续修改(如果需要的话) enumerable: true });
因为内容脚本在document_start时机注入,比网页所有脚本都早执行,网页里的任何代码读navigator.userAgent都会直接拿到你设置的UA,完美解决问题。
方案2:动态获取UA时,先拦截网页脚本再放行(适合UA需要后台生成的场景)
如果UA得根据不同网站或者其他逻辑动态生成,那得先把网页的脚本拦下来,等拿到UA并设置完成后,再让网页脚本执行:
第一步:后台脚本写好UA生成逻辑
// background.js chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message.action === 'getCustomUA') { // 这里写你的UA生成逻辑,比如根据当前网址返回不同UA const customUA = generateUA(sender.tab.url); sendResponse({ua: customUA}); } return true; // 保持端口开放,支持异步生成UA的情况 }); function generateUA(url) { // 举个例子:给example.com用特殊UA,其他用默认自定义UA if (url.includes('example.com')) { return 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 ExampleSiteUA/1.0'; } else { return 'Mozilla/5.0 (Macintosh; Intel Mac OS X 13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 DefaultCustomUA/1.0'; } }
第二步:内容脚本拦截脚本+设置UA
// content.js(run_at: document_start) let isUAReady = false; const pendingScripts = []; // 先启动监听,把所有即将加载的脚本都拦下来 const scriptWatcher = new MutationObserver((mutations) => { for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node.tagName === 'SCRIPT' && !isUAReady) { node.remove(); // 先从DOM里删掉,阻止它执行 pendingScripts.push(node); // 存起来,等下再放回去 } } } }); // 监听整个document的脚本插入 scriptWatcher.observe(document.documentElement, { childList: true, subtree: true }); // 异步拿UA并设置,之后恢复脚本执行 (async () => { try { const response = await chrome.runtime.sendMessage({action: 'getCustomUA'}); // 覆盖navigator.userAgent的getter Object.defineProperty(navigator, 'userAgent', { get: () => response.ua, configurable: true, enumerable: true }); isUAReady = true; // 把拦下来的脚本恢复执行 pendingScripts.forEach(script => { // 外部脚本要重新创建元素,不然移除后src可能失效 if (script.src) { const newScript = document.createElement('script'); newScript.src = script.src; newScript.async = script.async; newScript.defer = script.defer; document.head.appendChild(newScript); } else { // inline脚本直接插回去就行 document.head.appendChild(script); } }); // 停止监听,不用再拦了 scriptWatcher.disconnect(); } catch (err) { console.error('获取自定义UA失败:', err); // 出错了也得把脚本放回去,不然网页会卡死 isUAReady = true; pendingScripts.forEach(script => document.head.appendChild(script)); scriptWatcher.disconnect(); } })();
可选补充:同步修改HTTP请求头的UA
如果需要让浏览器发送的HTTP请求头里的UA和网页的navigator.userAgent一致,可以在后台加这段代码(记得在manifest里声明webRequest、webRequestBlocking权限和对应的主机权限,比如<all_urls>):
// background.js chrome.webRequest.onBeforeSendHeaders.addListener( (details) => { // 找到User-Agent头替换成自定义的 for (let i = 0; i < details.requestHeaders.length; i++) { if (details.requestHeaders[i].name.toLowerCase() === 'user-agent') { details.requestHeaders[i].value = generateUA(details.url); break; } } return {requestHeaders: details.requestHeaders}; }, {urls: ['<all_urls>']}, ['blocking', 'requestHeaders', 'extraHeaders'] );
为啥你原来的方法会有延迟?
简单说,chrome.runtime.sendMessage是异步的,它会把请求丢给后台,然后内容脚本继续往下走(其实是等待事件循环),而网页的脚本会在这个等待间隙先执行,所以第一次console.log拿到的是默认UA。等后台返回结果改完UA,后续的输出才会是修改后的。上面的方案要么直接同步改UA,要么把网页脚本拦到UA改完再放行,完美解决顺序问题。
内容的提问来源于stack exchange,提问作者Escape75

