1
头图

背景

今天在写一个某网站限流检测的chrome插件,需要捕获页面的某个请求结果。那么问题就来了,我们该如何捕获页面的请求结果呢?我们来捋捋都有哪些方案。

我开发的时候的配置为manifest_version: 3,下文内容也是在这个基础上展开的。

本文只列举方案,一些需同步在manifest_version进行配置地方并未提及,请自行配置

可行的方案

一、chrome.webRequest

chrome插件提供有chrome.webRequest这么个API,这是一个系列API,允许开发者对请求的多个阶段进行事件监听。

image.png

但比较可惜的是这个API能力比较有限,你不能通过它直接获取到响应内容。

OnBeforeRequestonCompleted这两个阶段的监听来说,你能获取到的数据有这些:

6920fe1403df47489109a6c005ec6653~tplv-73owjymdk6-jj-mark-v1_0_0_0_0_5o6Y6YeR5oqA5pyv56S-5Yy6IEAgYXFpb25nYmVp_q75.webp
db6f3e1b14c84037a27fabbbfd6fa8ca~tplv-73owjymdk6-jj-mark-v1_0_0_0_0_5o6Y6YeR5oqA5pyv56S-5Yy6IEAgYXFpb25nYmVp_q75.webp

当然,人家也不是完全没用,如果是以下几种情况就适用

  1. 所需要的内容在responseHeaders
  2. 利用他给的这些参数足够重新发起一次请求

听起来略微费劲,我放弃这种方案,感觉它更适合用来统计或者屏蔽一些请求。

二、chrome.devtools.network

devtools就是我们经常用的开发者工具栏,chrome.devtools.networkonRequestFinished事件可以在回调中拿到完整的请求和响应,然后通过getContent方法拿到具体的返回结果。

chrome.devtools.panels.create(
  "My Panel",
  "icon.png",
  "devtools.html",
  function (panel) {
    chrome.devtools.network.onRequestFinished.addListener(function (response) {
      console.log("onRequestFinished", response);
      response.getContent(function (content) {
        console.log("content", content);
      });
    });
  }
);

创建好的panel见下图
e29f3f10a0c143648e0915def248eb74~tplv-73owjymdk6-jj-mark-v1_0_0_0_0_5o6Y6YeR5oqA5pyv56S-5Yy6IEAgYXFpb25nYmVp_q75.webp

拿到的结果如下:

a911800fb3ff4d2893a53c94fe62f9b9~tplv-73owjymdk6-jj-mark-v1_0_0_0_0_5o6Y6YeR5oqA5pyv56S-5Yy6IEAgYXFpb25nYmVp_q75.webp

这确实是一个能拿到完整信息的方案,但前提是你得一直开着devtools才行,如果是交付用户使用的场景,这简直是不可容忍的,所以需酌情使用。

三、chrome.declarativeNetRequest请求重定向 + 本地服务

如果你在本地起一个服务,然后使用chrome.declarativeNetRequest方法通过配置把请求重定向到本地服务,这样也可以拿到请求的结果,相当于加了一层代理。

这种方法有点臃肿,但是有些特殊的场景下确实不失为一种解决方案。

四、替换页面请求方法(XMLHttpRequest)(推荐👍)

其实我们还可以替换页面中的请求方法,让页面用我们的方法去发请求,那我们拿请求的结果就如探囊取物,这是目前最理想的方式了。以XMLHttpRequest为例,可以这么写。

// inject.js
(function (xhr) {
  if (XMLHttpRequest.prototype.sayMyName) return;
  console.log("%c>>>>> replace XMLHttpRequest", "color:yellow;background:red");
  var XHR = XMLHttpRequest.prototype;
  XHR.sayMyName = "aqinogbei";
  var open = XHR.open;
  var send = XHR.send;
  XHR.open = function (method, url) {
    this._method = method; // 记录method和url
    this._url = url;
    return open.apply(this, arguments);
  };
  XHR.send = function () {
    if (this._url.includes("target_path")) {
        this.addEventListener("load", function (xhr) {
          console.log('xhr', xhr)
        });
    }
    return send.apply(this, arguments);
  };
})(XMLHttpRequest);

这样,我们就可以狸猫换太子,把页面中的请求方法换成自己的了。

但是现在又遇到一个问题,我们该如何将这段代码注入到页面中呢?这个方法还是较多的。

1. content_scripts注入 (推荐👍)

我们知道content_scripts是和目标页面运行在一起的,所以把上面的代码直接写在content.js中就行了。但是,如果你就这么做了,你就会发现:

