一种基于chrome插件的本地环境获取token的解决方案

3,474 阅读9分钟

背景

随着业务平台功能越来越多,为了使每个平台的业务能力更加专一,我们将原来嵌在各个业务平台且与平台业务不相关的登录代码解耦了出来,为之搭建了一个新的子平台 — 授权平台。现在的登陆流程变为:业务平台 token 过期后跳转至授权平台,输入正确的用户名和密码获取 token,然后存放于浏览器的 cookie 中,再跳转回业务平台完成登录。

然而这却带来了一个新的问题:本地环境下授权平台与业务平台分别运行,那如何跨域共享 cookie 呢?生产环境能够将多个服务运行在同一个域名下,可以实现 cookie 信息共享。然而本地环境每个服务分别单独运行,cookie 无法跨域共享。

一种做法是,登录测试环境后,复制到 token 再手动粘贴到本地环境的cookie列表中。然而此过程要访问第三方(测试环境),手动复制粘贴时而出错,加上 token 会经常过期因此要反复操作,不符合提能增效的目标,实在不算得上是一个好的方案。那还有更好的解决方案吗?

概念

Chrome 插件是一个用 Web 技术开发、用来增强浏览器功能的软件,它其实就是一个由 HTML、CSS、JS、图片等资源组成的一个.crx 后缀的压缩包。Chrome 插件提供了很多实用 API 供我们使用,包括但不限于:书签控制、下载控制、窗口控制、标签控制、网络请求控制,各类事件监听等等。

我们可以使用 Chrome 插件解决上述问题。具体方案为:在 chrome 插件中完成登陆流程,然后将获取到的 token 写到当前域的 cookie列表中,实现登录流程。整个过程就像在业务平台中登录一样简单

设计思路

为了实现这个功能,我们可以将其分解为三个核心问题:

  1. 如何编写一个简单的 Chrome 插件?
  2. 能否通过 Chrome 插件发送请求,获取到用户 token?
  3. 如何通过 Chrome 插件操作浏览器的 cookie?

如何编写一个简单的 Chrome 插件?

manifest.json

这是一个 Chrome 插件最重要也是必不可少的文件,用来配置所有和插件相关的配置,必须放在根目录。其中,manifest_versionnameversion 是必不可少的,descriptionicons 是推荐的。 下面给出的是一些常见的配置项,均有中文注释,完整的配置文档请戳这里

{
  // 清单文件的版本,这个必须写,而且必须是2
  "manifest_version": 2,
  // 插件的名称
  "name": "demo",
  // 插件的版本
  "version": "1.0.0",
  // 插件描述
  "description": "简单的Chrome扩展demo",
  // 图标
  "icons": {
    "16": "img/icon.png",
    "48": "img/icon.png",
    "128": "img/icon.png"
  },
  // 会一直常驻的后台JS或后台页面
  "background": {
    // 2种指定方式,如果指定JS,那么会自动生成一个背景页
    "page": "background.html"
    //"scripts": ["js/background.js"]
  },
  // 浏览器右上角图标设置,browser_action、page_action、app必须三选一
  "browser_action": {
    "default_icon": "img/icon.png",
    // 图标悬停时的标题,可选
    "default_title": "这是一个示例Chrome插件",
    "default_popup": "popup.html"
  },
  // 当某些特定页面打开才显示的图标
  /*"page_action":
	{
		"default_icon": "img/icon.png",
		"default_title": "我是pageAction",
		"default_popup": "popup.html"
	},*/
  // 需要直接注入页面的JS
  "content_scripts": [
    {
      //"matches": ["http://*/*", "https://*/*"],
      // "<all_urls>" 表示匹配所有地址
      "matches": ["<all_urls>"],
      // 多个JS按顺序注入
      "js": ["js/jquery-1.8.3.js", "js/content-script.js"],
      // JS的注入可以随便一点,但是CSS的注意就要千万小心了,因为一不小心就可能影响全局样式
      "css": ["css/custom.css"],
      // 代码注入的时间,可选值: "document_start", "document_end", or "document_idle",最后一个表示页面空闲时,默认document_idle
      "run_at": "document_start"
    },
    // 这里仅仅是为了演示content-script可以配置多个规则
    {
      "matches": ["*://*/*.png", "*://*/*.jpg", "*://*/*.gif", "*://*/*.bmp"],
      "js": ["js/show-image-content-size.js"]
    }
  ],
  // 权限申请
  "permissions": [
    "contextMenus", // 右键菜单
    "tabs", // 标签
    "notifications", // 通知
    "webRequest", // web请求
    "webRequestBlocking",
    "storage", // 插件本地存储
    "http://*/*", // 可以通过executeScript或者insertCSS访问的网站
    "https://*/*" // 可以通过executeScript或者insertCSS访问的网站
  ],
  // 普通页面能够直接访问的插件资源列表,如果不设置是无法直接访问的
  "web_accessible_resources": ["js/inject.js"],
  // 插件主页
  "homepage_url": "https://www.baidu.com",
  // 覆盖浏览器默认页面
  "chrome_url_overrides": {
    // 覆盖浏览器默认的新标签页
    "newtab": "newtab.html"
  },
  // Chrome40以前的插件配置页写法
  "options_page": "options.html",
  // Chrome40以后的插件配置页写法,如果2个都写,新版Chrome只认后面这一个
  "options_ui": {
    "page": "options.html",
    // 添加一些默认的样式,推荐使用
    "chrome_style": true
  },
  // 向地址栏注册一个关键字以提供搜索建议,只能设置一个关键字
  "omnibox": { "keyword": "go" },
  // 默认语言
  "default_locale": "zh_CN",
  // devtools页面入口,注意只能指向一个HTML文件,不能是JS文件
  "devtools_page": "devtools.html"
}

