因为懒,我写了个同步 cookie 的插件

为什么需要同步 cookie 的需求?

因为我们公司统一登录、统一认证体系实现方式是通过在公司域名下的 cookie 注入 acces_token 等内容,然后在不同系统间通过携带的 cookie 信息进行认证并跳转到对应系统。因为本地开发环境 localhost 和公司域名不在同一个域下,导致需要模拟登录后,需要手动将相关 cookie 信息拷贝在 main.js 文件中,注入到 localhost 域名下。这就导致每次换一个用户登录,我就要手动复制下面这些内容,而且当 cookie 过期时也要重复一遍这样的操作,这对一个程序员来说太繁琐了,太麻烦了,严重影响了摸鱼时间

1// 每次在开发环境都要手动复制 4 个 cookie 信息 2const evnNode = process.env.VUE_APP_ENV 3 4if (evnNode === 'development') { 5 document.cookie = 'access_token=xxx' 6 document.cookie = 'refresh_token=xxx' 7 document.cookie = 'token_since=123' 8 document.cookie = 'original_access_token=xxx' 9}

所以在这样一个背景下,我开始探索有什么办法能不用每次都手动复制这 4 个复制 cookie 的方案

最初想到的方案是直接通过获取公司域名下的 cookie 信息,但因为浏览器的安全性质,是不能获取跨域的 cookie 信息的,这个时候又想到改造浏览器的安全限制,但这个方案不具有通用性,就先放弃了。第二个考虑的方案是本地起一个 node 中间件,通过这个中间间实现携带 cookie,但是因为实现复杂也放弃了

之后在一次偶然的百度中发现 chrome 插件可以突破跨域的限制,获取到不同域名下的 cookie,然后百度了一下 chrome 插件的开发者文档,找到了监听 cookie 变化的事件,研究到这里,我觉得可以开始实现需求了

撸起袖子开始干

一个 chrome 插件本质也是一个前端应用运行在 chrome 浏览器的环境里,所以直接就选择了 Vue3 + Vite2 进行开发。先用 pnpm create vite 初始化一个 vite 项目,安装好需要使用的 UI 库 Ant Design Vue,删掉无用的内容之后先得到一个基础的项目结构

接下来配置 chrome 插件的信息,chrome 插件主要是在 manifest.json 文件中配置基础信息。在 public 目录下新建一个 manifast.json 文件,文件中有几个配置是比较重要的,这里特别解释一下

  • manifest_version:定义配置清单的版本,从 Chrome 88 开始就是 V3,我是用的也是 3 这个版本
  • permissions:申请操作 chrome 的一些操作权限,这个插件里我主要用到的是 storage 和 cookies 的权限
  • host_permissions:申请有权限操作的域名,这里直接指定所有域名 "<all_urls>" 即可
  • background:后台运行脚本指定的属性,可以是 HTML,也可以是 JS 文件,主要是用于在后台监听 cookie 变化

插件的 icon 我是在阿里的 iconfont 上下载的,下载时可以选择不同的大小,其他信息就直接附上源码好了

1{ 2 "manifest_version": 3, 3 "name": "sync-cookie-extension", 4 "version": "1.0.0", 5 "description": "开发环境同步测试 cookie 至 localhost,便于本地请求服务携带 cookie", 6 "icons": { 7 "16": "sources/cookie16.png", 8 "32": "sources/cookie32.png", 9 "48": "sources/cookie48.png", 10 "128": "sources/cookie128.png" 11 }, 12 "action": { 13 "default_icon": "sources/cookie48.png", 14 "default_title": "解决本地开发 localhost 请求无法携带 cookie 问题", 15 "default_popup": "index.html" 16 }, 17 "permissions": ["storage", "cookies"], 18 "host_permissions": ["<all_urls>"], 19 "background": { 20 "service_worker": "background.js", 21 "type": "module" 22 } 23}

然后就是插件的功能开发,根据需求这个插件主要实现的两个功能

  1. 支持配置需要同步到本地的域名和 cookie 名称,支持开启和关闭同步

  2. 当配置列表中的 cookie 发生变化时,能够将同步至本地