>>>>> replace XMLHttpRequest这条log也能打出来,但是我们期待的console.log('xhr', xhr)却没有执行,替换没生效。

那这是咋回事呢?这就不得不提出这样一个问题:content_scripts的运行环境和目标页面到底是不是在一块的?

chrome的开发者文档里是这么写的:

Content scripts are files that run in the context of web pages. Using the standard Document Object Model (DOM), they are able to read details of the web pages the browser visits, make changes to them, and pass information to their parent extension.

这里面有一句关键的,run in the context of web pages,随后还有更关键的。

Work in isolated worlds

Content scripts live in an isolated world, allowing a content script to make changes to its JavaScript environment without conflicting with the page or other extensions' content scripts.

Key term:  An isolated world is a private execution environment that isn't accessible to the page or other extensions. A practical consequence of this isolation is that JavaScript variables in an extension's content scripts are not visible to the host page or other extensions' content scripts. The concept was originally introduced with the initial launch of Chrome, providing isolation for browser tabs.

简单来说就是,content_scripts和目标页面是运行在一块的,DOM这些是共用一套,但是Javascript执行环境是隔离的。这也就解释了为啥上面替换没生效,因为上面的脚本换掉的是自己执行环境中的XMLHttpRequest,而不是目标页面的。

那么,能不能让注入代码的执行环境和目标页面的执行环境不隔离呢?

这是个好问题。答案是可以的,chrome插件提供了一个叫world的配置项,它有两个值:ISOLATED(默认值)和MAIN。前者指明content_scripts是在隔离的环境中执行的,后者指明content_scripts和目标页面在一个环境中执行

在一个环境执行,也就意味着,注入的脚本可以获取、修改目标页面的全局变量,替换请求方法更是不在话下。(对于爬虫开发者来说,这个配置意味着很多,是个值钱的知识点)。
所以,我们大致如下这么配置就可以实现替换。

// manifest.json
{
 "content_scripts": [
        {
            "matches": ["target_page_url"],// 改为自己的目标网站url
            "js": ["inject.js"], // 要注入的脚本
            "world": "MAIN", // 注入代码和目标页面在一个环境中执行
            "run_at": "document_start" // 注入脚本的时机
        }
    ]
}

到目前为止,上面这段核心manifest.json配置,加上inject.js,不需要额外的background.js,甚至无需permissions配置即可实现自动在目标页面注入我们的代码,获取请求结果,甚为简单、优雅。

但是需要注意的,这里有个bug。

虽然说chrome文档里写的是
Content scripts可以直接使用这些API

  1. mainfest.json中将"world"改为"ISOLATED",或删除 (默认"ISOLATED"
  2. background.js中动态注入mainfest.json
chrome.scripting.registerContentScripts([
    {
         id: 'script-id',
         js: [mainWorldLoader],
         persistAcrossSessions: false,
         world: 'MAIN'
   }
])

参考链接

当然,上面那种情况注入是主动,适合一打开页面就注入脚本的场景,如果场景不一样,还有别的注入方式。

2. 从background中注入

background里,我们可以使用chrome.scripting.executeScript方法向页面注入代码,相关配置参数较多,可以自行查看,比较关键的是,它支持world
配置,值的情况同上面的一样。

chrome.action.onClicked.addListener(function(tab) {
  chrome.scripting.executeScript({
      target: {tabId: tab.id},
      files: ["inject.js"],
      world: 'MAIN'
  });
});

它适合被动触发的情况,比如某个页面给background.js一个消息,然后background里执行chrome.scripting.executeScript方法,发起注入操作。这里需要注意的是,调用chrome.scripting.executeScript方法,需要申请scripting权限。

3. 其他注入方式

content_scripts里你还可以通过向页面中插入script标签的形式实现动态注入,这里不展开描述。

总结

至此,我们实现了优雅的捕获页面的请求结果这一目的。来总结下。

  • chrome.webRequest只能拿到请求头和响应头,不能获取响应内容
  • chrome.devtools.network可以获取完整响应内容,但需一直开着devtools
  • chrome.declarativeNetRequest重定向请求到本地服务,略显臃肿,特殊场景可以考虑
  • 替换页面请求方法,操作简单,需要考虑注入方式

    • 主动注入使用content_scripts + world方式
    • 被动注入在background中执行chrome.scripting.executeScript方法

完结,撒花✿✿ヽ(°▽°)ノ✿

EOF


aqiongbei
2k 声望281 粉丝

人生路上,你走的每一步都算数