下面重点介绍下backgroundpopup

background

这是一个常驻的页面,它的生命周期是插件中所有类型页面中最长的,它随着浏览器的打开而打开,随着浏览器的关闭而关闭,所以通常把需要一直运行的、启动就运行的、全局的代码放在 background 里面。

background 的权限非常高,几乎可以调用所有的 Chrome 扩展 API(除了 devtools),而且它可以无限制跨域,也就是可以跨域访问任何网站而无需要求对方设置CORS

配置中,background可以通过page指定一张网页,也可以通过scripts直接指定一个 JS,Chrome 会自动为这个 JS 生成一个默认的网页:

{
  // 会一直常驻的后台JS或后台页面
  "background": {
    // 2种指定方式,如果指定JS,那么会自动生成一个背景页
    "page": "background.html"
    //"scripts": ["js/background.js"]
  }
}

popup

popup是点击browser_action或者page_action图标时打开的一个小窗口网页,它和 background 非常类似,它们之间最大的不同是生命周期的不同。单击图标打开 popup,焦点离开又立即关闭,所以 popup 页面的生命周期一般很短,需要长时间运行的代码千万不要写在 popup 里面。一般用来做一些临时性的交互。

popup可以包含任意你想要的 HTML 内容,并且会自适应大小。可以通过default_popup字段来指定 popup 页面,也可以调用setPopup()方法。

配置方式:

{
  "browser_action": {
    "default_icon": "img/icon.png",
    // 图标悬停时的标题,可选
    "default_title": "这是一个示例Chrome插件",
    "default_popup": "popup.html"
  }
}

一个 demo

下图是一个popup的简单 demo。

Snipaste_2022-02-28_14-35-26.png

代码文件结构为如下:

Snipaste_2022-02-28_14-40-08.png

manifest.json 文件代码:

{
  "manifest_version": 2,
  "name": "Chrome插件demo",
  "version": "1.0",
  "description": "Chrome插件demo",
  "icons": {
    "48": "icon.png",
    "128": "icon.png"
  },
  "browser_action": {
    "default_icon": "icon.png",
    "default_popup": "popup.html"
  }
}

popup.html文件代码:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>popup</title>
  </head>
  <body style="width: 500px; min-height: 100px">
    hello world
  </body>
</html>

恭喜!你刚刚制作了一个 Chrome 插件。其他的使用介绍请访问这里

能否通过 Chrome 插件发送请求,获取到用户 token?

答案是可以的。chrome 扩展开发中文教程对于跨域请求的介绍为:

普通网页能够使用 XMLHttpRequest 对象发送或者接受服务器数据,但是它们受限于同源策略。扩展可以不受该限制。任何扩展只要它先获取了跨域请求许可,就可以进行跨域请求。

要使用跨域请求, 你必须在你的 manifest 文件中声明跨域请求权限。例如:

