🎯浏览器插件:WXT实现对第三方网站的接口进行拦截😀

876 阅读5分钟

写在开头

嘿,各位UU们好呀!👋

最近周末小编去惠州爬了罗浮山,从烈日爬到了夜晚,这山海拔有 1296 米,爬起来还真有点费腿,但看了山顶的极致风光后,一切都值了。🌄

e932567b43aba4ed3bc659c3f6fa3131.jpg

🎯需求背景

最近,小编在开发浏览器插件的时候,遇到了一个需求:需要拦截第三方网站的网络请求,获取特定接口的响应体,然后把数据上报。

浏览器插件开发小编使用的是 WXT 框架,原因无它,就是方便,不需要处理太多兼容性问题。

经过一番研究和实践,小编在 WXT 框架中采用 JS 注入的方式,通过代理 XMLHttpRequest 和 fetch API,成功实现了对第三方网站请求的拦截,并获取到了想要的接口数据。

接下来,小编将分享这个实现过程。咱们以拦截掘金的用户接口为例哈👻,因为小编的需求本质就是拦截某个网站的用户接口,获取其中的用户信息。

https://api.juejin.cn/user_api/v1/user/get?

image.png

🛠️实现过程

第1️⃣步:配置 WXT 权限

首先,我们需要在 WXT 配置中添加必要的权限。

wxt.config.ts 文件中添加权限配置:

export default defineConfig({
  manifest: {
    permissions: [
      'webRequest',
      'tabs', 
      'scripting'
    ],
    host_permissions: [
       '*://*/*'  // 允许所有网站(或指定目标网站)
    ]
  }
});

为什么要配置这些权限呢❓

因为实现网络请求拦截功能,主要依赖 chrome.webRequest API 和 chrome.scripting API。

根据 Chrome 扩展程序的规范,访问大多数扩展 API 和功能时,必须在扩展程序的 清单文件 中声明相应权限。某些权限会触发用户授权提示,用户同意后才能正常使用扩展功能。

关于这些权限的详细作用,小编就不一一展开了,感兴趣的小伙伴可以查阅官方文档:传送门

这里其实不用太纠结某个 API 具体属于哪个权限,咱们可以把常用权限都配置上,反正呢,影响不大。😋

permissions: [
  "activeTab",
  "alarms",
  "background",
  "bookmarks",
  "browsingData",
  "clipboardRead",
  "clipboardWrite",
  "contentSettings",
  "contextMenus",
  "cookies",
  "debugger",
  "declarativeContent",
  "declarativeNetRequest",
  "declarativeNetRequestFeedback",
  "desktopCapture",
  "downloads",
  "fontSettings",
  "gcm",
  "geolocation",
  "history",
  "identity",
  "idle",
  "management",
  "nativeMessaging",
  "notifications",
  "pageCapture",
  "power",
  "printerProvider",
  "privacy",
  "proxy",
  "scripting",
  "search",
  "sessions",
  "storage",
  "system.cpu",
  "system.display",
  "system.memory",
  "system.storage",
  "tabCapture",
  "tabGroups",
  "tabs",
  "topSites",
  "tts",
  "ttsEngine",
  "unlimitedStorage",
  "webNavigation",
  "webRequest"
]

第2️⃣步:创建网络拦截器

接下来,我们需要创建一个网络拦截器,用来重写 fetchXMLHttpRequest

utils/injectNetworkInterceptor.ts 文件中创建拦截器:

export function injectNetworkInterceptor() {
  const win = window as any;
  
  /**
   * @name 统一处理响应结果,可以存储到本地,或者通过postMessage发送到其他地方
   */
  function handleResponseData(data: any) {
    // 找到url特定接口
    if (data.url.includes("juejin") && data.url.includes("user/get")) {
      try {
        // 解析响应数据
        let responseData = data.responseBody;
        if (typeof responseData === 'string') {
          responseData = JSON.parse(responseData);
        }
        // 检查是否存在用户信息
        if (responseData && responseData.data) {
          const userInfo = {
            user_id: responseData.data.user_id,
            user_name: responseData.data.user_name,
          };
          // 存储到 sessionStorage ,再其他地方可以通过本地缓存获取到
          sessionStorage.setItem('juejin_user_info', JSON.stringify(userInfo));
        }
      } catch (error) {
        console.error('❌ 解析响应数据失败:', error);
      }
    }
  }

  // 避免重复注入
  if (win.__WEFLY_NETWORK_INTERCEPTOR_INJECTED__) return;
  win.__WEFLY_NETWORK_INTERCEPTOR_INJECTED__ = true;

  // 保存原始的fetch和XMLHttpRequest
  const originalFetch = window.fetch;
  const originalXMLHttpRequest = window.XMLHttpRequest;

  // ... 后续代码
}

这个拦截器的核心思路是:

  1. 重写浏览器原生的 fetchXMLHttpRequest 方法。
  2. 在请求完成后拦截响应数据。
  3. 根据URL和响应内容进行过滤和处理。
  4. 将需要的数据存储到本地。

第3️⃣步:重写 fetch API

在拦截器中重写 fetch 方法:

/**
 * 重写fetch API
 */
win.fetch = async function (...args: any) {
  const [resource, config] = args;
  const url = typeof resource === "string" ? resource : resource.url;
  const method = config?.method || "GET";
  let response = null;
  
  try {
    // 调用原始fetch
    response = await originalFetch.apply(this, args);
    // 克隆响应以便读取响应体
    const responseClone = response.clone();
    
    // 尝试获取响应体
    let responseBody = null;
    try {
      const contentType = response.headers.get("content-type") || "";
      if (contentType.includes("application/json")) {
        responseBody = await responseClone.json();
      } else if (contentType.includes("text/")) {
        responseBody = await responseClone.text();
      } else {
        responseBody = "[Binary Data]";
      }
    } catch (error) {
      responseBody = "[Parse Data Fail]";
    }
    
    const interceptedData = {
      type: "fetch",
      url,
      method,
      requestHeaders: config?.headers || {},
      requestBody: config?.body || null,
      responseStatus: response.status,
      responseStatusText: response.statusText,
      responseBody,
      timestamp: Date.now(),
    };
    
    handleResponseData(interceptedData);
  } catch (error) {
    if (url.includes("chrome-extension://") || url.includes("127.0.0.1")) {
      // 忽略扩展和本地资源的报错
      return;
    }
    console.error("❌ [fetch拦截] 请求失败:", url, error);
  } finally {
    return response;
  }
};

第4️⃣步:重写 XMLHttpRequest

同样需要重写 XMLHttpRequest,因为有些网站可能还在使用这个老的API:

/**
 * 重写XMLHttpRequest
 */
win.XMLHttpRequest = function () {
  const xhr: any = new originalXMLHttpRequest();
  const originalOpen = xhr.open;
  const originalSend = xhr.send;
  const originalSetRequestHeader = xhr.setRequestHeader;

  let requestData: any = {
    url: "",
    method: "",
    headers: {},
    body: null,
  };

  // 重写open方法
  xhr.open = function (method: any, url: any, async: any, user: any, password: any) {
    requestData.method = method;
    requestData.url = url;
    return originalOpen.call(this, method, url, async, user, password);
  };

  // 重写setRequestHeader方法
  xhr.setRequestHeader = function (header: any, value: any) {
    requestData.headers[header] = value;
    return originalSetRequestHeader.call(this, header, value);
  };

  // 重写send方法
  xhr.send = function (body: any) {
    requestData.body = body;
    
    // 监听响应
    const originalOnReadyStateChange = xhr.onreadystatechange;
    xhr.onreadystatechange = function () {
      if (xhr.readyState === 4) {
        // 请求完成
        const interceptedData = {
          type: "xhr",
          url: requestData.url,
          method: requestData.method,
          requestHeaders: requestData.headers,
          requestBody: requestData.body,
          responseStatus: xhr.status,
          responseStatusText: xhr.statusText,
          responseBody: xhr.responseText,
          timestamp: Date.now(),
        };
        handleResponseData(interceptedData);
      }
      
      // 调用原始的onreadystatechange
      if (originalOnReadyStateChange) {
        originalOnReadyStateChange.call(this);
      }
    };

    return originalSend.call(this, body);
  };

  return xhr;
};

// 复制原始XMLHttpRequest的静态属性
Object.setPrototypeOf(win.XMLHttpRequest, originalXMLHttpRequest);
Object.setPrototypeOf(win.XMLHttpRequest.prototype, originalXMLHttpRequest.prototype);

第5️⃣步:在 background 中注入拦截器

最后,我们需要在 background.ts 中监听标签页更新事件,并注入我们的拦截器:

import { injectNetworkInterceptor } from "@/utils/injectNetworkInterceptor"

export default defineBackground(() => {
  /**
   * @name 初始化JS注入拦截器
   * @description 使用chrome.scripting.executeScript API注入JavaScript代码来拦截第三方网站的网络请求
   */
  function initJSInjectionInterceptor() {
    browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
      // 只在页面完全加载后处理,且排除特殊页面和协议
      if (changeInfo.status === 'complete' && tab.url && isValidUrl(tab.url)) {
        console.log("🎯 插件注入JS代码")
        try {
          // 使用chrome.scripting.executeScript注入拦截代码
          await browser.scripting.executeScript({
            target: { tabId: tabId },
            func: injectNetworkInterceptor,
            world: 'MAIN' // 在主页面上下文中执行,可以访问页面的全局变量
          })
        } catch (error) {
          console.error('❌ 注入拦截代码失败:', error)
        }
      }
    })
  }
  
  /**
   * @name 检查URL是否有效
   * @param {string} url 页面URL
   * @returns {boolean} 是否为有效URL
   */
  function isValidUrl(url: string) {
    // 仅允许配置的域名注入JS
    const allowedDomains = [
      'juejin.cn',
      // 可以添加更多域名
    ];
    
    const invalidProtocols = [
      'chrome://',
      'chrome-extension://',
      'moz-extension://',
      'about:',
      'data:',
      'file:'
    ];
    
    // 检查是否为无效协议
    if (invalidProtocols.some(protocol => url.startsWith(protocol))) {
      return false;
    }
    
    // 检查是否为允许的域名
    return allowedDomains.some(domain => url.includes(domain));
  }
  
  // 启动拦截器
  initJSInjectionInterceptor();
});

这里的关键配置:

  • world: 'MAIN':确保代码在主页面上下文中执行,可以访问页面的全局变量。
  • changeInfo.status === 'complete':确保页面完全加载后再注入。
  • isValidUrl()方法:只在目标网站上注入代码,避免不必要的性能开销。

🎉结果

当我们访问掘金网站时,插件会自动注入拦截代码,只要页面发起用户信息相关的API请求时,咱们就能在拦截到的数据:

090901.gif





至此,本篇文章就写完啦,撒花撒花。

image.png

希望本文对你有所帮助,如有任何疑问,期待你的留言哦。
老样子,点赞+评论=你会了,收藏=你精通了。