第一个功能就是基于可编辑表格的 CRUD 一套功能,我是用的 Ant Design Vue 来开发的,一套操作下来页面效果是这样的(源码地址

下面就是实现最主要的同步功能:当 from 字段下 cookie name 发上变化时,将 cookie 同步至 to 字段对应的域名下(默认是 localhost )

第一步先要将我们在列表中配置的域名信息存储在 localstorage 中,一方面为了在插件后台中能够获取到需要同步的列表,另一方面当插件刷新时列表信息也不会丢失。然后还要写一个同步 cookie 的方法 updateCookie 方法用于加载时第一次同步 cookie

1// 在 useStorage.ts 中定义存储 localstorage 方法和更新 cookie 的方法 2import { 3 ICookieTableDataSource, 4 ICookie, 5 TCookieConfig, 6 LIST_KEY, 7} from '../type' 8 9// 增加协议头 10function addProtocol(uri: string) { 11 return uri.startsWith('http') ? uri : `http://${uri}` 12} 13 14// 移除协议头 15function removeProtocol(uri: string) { 16 return uri.startsWith('http') 17 ? uri.replace('http://', '').replace('https://', '') 18 : uri 19} 20 21const useStorage = () => { 22 async function updateStorage(list: ICookieTableDataSource[]) { 23 await chrome.storage.local.set({ [LIST_KEY]: list }) 24 } 25 26 async function getStorage(key = LIST_KEY) { 27 return await chrome.storage.local.get(key) 28 } 29 30 async function updateCookie(config: TCookieConfig) { 31 try { 32 const cookie = await chrome.cookies.get({ 33 url: addProtocol(config.from || 'url'), 34 name: config.cookieName || 'name', 35 }) 36 37 return cookie ? await setCookie(cookie, config) : null 38 } catch (error) { 39 console.error('error: ', error) 40 } 41 } 42 43 function setCookie(cookie: ICookie, config: TCookieConfig) { 44 return chrome.cookies.set({ 45 url: addProtocol(config.to || 'url'), 46 domain: removeProtocol(config.to || 'url'), 47 name: cookie.name, 48 path: '/', 49 value: cookie.value, 50 }) 51 } 52 53 return { 54 updateStorage, 55 getStorage, 56 updateCookie, 57 } 58} 59 60export default useStorage

第二步就是在插件首次加载的时候,从 localhost 读取是否开启同步和配置列表,然后读取配置列表的信息更新 cookie

1// 读取是否同步开启和配置列表 2const dataSource = ref<ICookieTableDataSource[]>(DEFAULT_LIST) // DEFAULT_LIST 是默认最初的同步列表,这样第一次加载插件时 localstorage 为空的话也不用手动在写一遍 3 4const { updateStorage, getStorage, updateCookie } = useStorage() 5 6onMounted(async () => { 7 // 初始化开启同步状态 8 const openSyncLocal = await getStorage('isOpenSync') 9 10 if (!isEmpty(openSyncLocal)) { 11 isOpenSync.value = openSyncLocal.isOpenSync 12 } 13 14 // 从 localStorage 初始化数据 15 const storage = await getStorage() 16 const domainList = !isEmpty(storage) 17 ? (Object.values(storage[LIST_KEY]) as ICookieTableDataSource[]) 18 : [] 19 20 if (!isEmpty(domainList)) { 21 dataSource.value = domainList 22 } 23 24 // 更新 localStorage 和 cookie 25 if (!isEmpty(unref(dataSource))) { 26 updateStorage(dataSource.value) 27 28 dataSource.value.forEach((item) => { 29 updateCookie({ 30 from: item.from, 31 to: item.to, 32 cookieName: item.cookieName, 33 }) 34 }) 35 } 36})

第三步当是否开启同步状态和配置列表发生变化时需要更新 localhost,这里使用 watch 监听同步状态的改变,然后再保存同步列表的方法里新增更新 localstorage

1watch(isOpenSync, async () => { 2 await chrome.storage.local.set({ isOpenSync: isOpenSync.value }) 3}) 4 5async function handleSave(rowId: string) { 6 Object.assign( 7 dataSource.value.filter((item) => item.id === rowId)[0], 8 editableData[rowId] 9 ) 10 delete editableData[rowId] 11 // 更新 localStorage 12 updateStorage(dataSource.value) 13}

到这里已经实现的第一次的 cookie 同步功能,然后就要用到监听 cookie 变化的事件 chrome.cookies.onChanged.addListener 了。我们之前在 manifest.json 文件中配置了 background 这个参数,这个时候就要用上了

1"background": { 2 "service_worker": "background.js", 3 "type": "module" 4}

在项目 public 目录下新建 background.js,添加 cookie 改变监听事件函数,然后从 localhost 中获取是否开启同步状态和配置列表,在开启同步的状态下,从列表中找到需要更新的 cookie 同步至本地就可以了

1addCookiesChangeEvent() 2 3function addCookiesChangeEvent() { 4 console.log('start addCookiesChangeEvent') 5 chrome.cookies.onChanged.addListener(async ({ cookie, removed }) => { 6 // 判断是否开启同步 7 const openSyncObj = await chrome.storage.local.get('isOpenSync') 8 const isOpenSync = openSyncObj.isOpenSync 9 10 if (!isOpenSync) return 11 12 const storage = await chrome.storage.local.get(['domainList']) 13 14 if (Object.keys(storage).length === 0) return 15 const domainList = Object.values(storage['domainList']) 16 17 // 需求更新的 cookie 18 const target = domainList.find((item) => { 19 return ( 20 equalDomain(item.from, cookie.domain) && item.cookieName === cookie.name 21 ) 22 }) 23 24 if (target) { 25 if (removed) { 26 removeCookie(cookie, target) 27 } else { 28 setCookie(cookie, target) 29 } 30 } 31 }) 32} 33 34function setCookie(cookie, config) { 35 return chrome.cookies.set({ 36 url: addProtocol(config.to || 'url'), 37 domain: removeProtocol(config.to || 'url'), 38 name: cookie.name, 39 path: '/', 40 value: cookie.value, 41 }) 42} 43 44function removeCookie(cookie, config) { 45 chrome.cookies.remove({ 46 url: addProtocol(config.to || 'url'), 47 name: cookie.name, 48 }) 49} 50 51// 增加协议头 52function addProtocol(uri) { 53 return uri.startsWith('http') ? uri : `http://${uri}` 54} 55 56// 移除协议头 57function removeProtocol(uri) { 58 return uri.startsWith('http') 59 ? uri.replace('http://', '').replace('https://', '') 60 : uri 61} 62 63function equalDomain(domain1, domain2) { 64 return addProtocol(domain1) === addProtocol(domain2) 65}

到这里同步功能就已经实现了,接下来打包项目 pnpm run build,打开 chrome 浏览器开发者模式,选择“加载解压缩的扩展”,选择打包的 dist 文件安装,如果安装成功的话可以看到这样一个图标

最后测试一下插件的效果,在百度域名下输入一个测试域名,然后在 localhost 下刷新一下,可以看到 cookie 已经成功同步过去了,大功告成

代码我也上传到了 github,有兴趣的话大家也可以 star 支持一波,源码地址