同步cookie插件原理及實現示例

為什麼需要同步 cookie 的需求?

因為我們公司統一登錄、統一認證體系實現方式是通過在公司域名下的 cookie 註入 acces_token 等內容,然後在不同系統間通過攜帶的 cookie 信息進行認證並跳轉到對應系統。

因為本地開發環境 localhost 和公司域名不在同一個域下,導致需要模擬登錄後,需要手動將相關 cookie 信息拷貝在 main.js 文件中,註入到 localhost 域名下。這就導致每次換一個用戶登錄,我就要手動復制下面這些內容,而且當 cookie 過期時也要重復一遍這樣的操作,這對一個程序員來說太繁瑣瞭,太麻煩瞭,嚴重影響瞭摸魚時間

// 每次在開發環境都要手動復制 4 個 cookie 信息
const evnNode = process.env.VUE_APP_ENV
if (evnNode === 'development') {
  document.cookie = 'access_token=xxx'
  document.cookie = 'refresh_token=xxx'
  document.cookie = 'token_since=123'
  document.cookie = 'original_access_token=xxx'
}

所以在這樣一個背景下,我開始探索有什麼辦法能不用每次都手動復制這 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 上下載的,下載時可以選擇不同的大小,其他信息就直接附上源碼好瞭

{
  "manifest_version": 3,
  "name": "sync-cookie-extension",
  "version": "1.0.0",
  "description": "開發環境同步測試 cookie 至 localhost,便於本地請求服務攜帶 cookie",
  "icons": {
    "16": "sources/cookie16.png",
    "32": "sources/cookie32.png",
    "48": "sources/cookie48.png",
    "128": "sources/cookie128.png"
  },
  "action": {
    "default_icon": "sources/cookie48.png",
    "default_title": "解決本地開發 localhost 請求無法攜帶 cookie 問題",
    "default_popup": "index.html"
  },
  "permissions": ["storage", "cookies"],
  "host_permissions": ["<all_urls>"],
  "background": {
    "service_worker": "background.js",
    "type": "module"
  }
}

然後就是插件的功能開發,根據需求這個插件主要實現的兩個功能

  • 支持配置需要同步到本地的域名和 cookie 名稱,支持開啟和關閉同步
  • 當配置列表中的 cookie 發生變化時,能夠將同步至本地

第一個功能就是基於可編輯表格的 CRUD 一套功能,我是用的 Ant Design Vue 來開發的,一套操作下來頁面效果是這樣的(源碼地址)

下面就是實現最主要的同步功能:當 from 字段下 cookie name 發上變化時,將 cookie 同步至 to 字段對應的域名下(默認是 localhost )

第一步先要將我們在列表中配置的域名信息存儲在 localstorage 中,一方面為瞭在插件後臺中能夠獲取到需要同步的列表,另一方面當插件刷新時列表信息也不會丟失。然後還要寫一個同步 cookie 的方法 updateCookie 方法用於加載時第一次同步 cookie

// 在 useStorage.ts 中定義存儲 localstorage 方法和更新 cookie 的方法
import { ICookieTableDataSource, ICookie, TCookieConfig, LIST_KEY } from "../type";
// 增加協議頭
function addProtocol(uri: string) {
  return uri.startsWith("http") ? uri : `http://${uri}`;
}
// 移除協議頭
function removeProtocol(uri: string) {
  return uri.startsWith("http")
    ? uri.replace("http://", "").replace("https://", "")
    : uri;
}
const useStorage = () => {
  async function updateStorage(list: ICookieTableDataSource[]) {
    await chrome.storage.local.set({ [LIST_KEY]: list });
  }
  async function getStorage(key = LIST_KEY) {
    return await chrome.storage.local.get(key);
  }
  async function updateCookie(config: TCookieConfig) {
    try {
      const cookie = await chrome.cookies.get({
        url: addProtocol(config.from || "url"),
        name: config.cookieName || "name",
      });
      return cookie ? await setCookie(cookie, config) : null;
    } catch (error) {
      console.error("error: ", error);
    }
  }
  function setCookie(cookie: ICookie, config: TCookieConfig) {
    return chrome.cookies.set({
      url: addProtocol(config.to || "url"),
      domain: removeProtocol(config.to || "url"),
      name: cookie.name,
      path: "/",
      value: cookie.value,
    });
  }
  return {
    updateStorage,
    getStorage,
    updateCookie,
  };
};
export default useStorage;

第二步就是在插件首次加載的時候,從 localhost 讀取是否開啟同步和配置列表,然後讀取配置列表的信息更新 cookie

