网络
随着您深入探索浏览器扩展开发,您会很快意识到,在发送网络请求方面存在一些有趣的独特之处。尽管浏览器扩展和网页使用相同的 API 和网络层来发送请求,但在构建一个完善的浏览器扩展时,您需要克服一些根本性的差异。本章将涵盖扩展网络通信的一些核心领域:发送网络请求、身份验证以及浏览器扩展网络通信 API。
比较网站与扩展程序
在开始本章之前,我们先来对比一下传统网站和浏览器扩展程序。 以下是一个包含3列的Markdown表格示例:
类别 | 网站 | 浏览器扩展程序 MV3 |
---|---|---|
Origin | 一个网站具有可预测的来源 以及用户提供的域名。 | 浏览器扩展程序会通过 扩展程序 ID 自动分配一个来源。 |
APIs | 网站可在任意位置使用XMLHttpRequest 或 fetch() 。 | 后台脚本只能使用 fetch() 。 |
Remote assets | 远程资源可从相同来源提供服务, 而无需配置跨域策略。 | 远程资源必须正确配置跨域策略后才能加载。 不得从远程来源执行脚本。 |
Page types | 网站上的所有网页都能以一致 的方式发送请求并完成身份验证。 | 弹出窗口/选项页、 内容脚本和后台脚本在发送请求方面 各自存在不同的限制。 例如,内容脚本需遵循宿主页面的跨域限制, 而弹出窗口则不受此限制。 |
Server requests | 网站可以向后台发送同源请求。 | 如果浏览器扩展程序使用了后台 服务器,那么这些请求将始终是跨域请求。 |
Authentication | 无限制:网站可在任意位置使用 Cookie 认证、JWT 认证或 OAuth 认证。 | 并非所有认证方式在所有场景下都适用, 例如,服务工作线程(Service Workers) 无法使用 Cookie 认证。 |
Long-running requests | 只要选项卡(tab)处于打开状态, 任何长时间运行的请求 都将保持活动状态。 | 后台服务工作线程 (background service workers)、 弹出窗口(popups)以及 内容脚本(content scripts)中的 长时间运行请求均可能意外终止, 例如弹出窗口被关闭或服务工作线程被终止。 |
Cross-browser | 该网站来源(域名)在各类 浏览器中均保持一致。 | 浏览器扩展程序的 来源(origin)在不同浏览器中存在差异: 例如,在 Chrome 浏览器中是 chrome-extension://extensionID ,而在 Firefox 浏览器中则是 moz-extension://uuid 。 |
网络架构
在发送网络请求方面,浏览器扩展程序的不同组件对于某些任务来说,其适用性各有优劣。如何调度网络请求取决于浏览器扩展程序的性质。本节将介绍设计扩展程序时需要考虑的一些事项。
选项页
选项页几乎与传统网站完全相同,因此对于许多开发人员来说,在构建浏览器扩展程序时,选项页是一个自然的选择。如果你的整个用户界面都构建在选项页中,那么使用任何形式的认证(无论是 Cookie 认证、JSON Web Token (JWT) 认证还是 OAuth)都不会有问题。除非选项页以模态方式呈现(open_in_tab=false
),否则它将与浏览器选项卡具有相同的生命周期,因此可以安全地从选项页调度长时间运行的请求,而无需担心过早终止。
选项页是一个不错的选择,可以作为依赖后台脚本的浏览器扩展程序的补充,同时也希望执行长时间运行的请求。后台脚本可以打开一个选项卡,作为长时间运行请求的载体。
弹出窗口和开发者工具页面
弹出窗口和开发者工具页面在本质上与选项页几乎相同,但有一个主要区别:它们预期会频繁地被关闭。因此,它们不是长时间运行网络请求的良好载体。不过,它们是进行认证和通用网络请求的良好选择。
内容脚本
内容脚本是发送网络请求的一个特别有趣的工具。由于它们在宿主页面上运行,因此它们受到相同的跨域限制。这意味着与自己的服务器通信可能需要将请求委托给后台脚本。然而,由于内容脚本请求被视为宿主页面请求,因此它们可以使用宿主页面的 Cookie,这意味着你可以以经过认证的用户身份发送请求。
在内容脚本中进行认证很棘手,因为宿主页面可以看到放入 DOM 或共享 API 中的任何内容。此外,长时间运行的网络请求显然依赖于宿主页面保持打开状态;如果选项卡关闭或用户导航离开,请求或 WebSocket 将被突然终止。
提示:认证欺骗能力非常强大!它通常需要对宿主页面进行一些逆向工程,但它允许你自动化宿主页面被允许执行的经过认证的操作。例如,假设某个网页显示了一个经过认证的用户的购物车,其中有 100 个商品。每个商品都有一个“移除商品”按钮,该按钮会触发对 /cart/remove
的网络请求,并附带一个 JSON 负载,如 {"item_id": 123}
,而你希望清空购物车。与其自动化 100 次按钮点击,不如提取所需的 item_id
值,并让内容脚本直接发送这 100 个经过认证的请求。
后台脚本
从清单版本 2 (Manifest V2) 迁移到清单版本 3 (Manifest V3) 对后台脚本来说尤其具有挑战性,因为网络请求模式发生了重大且破坏性的变化。以前,后台脚本作为一个持久无头网页存在,这意味着允许使用 Cookie 认证、长时间运行的请求和认证对话框窗口。现在,所有这些事情都发生了实质性变化:
- Cookie 认证:从技术上讲仍然可能,但 Cookie 需要从渲染的 HTML 页面中传递进来并手动提供,这非常烦人且不切实际——或者在 HttpOnly Cookie 的情况下是不可能的。
- 长时间运行的请求:浏览器在确定服务工作线程是否空闲时不会考虑正在进行的请求,因此长时间运行的请求可能会过早终止。
- 认证对话框窗口:不能使用
window.open()
,必须使用身份 API。
后台脚本仍然是一个优秀且功能强大的工具,但在 Manifest V3 中,它们肯定受到了更多限制。使用 JWT 或 OAuth 认证且不依赖长时间运行请求的扩展程序将能够无缝地利用后台服务工作线程网络,而不会出现问题。
固定扩展程序 ID
在扩展程序网络的上下文中,能够预测和控制浏览器扩展程序的来源是非常有用的。在本地开发时,浏览器会自动为你的扩展程序分配一个 ID;当上传到 Chrome 网上应用店时,它会为你的扩展程序分配一个完全不同的 ID。对于希望拥有一致 ID 的开发人员来说,这是一个问题。
幸运的是,可以提前固定扩展程序的 ID,使其保持一致且可预测。这样做涉及以下步骤:
- 将扩展程序的初始版本上传到 Chrome 网上应用店。该商店将为你的扩展程序生成一个公钥,该公钥反过来用于生成扩展程序 ID。如果你的本地浏览器提供了该公钥,它将生成相同的扩展程序 ID(图 13-1)。
- 从网上应用店检索公钥(图 13-2)。
- 将公钥添加到清单中的
"key"
属性下。去掉 BEGIN/END 行和所有空白,并将其作为一个大字符串传递(图 13-3)。 - 本地加载扩展程序。你的本地浏览器应该生成一个与 Chrome 网上应用店相同的扩展程序 ID(图 13-4)。
注意: 请记住,生产扩展程序和本地扩展程序具有相同的 ID 意味着浏览器会将它们视为同一个。为防止冲突,请确保在任何给定时间只安装了生产或本地扩展程序之一。
该过程如下所示:
认证方式
没有强大的认证能力,网络就不会走得太远。浏览器扩展程序可以使用与网站相同的基础技术进行认证。然而,由于扩展程序的固有架构,必须仔细考虑认证方式。本节讨论不同的认证模式。
无认证
浏览器扩展程序不一定需要认证。通常,巧妙地应用一小部分 WebExtensions API 就足以使扩展程序变得有用。此外,当内容脚本与原始 DOM 交互时,浏览器扩展程序与经过认证的会话交互的能力与登录状态的概念完全无关。换句话说,宿主网站可以承担登录用户的繁重工作,浏览器扩展程序可以等待登录状态,然后内容脚本可以读取和写入 DOM。
一些 API 允许你利用原生浏览器身份分离,而无需管理认证。例如,存储 API 允许你使用 chrome.storage.sync
,它附加到用户的浏览器帐户,并将同步到其帐户,或者使用 chrome.storage.local
,它本地化到当前浏览器实例。
内容脚本欺骗
如本章前面所述,来自内容脚本的网络请求与来自宿主页面脚本的网络请求的处理方式相同。因此,如果网站使用 HttpOnly Cookie 认证,内容脚本将能够利用该经过认证的状态,并发送将自动包含这些 Cookie 的请求。
当然,一个构建良好的网站也会包含一个 CSRF 令牌,但由于 CSRF 令牌通常在 <form>
等一致位置提供,因此内容脚本可以检查 DOM,提取 CSRF 令牌,并将其与传出请求一起传递。
Cookie 认证
从技术上讲,浏览器扩展程序用户界面可以使用 Cookie 认证,但我不推荐这种方式。这种认证风格在网页由服务器直接渲染时效果最好,而对于浏览器扩展程序来说,情况并非如此。此外,基于 Cookie 的经过认证的状态不能被后台服务工作线程直接访问。总体而言,由于实现它涉及太多不必要的开销,因此建议你除非有特别令人信服的理由,否则应避免使用 Cookie 认证。
JSON Web Token 认证
令牌式认证在基于应用程序的认证中效果很好,在这种认证中,Cookie 不切实际。对于浏览器扩展程序来说,像 JWT(JSON Web Token)这样的令牌认证方案是理想的。弹出窗口、选项页、开发者工具页面和后台脚本都可以直接与服务器进行认证,并在扩展程序组件之间共享认证令牌,而不会出现问题。内容脚本能够使用 JWT,但宿主页面的跨域策略可能会阻止对服务器的请求,因此通过后台服务工作线程间接发送请求是首选的解决方法。
OAuth 和 OpenID
借助 chrome.identity
API,浏览器扩展程序对 OAuth 和 OpenID 提供了一流的支持。此 API 在内容脚本中不受支持,但可以在浏览器扩展程序中的其他任何地方有效部署。对于不希望实现后台服务器的开发人员来说,通过 OAuth 或 OpenID 进行授权和认证非常实用。
chrome.identity
API 由 Google Chrome 发起,但在所有基于 Chromium 的浏览器和 Firefox 中都至少得到了部分支持。在 Google Chrome 中,浏览器扩展程序享受额外的身份 API 方法,包括简化的 OAuth 授权流程(本章后面将详细介绍)。
注意:Safari 目前不支持身份 API。
OAuth、OpenID 和身份 API
注意:本节假设你对 OAuth2 协议的工作原理有基本了解。有关 OAuth2 的介绍,Auth0 有一篇优秀的文章:https://auth0.com/intro-to-iam/what-is-oauth-2/。
将认证和授权委托给第三方平台的能力非常有用,但在浏览器扩展程序中实现这一点很棘手。考虑一些涉及的挑战:
- 将登录委托给第三方平台需要能够打开一个受信任的界面来收集凭据。后台服务工作线程不能使用
window.open()
。 - 配置平台以支持 OAuth 需要提前了解扩展程序 ID。
- OAuth 和 OpenID 使用相同的底层协议,其中包括一个重定向 URL。浏览器扩展程序只能在带有扩展程序协议的私有 URL 中渲染。
幸运的是,身份 API 通过提供一组灵活的工具来解决所有这些问题,以实现委托的认证和授权。
OAuth API 方法
在扩展程序中使用 OAuth 有两种方法:
chrome.identity.getAuthToken()
允许你进行原生认证。此方法允许你跳过提供重定向 URL 和发出授权令牌请求。相反,你只需在 oauth2 清单属性中提供所需的值,并调用此方法。浏览器将启动 OAuth 对话框,并将 OAuth 令牌传递给该方法的回调。这是迄今为止在扩展程序中实现 OAuth 最简单的方法,但它仅在 Google Chrome 中可用。这需要一个“Chrome 应用”客户端 ID。chrome.identity.launchWebAuthFlow()
是使用 OAuth2 的更通用的方法。它是跨浏览器的(在 Firefox、Edge 等上工作)和跨平台的(支持与 Facebook、Github 等的 OAuth2)。它更加费力,因为它需要你手动实现 OAuth2 的每个步骤。
OAuth 重定向 URL
为了解决 OAuth 重定向 URL 问题,浏览器支持一个特殊的 URL,该 URL 将授权流程重定向回扩展程序。要访问此 URL,你可以使用 chrome.identity.getRedirectURL()
方法。Chrome 文档描述了其行为:
此方法通过启动 Web 视图并将其导航到提供程序的授权流程中的第一个 URL,从而启用与非 Google 身份提供程序的身份验证流程。当提供程序重定向到匹配模式 https://<app-id>.chromiumapp.org/*
的 URL 时,窗口将关闭,并且最终的重定向 URL 将传递给回调函数。
换句话说,浏览器对这个 URL 进行了特殊处理,以便在浏览器扩展程序的上下文中支持认证。所有基于 Chromium 的浏览器和 Firefox 都支持此方法。
提示:你可以在 Chromium 源代码中看到这个特殊 URL 处理逻辑:https://chromium.googlesource.com/chromium/chromium/+/master/chrome/browser/extensions/api/identity/web_auth_flow.cc。
配置授权平台
为 Chrome 扩展程序设置 OAuth 的第一步是配置授权平台,以向扩展程序提供访问权限。这涉及配置 OAuth 同意屏幕并生成客户端 ID(图 13-5、13-6、13-7、13-8 和 13-9)。同意屏幕是用户开始 OAuth 认证流程时在弹出窗口中显示的内容;客户端 ID 是唯一标识扩展程序的字符串,并允许其与授权平台交互(图 13-10)。
注意:此过程对于各种授权平台和 OAuth 流程将略有不同,但它总是会为你提供一个客户端 ID。
以下屏幕截图演示了配置 Google OAuth 时的一些步骤。
并非所有平台都支持这一区分。就 Google OAuth 而言,“Chrome 应用”和“Web 应用”均可用于浏览器扩展。
- “Chrome 应用”表示您使用的是清单(manifest)文件中的 oauth2 字段以及 getAuthToken() 方法。这还会在活跃的 Google Chrome 浏览器配置文件中对用户进行身份验证,而这可能并非理想情况。
- “Web 应用”表示您使用的是 launchWebAuthFlow() 方法。这允许您与 Google Chrome 浏览器配置文件分开进行身份验证。
谷歌最终会以可下载的 JSON 文件形式提供您的客户端 ID。以下是一个示例:
client_secret.json
{
"installed": {
"client_id": "<some_id>.apps.googleusercontent.com",
"project_id": "browser-extension-explorer",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs"
}
}
其他平台会以不同的方式和格式提供客户端 ID,但最终,您必须将其集成到浏览器扩展中,以启用 OAuth 身份验证。
额外帮助
设置 OAuth 可能会非常令人困惑。我精心挑选了一些链接,指导您以各种方式完成设置:
OAuth 和 OpenID 示例
对于扩展的 OAuth,没有比亲自编写实际工作代码更好的替代方案了。以下示例展示了配置和触发 OAuth 流程的不同方式。这些示例以源代码形式提供,但需要您自行完成一些工作才能使其正常运行。您需要配置自己的 OAuth 提供方信息(如客户端 ID),才能使其正确工作。
提示:本书的配套扩展“Browser Extension Explorer”包含三个可正常运行的 OAuth 示例。请访问 buildingbrowserextensions.com 安装它。
使用 getAuthToken()
的 Google OAuth
此示例使用 Google Chrome 的原生 OAuth 功能来验证用户。点击工具栏图标会触发 OAuth 流程,成功验证后,会记录一些基本字段。注意事项如下:
- 请求的权限包括
identity
(可访问身份 API)和identity.email
(允许扩展轻松访问当前配置文件中已登录用户帐户的电子邮件和 Gaia ID,通过chrome.identity.getProfileUserInfo()
实现)。 getAuthToken()
是幂等的。用户验证后,它会缓存 OAuth 令牌。如果用户已验证时调用它,则不会出现 OAuth 对话框:该方法将从缓存中检索令牌并将其传递给回调函数。interactive
选项控制如果用户未验证,是否尝试启动 OAuth 对话框。如果为false
,则未验证时会退出并抛出错误。否则,将打开 OAuth 对话框。- 请注意,此流程中除了清单中提供的 URL 外,没有其他 URL。重定向 URL 和 OAuth 端点均由浏览器自动处理!
示例 13-1a. manifest.json
{
"name": "MVX",
"version": "0.0.1",
"manifest_version": 3,
"action": {},
"background": {
"service_worker": "background.js",
"type": "module"
},
"permissions": ["identity", "identity.email"],
"oauth2": {
"client_id": "YOUR_CLIENT_ID",
"scopes": [
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/userinfo.profile"
]
}
}
示例 13-1b. background.js
chrome.action.onClicked.addListener(() => {
chrome.identity.getAuthToken(
{},
(token) => {
if (token) {
chrome.identity.getProfileUserInfo(
{ accountStatus: "ANY" },
(info) => console.log(info)
);
}
}
);
});
// 控制台输出:
// { "email": <google_email>, "id": <google_gaia_id> }
使用 launchWebAuthFlow()
的 Google OpenID
注意:本节假设您对 OpenID Connect 协议的工作原理有基本了解。有关 OpenID 的介绍,Auth0 有一篇优秀的文章:什么是 OpenID Connect (OIDC)?
此示例使用 launchWebAuthFlow()
实现 Google OpenID Connect 流程来验证用户。点击工具栏图标会触发 OpenID 流程,成功验证后,会记录 OpenID 负载。注意事项如下:
- 与上一个示例相比,由于我们没有使用
getProfileUserInfo()
方法,因此不再需要identity.email
权限。 - 请注意,客户端 ID 是在 JavaScript 中传递的,而不是在清单中提供的。
- 使用
launchWebAuthFlow()
时,您现在必须手动提供浏览器将用于重定向回扩展回调的重定向 URL。 launchWebAuthFlow()
有一个interactive
控件,其行为与getAuthToken()
的相同。- OpenID 需要在初始 URL 中指定特定的查询字符串参数,并返回一个 JSON Web 令牌 (JWT)。此示例会解包 JWT,以便您可以查看其中打包的数据。
示例 13-2a. manifest.json
{
"name": "MVX",
"version": "0.0.1",
"manifest_version": 3,
"action": {},
"background": {
"service_worker": "background.js",
"type": "module"
},
"permissions": ["identity"]
}
示例 13-2b. background.js
chrome.action.onClicked.addListener(() => {
const clientId = "YOUR_CLIENT_ID";
const extensionRedirectUri = chrome.identity.getRedirectURL();
const nonce = Math.random().toString(36).substring(2, 15);
const authUrl = new URL("https://accounts.google.com/o/oauth2/v2/auth");
// 定义 OpenID authUrl 的字段
authUrl.searchParams.set("client_id", clientId);
authUrl.searchParams.set("response_type", "id_token");
authUrl.searchParams.set("redirect_uri", extensionRedirectUri);
authUrl.searchParams.set("scope", "openid profile email");
authUrl.searchParams.set("nonce", nonce);
authUrl.searchParams.set("prompt", "consent");
chrome.identity.launchWebAuthFlow(
{
url: authUrl.href,
interactive: true,
},
(redirectUrl) => {
if (redirectUrl) {
// ID 令牌在 URL 哈希中
const urlHash = redirectUrl.split("#")[1];
const params = new URLSearchParams(urlHash);
const jwt = params.get("id_token");
// 解析 JSON Web 令牌
const base64Url = jwt.split(".")[1];
const base64 = base64Url.replace("-", "+").replace("_", "/");
const token = JSON.parse(atob(base64));
console.log(token);
}
}
);
});
// 控制台输出:
// {
// "iss": "https://accounts.google.com",
// "azp": "...",
// "aud": "...",
// "sub": "...",
// "email": "XXXXX@gmail.com",
// "email_verified": true,
// "nonce": "...",
// "name": "Matt Frisbie",
// "picture": "...",
// "given_name": "Matt",
// "family_name": "Frisbie",
// "locale": "en",
// "iat": ...,
// "exp": ...,
// "jti": "..."
// }
使用 launchWebAuthFlow()
的手动 GitHub OAuth
此示例使用 launchWebAuthFlow()
实现 GitHub OAuth2 流程来验证用户。点击工具栏图标会触发 GitHub OAuth2 流程,成功验证后,会记录 GitHub 个人资料负载。注意事项如下:
- 此示例完成了完整的 OAuth 流程的所有工作:启动身份验证对话框、收集授权码、使用该授权码获取访问令牌,最后执行 OAuth 身份验证请求。
- GitHub OAuth 对话框通过将其附加到重定向 URL 来传递初始授权码,该授权码在此处被回调函数提取出来。
- GitHub 的 OAuth 为您提供了一个客户端密钥,该密钥必须包含在请求中以获取访问令牌。
示例 13-3a. manifest.json
{
"name": "MVX",
"version": "0.0.1",
"manifest_version": 3,
"action": {},
"background": {
"service_worker": "background.js",
"type": "module"
},
"permissions": ["identity"]
}
示例 13-3b. background.js
chrome.action.onClicked.addListener(() => {
const clientId = "YOUR_CLIENT_ID";
const extensionRedirectUri = chrome.identity.getRedirectURL();
const authUrl = new URL("https://github.com/login/oauth/authorize");
authUrl.searchParams.set("client_id", clientId);
authUrl.searchParams.set("redirect_uri", extensionRedirectUri);
chrome.identity.launchWebAuthFlow(
{
url: authUrl.href,
interactive: true,
},
async (redirectUrl) => {
if (redirectUrl) {
const queryString = new URL(redirectUrl).search;
const params = new URLSearchParams(queryString);
const code = params.get("code");
const authUrl = new URL("https://github.com/login/oauth/access_token");
authUrl.searchParams.append("client_id", clientId);
authUrl.searchParams.append("redirect_uri", extensionRedirectUri);
authUrl.searchParams.append("client_secret", "YOUR_CLIENT_SECRET");
authUrl.searchParams.append("code", code);
const response = await fetch(authUrl, {
method: "POST",
headers: {
Accept: "application/json",
},
});
const accessTokenData = await response.json();
const r = await fetch("https://api.github.com/user", {
headers: {
Authorization: "Bearer " + accessTokenData.access_token,
},
});
console.log(await r.json());
}
}
);
});
// 控制台输出:
// {
// "login": "msfrisbie",
// "id": ...,
// "node_id": "...",
// "avatar_url": "...",
// "name": "Matt Frisbie",
// "blog": "https://www.mattfriz.com",
// "location": "Chicago, IL",
// "bio": "Software engineer, bestselling author",
// "twitter_username": "mattfriz",
// "url": "https://api.github.com/users/msfrisbie",
// ...
// }
网络 API
浏览器扩展被授予访问一些强大的 API 的权限,这些 API 可以用于检查和修改浏览器内的流量流动。这些方法是广告拦截扩展如此有效的原因。本节将考察三个 API:
- webNavigation API:允许您以非常细粒度的方式观察顶级浏览器导航事件。
- webRequest API:允许您以阻塞方式拦截和修改流量,这意味着对于任何网络请求,扩展都可以注入一个 JavaScript 函数,该函数可能会或可能不会修改或完全取消请求。“阻塞”标识符表示网络请求将等待 JavaScript 函数返回后再继续。
- declarativeNetRequest API:允许您构建规则集,指示浏览器应如何原生处理网络请求。这些规则可能会告诉浏览器修改或阻止请求。此 API 是 webRequest API 的后续版本,在某些方面功能较弱。此 API 仅在清单 v3 中可用。
注意:在清单 v3 中,对 webRequest 的支持是碎片化的。例如,Firefox 将继续支持此 API,而 Chromium 浏览器正在移除支持。
webNavigation API
webNavigation API 允许扩展查看浏览器的导航事件。该 API 允许扩展为以下导航生命周期事件添加事件处理程序:
onBeforeNavigate
onCommitted
onCompleted
onCreatedNavigationTarget
onDOMContentLoaded
onErrorOccurred
onHistoryStateUpdated
onReferenceFragmentUpdated
onTabReplaced
图 13-11 显示了事件流。
关于 webNavigation API 的事件细节
每个事件的详细信息可以在 MDN 文档中找到:https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/webNavigation。
大多数情况下,扩展程序只关心为正常浏览器导航触发的主要生命周期事件设置处理程序:
onBeforeNavigate
onCommitted
onDOMContentLoaded
onCompleted
以下简单扩展程序将在浏览器中的标签页访问新 URL 时记录到扩展控制台。加载扩展程序,打开一个新标签页,并导航到任何网站以实时查看导航事件。
示例 13-4a. manifest.json
{
"name": "MVX",
"version": "0.0.1",
"manifest_version": 3,
"background": {
"service_worker": "background.js",
"type": "module"
},
"permissions": ["webNavigation"]
}
示例 13-4b. background.js
chrome.webNavigation.onCompleted.addListener((details) => {
console.log(details);
});
// 示例控制台日志:
// {
// "documentId": "7D0F7EBBCB0020C74DA89F2D36C461B4",
// "documentLifecycle": "active",
// "frameId": 0,
// "frameType": "outermost_frame",
// "parentFrameId": -1,
// "processId": 1040,
// "tabId": 173953269,
// "timeStamp": 1661549345594.7412,
// "url": "https://en.wikipedia.org/wiki/Main_Page"
// }
webRequest API
webRequest API 允许浏览器扩展程序检查和修改来自网页的网络请求事件。该 API 允许扩展程序为以下请求生命周期事件添加事件处理程序:
onActionIgnored
onAuthRequired
onBeforeRedirect
onBeforeRequest
onBeforeSendHeaders
onCompleted
onErrorOccurred
onHeadersReceived
onResponseStarted
onSendHeaders
图 13-12 显示了事件流。
关于 webRequest API 的事件细节
每个事件的详细信息可以在 MDN 文档中找到:https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/webRequest。
修改请求
如果仅希望检查请求生命周期事件数据,只需要 webRequest
权限。然而,如果希望修改这些请求,则需要额外的 webRequestBlocking
权限。这将允许执行以下任务:
- 扩展程序可以在
onBeforeRequest
、onBeforeSendHeaders
或onAuthRequired
阻塞处理程序中取消请求。 - 扩展程序可以在
onBeforeRequest
或onHeadersReceived
阻塞处理程序中重定向请求。 - 扩展程序可以在
onBeforeSendHeaders
阻塞处理程序中修改请求头。 - 扩展程序可以在
onHeadersReceived
阻塞处理程序中修改响应头。 - 扩展程序可以在
onAuthRequired
阻塞处理程序中修改请求的身份验证凭据。
以下简单示例将阻止 Wikipedia 上的所有图像请求。请注意,如果希望检查或修改该来源的请求,还必须请求 URL 权限。
注意:webRequest API 和 manifest v3 在 Google Chrome 中缺乏支持,因此此示例使用 manifest v2 编写。我建议在 Firefox 中进行测试。
示例 13-5a. manifest.json
{
"name": "MVX",
"version": "0.0.1",
"manifest_version": 2,
"background": {
"scripts": ["background.js"]
},
"permissions": [
"webRequest",
"webRequestBlocking",
"<all_urls>"
]
}
示例 13-5b. background.js
chrome.webRequest.onBeforeRequest.addListener(
() => {
return {
cancel: true
};
},
{
urls: [
"*://www.wikipedia.org/portal/wikipedia.org/assets/img/*"
]
},
["blocking"]
);
分裂
浏览器扩展空间正朝着一个奇怪的方向发展。在撰写本书时,似乎 Chromium 浏览器已决定在 manifest v3 中结束对 webRequest 的支持,但 Firefox 可能会继续在 manifest v3 中支持该 API。目前尚不清楚该领域最终会如何发展,但目前我不建议构建直接依赖此 API 的扩展程序。
declarativeNetRequest API
declarativeNetRequest API(DNR)被宣传为 webRequest API 的继任者。这一转变的动机是消除阻塞的 JavaScript 处理程序,转而采用由浏览器实现的更高效的声明性模型。现在,不再在 JavaScript 函数中执行修改,而是将每个匹配请求的“指令”作为 JSON 配置对象提供。
对于大多数扩展程序,DNR 是 webRequest 的有效替代品。然而,广告拦截器的质量将受到两个主要原因的影响:
- 浏览器对扩展程序可以为 declarativeNetRequest 定义的规则数量设置了全局上限。现代广告拦截器定义了大约 100,000+ 条规则,广告拦截器扩展程序无疑会很快达到此上限。
- 现代广告拦截器依赖于阻塞的 webRequest 处理程序中的复杂逻辑。并非所有这些逻辑都可以转换为相对简单的 declarativeNetRequest 规则,因此广告拦截器扩展程序在屏蔽广告方面将不再那么有效。
权限
declarativeNetRequest API 可以与三种不同的权限一起使用:
declarativeNetRequest
权限允许扩展程序定义 DNR 规则,但仍然需要请求主机权限才能在特定来源上应用规则。- 可以使用
declarativeNetRequestWithHostAccess
权限代替declarativeNetRequest
权限。它允许在无需请求主机权限的情况下在任何来源上阻止或升级请求。重定向或头修改仍然需要特定的主机权限。 declarativeNetRequestFeedback
权限可以作为前两者的补充添加。使用它,扩展程序可以访问返回匹配规则信息的函数和事件。
规则和规则集结构
所有 DNR 规则都采用 JSON 对象的形式。每个扩展程序每次请求仅应用一条规则。每个规则对象的结构如下:
id
必须是唯一的正整数。priority
是一个可选整数,可用于控制在多个规则匹配请求时应用哪个规则。condition
是一个对象,描述何时应用此规则。使用它,可以包含或排除域、域类型、标签 ID 和资源类型。action
是一个对象,描述此规则的作用。它包括一个类型,可以是block
、redirect
、allow
、upgradeScheme
、modifyHeaders
或allowAllRequests
,以及各种配置规则行为的属性。
DNR 规则有两种定义方式:
- 静态规则集是 JSON 文件中的规则对象数组。规则集文件可以在清单中提供或通过编程方式添加。它们还可以按需启用或禁用。静态规则集只能作为与扩展程序一起打包的不可变文件存在。浏览器对静态规则的总数有限制。
- 动态规则是仅通过编程方式创建的规则对象。它们可以添加、删除、修改以及启用或禁用。一部分动态规则是会话规则,这些规则是不会在浏览器之间持久化的动态规则。浏览器对动态规则的总数有限制。
无论是静态还是动态,浏览器都会以相同的方式处理 DNR 规则。
注意:declarativeNetRequest API 相当深入。有关其所有功能的详细信息,请参阅 Google Chrome 文档:https://developer.chrome.com/docs/extensions/reference/declarativeNetRequest。
静态规则集
让我们通过检查以下简单扩展程序来研究静态规则集,该扩展程序可以阻止 Wikipedia 上的图像:
示例 13-6a. manifest.json
{
"name": "MVX",
"version": "0.0.1",
"manifest_version": 3,
"background": {
"service_worker": "background.js",
"type": "module"
},
"declarative_net_request": {
"rule_resources": [
{
"id": "ruleset_1",
"enabled": true,
"path": "rules_1.json"
}
]
},
"action": {},
"permissions": ["declarativeNetRequest"],
"host_permissions": [
"*://*.wikipedia.org/*",
"*://*.wikimedia.org/*"
]
}
示例 13-6b. background.js
const RULESET_ID = "ruleset_1";
chrome.action.onClicked.addListener(async () => {
const enabled_rulesets = await chrome.declarativeNetRequest.getEnabledRulesets();
if (enabled_rulesets.includes(RULESET_ID)) {
chrome.declarativeNetRequest.updateEnabledRulesets({
disableRulesetIds: [RULESET_ID]
});
} else {
chrome.declarativeNetRequest.updateEnabledRulesets({
enableRulesetIds: [RULESET_ID]
});
}
console.log("Toggled ruleset");
});
示例 13-6c. rules_1.json
[
{
"id": 1,
"priority": 1,
"action": {
"type": "block"
},
"condition": {
"domains": ["wikipedia.org", "wikimedia.org"],
"resourceTypes": ["image"]
}
}
]
加载此扩展程序并导航到 wikipedia.org。初始扩展程序状态将启用规则集。您会注意到图像未加载。单击扩展程序的工具栏按钮以切换规则集。重新加载 Wikipedia 时,图像应正常加载。
您可能会注意到本地开发文件夹中出现了一个新的 _metadata
目录(图 13-13)。顾名思义,目录中的二进制文件是浏览器跟踪活动规则集的方式。可以忽略它,但请确保不要将其提交到版本控制或添加到生产构建中。
动态规则
动态规则与静态规则集非常相似。同样,它们可以按需查询、添加和删除。在本示例中,让我们编写一个动态重定向规则:
示例 13-7a. manifest.json
{
"name": "MVX",
"version": "0.0.1",
"manifest_version": 3,
"background": {
"service_worker": "background.js",
"type": "module"
},
"action": {},
"permissions": ["declarativeNetRequest"],
"host_permissions": ["*://*.wikipedia.org/*", "*://*.wikimedia.org/*"]
}
示例 13-7b. background.js
const RULE_ID = 1;
const RULE_1 = {
id: RULE_ID,
priority: 1,
action: {
type: "redirect",
redirect: {
url: "https://upload.wikimedia.org/wikipedia/commons/thumb/b/b1/Hot_dog_with_mustard.png/1920px-Hot_dog_with_mustard.png"
}
},
condition: {
domains: ["wikipedia.org", "wikimedia.org"],
resourceTypes: ["image"]
}
};
chrome.action.onClicked.addListener(async () => {
const dynamic_rules = await chrome.declarativeNetRequest.getDynamicRules();
if (dynamic_rules.find((rule) => rule.id === RULE_ID)) {
chrome.declarativeNetRequest.updateDynamicRules({
removeRuleIds: [RULE_ID]
});
} else {
chrome.declarativeNetRequest.updateDynamicRules({
addRules: [RULE_1]
});
}
console.log("Toggled rule");
});
加载此扩展程序并导航到 wikipedia.org。您会注意到图像正常加载。单击扩展程序的工具栏按钮并重新加载 Wikipedia。Wikipedia 上的所有图像现在应该都变成了一张热狗的图片。
总结
在本章中,我们探讨了浏览器扩展可以发送网络请求的各种模式。我们回顾了所有不同的组件以及它们如何和应该发送网络请求。接下来,我们讨论了身份验证策略,包括深入探讨如何在浏览器扩展中实现 OAuth。最后,我们讨论了如何使用主要的 WebExtensions API 来检查和操作浏览器中的流量。
在下一章中,我们将介绍如何本地开发扩展程序、在扩展程序市场上发布以及部署更新。