{
  "name": "My extension",
  "permissions": ["*://*.google.com"]
}

如何通过 Chrome 插件操作浏览器的 cookie?

要使用 cookies API, 你必须在你的 manifest 文件中声明cookies权限,以及任何你希望 cookie 可以访问的主机权限。例如:

{
  "name": "My extension",
  "permissions": ["cookies", "*://*.google.com"]
}

下面介绍一下chrome.cookies API:

名称参数含义
getchrome.cookies.get(object details, function callback)获取一个 cookie 的信息。如果对于给定的 URL 有多个 cookie 存在,将返回对应于最长路径的 cookie。对于路径长度相同的 cookies,将返回最早创建的 cookie。
getAllchrome.cookies.getAll(object details, function callback)从一个 cookie 存储获取与给定信息匹配的所有 cookies。所获取 cookies 将按照最长路径优先排序。当多个 cookies 有相同长度路径,最早创建的 cookie 在前面。
getAllCookieStoreschrome.cookies.getAllCookieStores(function callback)列举所有存在的 cookie 存储。
removechrome.cookies.remove(object details)根据名称删除 cookie。
setchrome.cookies.set(object details)用给定数据设置一个 cookie。如果相同的 cookie 存在,它们可能会被覆盖。

chrome.cookies API更加全面的使用介绍请访问这里

实现

工欲善其事必先利其器

我找了一个开源的脚手架,可以帮助我们使用 vue 快速落地实现。这里我使用的是vue-web-extension,github 地址请访问这里

执行命令,自动安装脚手架:

vue create --preset kocal/vue-web-extension my-extension

安装时会打开一个向导,会询问一堆问题。回答可参考:

? Pick an ESLint config: Prettier
? Pick additional lint features: Lint on save
? Which browser extension components do you wish to generate? background, popup
? Generate a new signing key (danger)? No
? Install axios? Yes

接下来,切换到项目目录,然后就可以运行项目构建我们的新插件了:

cd my-extension
npm run serve

运行之后,会将项目打包到项目根目录的 dist 文件夹中,我们编码时,脚手架能够监视代码更改并同步至 dist 文件夹下。

我们再来看下脚手架的工程目录结构:

.
|-- .browserslistrc
|-- .eslintrc.js
|-- .gitignore
|-- babel.config.js
|-- package-lock.json
|-- package.json
|-- vue.config.js
|-- public
|   |-- browser-extension.html
|   |-- favicon.ico
|   |-- index.html
|   |-- icons
|   |   |-- 128.png
|   |   |-- 16.png
|   |   |-- 19.png
|   |   |-- 38.png
|   |   |-- 48.png
|   |-- _locales
|       |-- en
|           |-- messages.json
|-- src
    |-- App.vue
    |-- background.js
    |-- main.js
    |-- manifest.json
    |-- assets
    |   |-- logo.png
    |-- components
    |   |-- HelloWorld.vue
    |-- popup
    |   |-- App.vue
    |   |-- main.js
    |-- router
    |   |-- index.js
    |-- store
    |   |-- index.js
    |-- views
        |-- About.vue
        |-- Home.vue

src文件夹包含我们将用于扩展的所有文件。manifest.jsonbackground.js 对于我们来说是熟悉的。Vue 组件通过 vue-loader 管理,并在项目构建时,将所有 .vue 文件打包编译成一个浏览器可以理解的 JavaScript 包。

src 文件夹中还有一个 icons 文件夹。如果你看一眼 Chrome 的工具栏,会看到我们的扩展程序的新图标,这个图标就是从此文件夹拿的。当我们单击它时,会弹出一个窗口,窗口的内容是由 popup/App.vue 创建的。

接下来我们可以下载一些组件库,使产品样式看起来更加优美,例如:elementUI 等。

效果展示

当我们输入用户名密码后,点击登录,如果弹窗内展示一串 token 文本,代表登陆成功,此时刷新当前页面即可看到业务平台正常展示数据。如果展示其他报错信息,代表登陆失败,请检查输入的用户名密码是否正确。

123.jpg

参考链接

Chrome 插件开发全攻略
chrome 扩展开发中文教程
如何使用 Vue 构建 Chrome 扩展
vue-web-extension 脚手架
Cookie 的 SameSite 属性