前景提要

最近在开发一款电商网站商品素材下载浏览器插件,前几天搞定了图片下载功能,又经过几天焦头烂额的踩坑和尝试,最终把批量下载功能也实现了。昨天想着给插件加上登录功能,方便用户保存相关记录。因为是面向国外用户的插件,想着直接支持google oauth2登录,用户未登录时点击登录按钮,会跳转到插件网站的登录页面,用户选择google登录后会跳转到google登录页面,完成登录后同步登录状态到插件。

也就是要实现登录功能才遇到了这个问题,网上也没搜到太多资料。网上的资料大多是background和content以及popup间的通信方案,这是官方就支持的,因此没有什么好说的,但是我需要的插件和网站直接进行状态/数据同步的方案的资料却不是很多。

好在研究了一天时间(其实大部分时间在解决别的问题),终于还是解决了这个问题。

插件与网站状态同步实现方案

我们的现在的插件有一个background script叫做service_worker.js,以及一个content script叫做content.js

我们假设插件生效的网站为A网站,域名假设为www.website.com,假设我们的插件网站为B网站,域名假设为www.plugin.com。B网站的登录页面为www.plugin.com/login,B网站记录登录状态采用传统的cookies方式+session方式,而没有采用token方式。

如何让插件知道网站B的登录状态发生了变化?

  1. 利用background script 监听网站B的cookies变化

  2. 利用content script 监听网站B的登录状态变化

background script 监听网站B的cookies变化

我们可以利用background script的chrome.cookies.onChanged事件来监听网站B的cookies变化。当B网站的cookies发生变化时,会触发该事件,我们可以在事件回调中判断是否是我们插件网站的cookies变化,如果是,就可以根据cookies来判断用户是否登录。

需要修改manifest.json文件,添加cookies权限。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"manifest_version": 3,
"name": "Plugin",
"version": "1.0",
"permissions": [
"cookies"
],
"background": {
"service_worker": "service_worker.js"
},
"content_scripts": [
{
"matches": [
"https://www.plugin.com/*"
],
"js": [
"content.js"
]
}
]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
chrome.cookies.onChanged.addListener(function (changeInfo) {
if (changeInfo.cookie.domain === ".plugin.com") {
// 插件网站的cookies发生了变化
if (changeInfo.cookie.name === "session") {
// session cookies发生了变化,判断登录状态
if (changeInfo.cookie.value) {
// session cookies存在,用户登录
// 发送登录状态到background.js
chrome.runtime.sendMessage({ url: "www.plugin.com/login", status: "loggedIn" });
} else {
// session cookies不存在,用户未登录
// 发送未登录状态到background.js
chrome.runtime.sendMessage({ url: "www.plugin.com/login", status: "notLoggedIn" });
}
}
}
});

整个流程是这样的:

  1. 用户访问登录页面www.plugin.com/login,进行登录操作,登录成功后,服务器写入session cookies。
  2. service_worker.js文件监听到www.plugin.com/login的cookies变化,进行登录相关业务逻辑处理。然后发送登录状态到content.js文件。
  3. content.js文件监听到登录状态变化,执行相应的逻辑,比如显示/隐藏登录按钮。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
┌──────────────────────────┐     ┌──────────────────────────┐
│ │ │ │
│ 用户浏览器 │ │ 插件网站服务器 │
│ │ │ (www.plugin.com) │
└─────────────────┬────────┘ └───────────┬──────────────┘
│ │
▼ │
┌──────────────────────────┐ │
│ │ │
│ 登录页面 /login │────────────────▶│ 写入session cookies │
│ │ │
└──────────────────────────┘ │


┌──────────────────────────┐
│ │
│ 扩展后台服务 │
│ service_worker.js │
│ 监听cookies变化 │
└───────────┬──────────────┘


┌──────────────────────────┐
│ │
│ 扩展内容脚本 │
│ content.js │
│ │
└───────────┬──────────────┘


┌──────────────────────────┐
│ │
│ 登录按钮显示/隐藏 │
│ │
└──────────────────────────┘

其实可以监听的应该不仅仅是session对应的cookies,其他比如token cookies等也可以监听,只是我们用的登录方案是基于cookies的,因此只需要监听cookies变化即可。

content script 监听网站B的登录状态变化

此种方案需要新增一个content script文件,我们假设叫 login_callback_script.js文件,专门用于监听网站B的登录状态变化,当然了,我们也可以在content.js文件中监听,但是为了避免与插件网站B的其他功能冲突,我们单独新建一个文件。

因为content script运行在网站B的上下文中,因此我们需要对网站B进行改造,新增一个/callback路由,用于提供让callback_script.js运行的环境。因此我们需要在manifest.json新增一个规则,如下:

1
2
3
4
5
6
7
8
9
10
// manifest.json
"content_scripts": [
{
"matches": [
"https://plugin.com/callback*" // 仅匹配回调页面
],
"js": ["callback_script.js"],
"run_at": "document_start"
}
]
1
2
3
4
5
6
7
8
9
10
11
// login_callback_script.js
// 你需要处理插件网站的登录状态的相关逻辑,此处cookies仅做示例,不是实际业务逻辑
if (document.cookie.includes("session")) {
// 用户登录
// 发送登录状态到background.js
chrome.runtime.sendMessage({ url: "www.plugin.com/login", status: "loggedIn" });
} else {
// 用户未登录
// 发送未登录状态到background.js
chrome.runtime.sendMessage({ url: "www.plugin.com/login", status: "notLoggedIn" });
}
1
2
3
4
5
6
7
8
9
10
11
// service_worker.js
chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) {
if (request.url === "www.plugin.com/login") {
// 处理登录状态变化
if (request.status === "loggedIn") {
// 用户登录
} else if (request.status === "notLoggedIn") {
// 用户未登录
}
}
});

整个流程是这样的:

  1. 用户访问登录页面www.plugin.com/login,进行登录操作,登录成功后,页面跳转至www.plugin.com/callback
  2. login_callback_script.js文件运行,判断用户是否登录,并发送登录状态到service_worker.js文件。
  3. service_worker.js文件会根据登录状态变化,执行相应的逻辑,比如通知content.js文件登录状态发生了变化。
  4. content.js文件会根据登录状态变化,执行相应的逻辑,比如显示/隐藏登录按钮。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
┌─────────────────────────────────┐     ┌─────────────────────────────────┐     ┌─────────────────────────────────┐
│ 用户浏览器 │ │ 登录回调脚本 │ │ 扩展后台服务 │
│ (访问 www.plugin.com/login) │ │ (login_callback_script.js) │ │ (service_worker.js) │
└─────────────────┬───────────────┘ └─────────────────┬───────────────┘ └─────────────────┬───────────────┘
│ │ │
│ 1. 用户登录成功 │ │
│ 页面跳转至 callback │ │
├─────────────────────────────────────────>│ │
│ │ │
│ │ 2. 判断登录状态并发送消息 │
│ ├─────────────────────────────────────────>│
│ │ │
│ │ │
│ │ │ 3. 处理登录状态变化
│ │ │ 通知内容脚本
│ │ ├─────────────────┐
│ │ │ │
│ │ │ │
┌─────────────────┴───────────────┐ ┌─────────────────┴───────────────┐ └─────────────────┴───────────────┘
│ 扩展内容脚本 │ │ │
│ (content.js) │ │ │
└─────────────────┬───────────────┘ └─────────────────────────────────┘

│ 4. 接收登录状态通知
│ 执行相应逻辑

│ 5. 显示/隐藏登录按钮

└─────────────────────────────────────────────────────────┐


┌─────────────────────────┐
│ 用户界面更新 │
└─────────────────────────┘

看清了有些麻烦是吧,所以我们可以把login_callback_script.js文件中的逻辑放到content.js文件中,但仍然需要新增一个页面路径/callback,让content.js文件可以运行。

修改后的流程是这样的:

  1. 用户访问登录页面www.plugin.com/login,进行登录操作,登录成功后,页面跳转至www.plugin.com/callback
  2. B网站content.js文件开始运行,判断用户是否登录,并发送登录状态到service_worker.js文件。
  3. service_worker.js文件会根据登录状态变化,执行相应的逻辑,比如通知content.js文件登录状态发生了变化。
  4. A网站content.js文件会根据登录状态变化,执行相应的逻辑,比如显示/隐藏登录按钮。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
┌──────────────────────────┐     ┌──────────────────────────┐
│ │ │ │
│ 用户浏览器 │ │ B网站 (www.plugin.com) │
│ │ │ │
└─────────────────┬────────┘ └───────────┬──────────────┘
│ │
▼ ▼
┌──────────────────────────┐ ┌──────────────────────────┐
│ │ │ │
│ 登录页面 /login │────▶│ 回调页面 /callback │
│ │ │ │
└──────────────────────────┘ └───────────┬──────────────┘


┌──────────────────────────┐
│ │
│ B网站 content.js │
│ │
└───────────┬──────────────┘


┌──────────────────────────┐
│ │
│ 扩展后台服务 │
│ service_worker.js │
│ │
└───────────┬──────────────┘


┌──────────────────────────┐
│ │
│ A网站 content.js │
│ │
└───────────┬──────────────┘


┌──────────────────────────┐
│ │
│ 登录按钮显示/隐藏 │
│ │
└──────────────────────────┘

浪费我半天时间timeout 的问题

本来我是想着给插件添加oAuth2第三方登录,我想起我之前有一个demo项目实现了oAuth2,因此想着拿来直接改改就行,哪知道之前的demo很久没用了,google的oAuth2实例超过6个月不用就被删除了,因此我就新建了一个新的实例,结果遇到了一个登录超时的问题。用户在授权之后,服务器需要请求/token接口来获取access token,但是我之前的demo中,/token接口的一直是正常运行的,这次一直是请求超时,我想过是GFW的问题,所以我在运行的时候,一直是给系统设置了代理,但始终没有解决这个问题。

最后我不得不给node-fetch配置代理,不知道为什么,之前的demo中没有配置代理就正常工作,现在就不行了,好在配置了代理之后,就正常运行了。

这里再次诅咒GFW,它的存在导致了中国的程序员要解决很多原本不应该存在的问题,才能和世界上其他的程序员一样。