// 讀取是否同步開啟和配置列表
const dataSource = ref<ICookieTableDataSource[]>(DEFAULT_LIST); // DEFAULT_LIST 是默認最初的同步列表,這樣第一次加載插件時 localstorage 為空的話也不用手動在寫一遍
const { updateStorage, getStorage, updateCookie } = useStorage();
onMounted(async () => {
  // 初始化開啟同步狀態
  const openSyncLocal = await getStorage("isOpenSync");
  if (!isEmpty(openSyncLocal)) {
    isOpenSync.value = openSyncLocal.isOpenSync;
  }
  // 從 localStorage 初始化數據
  const storage = await getStorage();
  const domainList = !isEmpty(storage)
    ? (Object.values(storage[LIST_KEY]) as ICookieTableDataSource[])
    : [];
  if (!isEmpty(domainList)) {
    dataSource.value = domainList;
  }
  // 更新 localStorage 和 cookie
  if (!isEmpty(unref(dataSource))) {
    updateStorage(dataSource.value);
    dataSource.value.forEach((item) => {
      updateCookie({
        from: item.from,
        to: item.to,
        cookieName: item.cookieName,
      });
    });
  }
});

第三步當是否開啟同步狀態和配置列表發生變化時需要更新 localhost,這裡使用 watch 監聽同步狀態的改變,然後再保存同步列表的方法裡新增更新 localstorage

watch(isOpenSync, async () => {
  await chrome.storage.local.set({ isOpenSync: isOpenSync.value });
});
async function handleSave(rowId: string) {
  Object.assign(
    dataSource.value.filter((item) => item.id === rowId)[0],
    editableData[rowId]
  );
  delete editableData[rowId];
  // 更新 localStorage
  updateStorage(dataSource.value);
}

到這裡已經實現的第一次的 cookie 同步功能,然後就要用到監聽 cookie 變化的事件 chrome.cookies.onChanged.addListener 瞭。我們之前在 manifest.json 文件中配置瞭 background 這個參數,這個時候就要用上瞭

"background": {
  "service_worker": "background.js",
  "type": "module"
}

在項目 public 目錄下新建 background.js,添加 cookie 改變監聽事件函數,然後從 localhost 中獲取是否開啟同步狀態和配置列表,在開啟同步的狀態下,從列表中找到需要更新的 cookie 同步至本地就可以瞭

addCookiesChangeEvent();
function addCookiesChangeEvent() {
  console.log("start addCookiesChangeEvent");
  chrome.cookies.onChanged.addListener(async ({ cookie, removed }) => {
    // 判斷是否開啟同步
    const openSyncObj = await chrome.storage.local.get("isOpenSync");
    const isOpenSync = openSyncObj.isOpenSync;
    if (!isOpenSync) return;
    const storage = await chrome.storage.local.get(["domainList"]);
    if (Object.keys(storage).length === 0) return;
    const domainList = Object.values(storage["domainList"]);
    // 需求更新的 cookie
    const target = domainList.find((item) => {
      return (
        equalDomain(item.from, cookie.domain) && item.cookieName === cookie.name
      );
    });
    if (target) {
      if (removed) {
        removeCookie(cookie, target);
      } else {
        setCookie(cookie, target);
      }
    }
  });
}
function setCookie(cookie, config) {
  return chrome.cookies.set({
    url: addProtocol(config.to || "url"),
    domain: removeProtocol(config.to || "url"),
    name: cookie.name,
    path: "/",
    value: cookie.value,
  });
}
function removeCookie(cookie, config) {
  chrome.cookies.remove({
    url: addProtocol(config.to || "url"),
    name: cookie.name,
  });
}
// 增加協議頭
function addProtocol(uri) {
  return uri.startsWith("http") ? uri : `http://${uri}`;
}
// 移除協議頭
function removeProtocol(uri) {
  return uri.startsWith("http")
    ? uri.replace("http://", "").replace("https://", "")
    : uri;
}
function equalDomain(domain1, domain2) {
  return addProtocol(domain1) === addProtocol(domain2);
}

到這裡同步功能就已經實現瞭,接下來打包項目 pnpm run build,打開 chrome 瀏覽器開發者模式,選擇“加載解壓縮的擴展”,選擇打包的 dist 文件安裝,如果安裝成功的話可以看到這樣一個圖標

最後測試一下插件的效果,在百度域名下輸入一個測試域名,然後在 localhost 下刷新一下,可以看到 cookie 已經成功同步過去瞭,大功告成

代碼我也上傳到瞭 github,有興趣的話大傢也可以 star 支持一波,源碼地址

以上就是同步cookie插件原理及實現示例的詳細內容,更多關於同步cookie插件的資料請關註WalkonNet其它相關文章!

推薦閱讀: