反覆切換"賬號"有些麻煩? 寫個谷歌外掛幫你快速切換使用者登入態

lulu_up發表於2021-12-26

反覆切換"賬號"有些麻煩? 寫個谷歌外掛幫你快速切換使用者登入態

image.png

背景

     隨著業務的不斷壯大以及使用者型別的不斷增多, 我們要切換不同賬號進行"除錯" & "測試"。

     想要做一個外掛針對所有依賴cookie做登入態記錄的網站進行登入態的切換, 在開發外掛的過程中學到了不少知識, 所以這次將開發這個外掛的過程分享出來。

一、當前專案切換賬號的流程

第一步:

     點選退出 (會載入3s以上), 因為不只是清除本地的cookie還要清除服務端的登入態。

image.png

第二步:

     切換到正確的登入方式, 手機登入還是郵箱登入,輸入賬戶密碼, 當然瀏覽器會幫你記住密碼 (會載入3s以上)

image.png

第三步:需要重新找目標頁面 (2s以上)

     因為登入成功會預設跳回home頁面, 所以我們要重新跳轉到目標頁面。

總結

     上述步驟雖然用時不長, 但是可以感知到其是有優化空間的, 比如需要手動選擇使用者以及密碼, 並且這些賬號密碼沒有一個語義化的命名, 需要自己去記憶哪些賬號與郵箱下面對應的人是有什麼許可權的。

二、技術分析與技術目標

原理:

     市面上一大批"使用者登入"流程的原理是將使用者"token"儲存在"cookie"裡, 並設定為"httpOnly"模式(防止前端程式碼改動cookie), 然後每次使用者請求都會帶上這個"cookie", "server"通過校驗這個"cookie"來進行身份識別, 從而得知請求是哪個使用者傳送的。

     當然也可以是後端返回給前端後, 前端儲存在"localStorage", 每次請求時都在header裡帶上這個"token", 後續與上面流程一致。

     並且這個"token"幾乎都會有一個過期時間, 當"token"失效時"server"一般會返回特定的狀態碼, 比如返回401代表了登入態無效, 前端識別出狀態碼為401則跳轉至登入流程。

分析:

     所謂"登入態"其實可以抽象的理解為在前端儲存的"token", 而我們模擬登入態就是獲取到這個"token", 其實鏈路已經很明朗了, 複製使用者當前的所有"cookie"就是記錄了一個使用者, 當使用者登入了另一個賬號想要切回之前的賬戶時, 我們將之前的"cookie"賦予給當前域名即可完成使用者的切換。

     比較難解決的問題就是如何獲取"httpOnly"狀態的資料, 並且將資料正確賦予回去。

目標
  1. 可以在登陸過的多個賬號之間切換, 並且可以為登入的賬號命名。
  2. 可以根據域名進行賬號的分組, 可對賬號增刪改查。
  3. 可以將自己的登入資訊"分享"給其他人。
  4. 可推廣至所有使用cookie儲存登入資訊的場景下使用。

三、谷歌外掛登場吧

推薦我之前寫的幾篇文章:
谷歌外掛入門文章推薦(上)
谷歌外掛入門文章推薦(下)

     先把谷歌外掛的許可權開起來, 因為可能要呼叫所有的domain下面的cookie, 所以許可權(permissions)不開到最大是做不到的:

manifest.json

{
  "manifest_version": 2,
  "version": "0.1",
  "name": "切換使用者",
  "description": "通過記錄使用者登入資訊, 快速切換使用者",
  "permissions": ["cookies", "<all_urls>", "tabs", "storage"],
  "browser_action": {
    "default_popup": "popup/index.html",
    "default_icon": "images/logo.png"
  }
}

image.png

新建popup資料夾, 內含index.html檔案

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <link rel="stylesheet" href="./bundle.css" />
  </head>
  <body>
    <script src="./index.js"></script>
  </body>
</html>

image.png

四、svelte開發頁面

推薦我之前的兩篇文章
svelte入門上
svelte入門下

建立svelte專案名字叫dev_popup, 並如下方式對其配置rollup.config.js

  output: {
    sourcemap: false,
    format: "iife",
    name: "app",
    file: "../popup/index.js",
  },

在 App.svelte 檔案裡面隨便寫個結構, 並且給個基礎樣式:

<main class="wrap">
  <div>你好</div>
</main>
<style>
ul,
li {
  list-style: none;
  margin: 0;
  padding: 0;
}
.wrap {
  overflow: auto;
  color: rgb(71, 68, 68);
  width: 600px;
  height: 400px;
  padding: 20px;
  padding-bottom: 30px;
}
</style>

看到下圖的效果代表引入成功了:

nnnn.png

這個popup彈出框是每次"彈出"都會重新執行內部程式碼, 接下來我們就嘗試獲取所在頁面的資訊。

五、獲取當前頁面的url, domain

     由於每次我們點選"彈出框"以外的區域都會造成彈出框的隱藏, 所以其實每次開啟彈框都需要檢測一次當前所處的頁面是什麼, 而所謂的"當前頁面"就是當前處於啟用"lastFocusedWindow"&"active", 最後獲取焦點的視窗並且被啟用的tab, 這個邏輯我們寫在第一行即可:

  chrome.tabs.query({ active: true, lastFocusedWindow: true }, (tabs) => {
      console.log(tabs);
  });

image.png

     上述獲取的資料沒有給出相應的domain, 我們自己手動用正則/^(https?:\/\/([^\/]+))?/g;解析一下。

  let url = "";
  let domain = "";

  chrome.tabs.query({ active: true, lastFocusedWindow: true }, (tabs) => {
    getDomain(tabs[0].url);
  });

  function getDomain(weburl) {
    const urlReg = /^(https?:\/\/([^\/]+))?/g;
    const res = urlReg.exec(weburl);
    domain = res[2];
    url = res[0];
  }

     上述正則的意思是, 以http://或https://開頭, 匹配出內容中沒有出現"/"的部分, 這部分就是domain啦, 因為這裡設計使用網站的domain為key來儲存使用者的登入資訊。

六、獲取當前域名下"cookie", 攻克httpOnly

     這裡我查到可以使用 chrome.cookies.getAll(options, callback) 的方式獲取到使用者所有的cookie, 但是這個方法的傳參有些需要坑要注意。

     它的options可以傳參的列表:

image.png

     當時我第一次選擇的是傳入 domain 來獲取cookie, 但是問題就是按domain獲取到的資料實在太多了, 公司內部的網站可以獲取到150+條資料, 因為我們要實現使用者資訊的持久儲存, 並且谷歌外掛的本地儲存容量是 "5M", 當我們設定cookie的時候會很大一部分cookie是會報設定失敗的錯的, 所以我們一定是要更精準的獲取到cookie。

    chrome.cookies.getAll({ url }, function (cookies) {
           console.log(cookies)
    });

image.png

     在圖裡我們可以看出, 這種方式是可以獲取到 "httpOnly"為"true" 的值的, 並且在資料中我們可以發現domain的樣子有些奇怪, 為什麼分為兩種: "www.baidu.com" & ".baidu.com" 這兩種寫法又有什麼區別那, 看到了當然要研究一下。

七、domain 以"點"開頭

     比如當前在百度貼吧https://tieba.baidu.com這樣的頁面, 其內有cookie為下圖

image.png

     在控制檯讀取cookie:

image.png

     傳送請求, 雖然domain為www.baidu.com可以顯示在application中, 但是讀取不到並且請求不會攜帶:

image.png

     並且無法設定domain為www.baidu.com, 通過查詢資料發現, 子域名想要獲取到上級域名的cookie, 需要上級域名以"點"開頭才能傳遞給下級域名。

八、儲存使用者資訊

     我們可以獲取到使用者的cookie資訊, 則我們要把這個資訊儲存起來, 這樣等使用者下次使用我們的外掛還是可以看到之前的使用者資訊, 谷歌外掛提供了與localStorage差不多的api:

儲存: (要序列化一下)

        chrome.storage.local.set({
          users: JSON.stringify(users),
        });

獲取:(需反序列化)

    chrome.storage.local.get("users", (local) => {
      const users = JSON.parse(local.users || null) || {};
      console.log()
    });

我們可以把獲取使用者資訊抽象成一個函式:

  function getUserData(cb) {
    chrome.storage.local.get("users", (local) => {
      const users = JSON.parse(local.users || null) || {};
      cb(users);
    });
  }

九、建立使用者

     所謂建立使用者, 本質就是記錄當前所在頁面的使用者的cookie資訊, 並將資訊存在chrome.storage.local, 知識點無非就是簡單設計個資料結構而已:

  function showCreateUser() {
    const userName = prompt("填寫使用者名稱", "");
    if (userName) {
      createUser(userName);
    }
  }
    function createUser(userName) {
    chrome.cookies.getAll({ url }, function (cookies) {
      getUserData((users) => {
        const obj = { userName, cookies, createTime: getNowTime() };
        users[domain] ? users[domain].push(obj) : (users[domain] = [obj]);
        chrome.storage.local.set({
          users: JSON.stringify(users),
        });
        data = users;
      });
    });
  }

     prompt 這個原生的api是不是已經忘了? 沒錯說的就是你!

     這個api是原生的彈出輸入框, 回撥結果就是使用者輸入的內容, 這個我們讓使用者為新增的"賬號"起名:

image.png

並且按照domain分組, 儲存使用者的所有cookie資訊, 資料結構如下圖:

image.png

十、 切換使用者

     這裡的本質就是將所有的cookie賦予給當前網站, 也就是 chrome.cookies.set, 當然這個可以直接呼叫api, 每次把cookies傳進來, 我們迴圈放入, 因為谷歌沒有提供一次批量放入的api:

  function activeUser(cookies, clearCookies) {
    cookies.forEach((item) => {
      chrome.cookies.set({
        url,
        name: item.name,
        value: clearCookies ? "" : item.value,
        domain: item.domain,
        path: item.path,
        httpOnly: item.httpOnly,
        secure: item.secure,
        storeId: item.storeId,
        expirationDate: item.expirationDate,
      });
    });
    alert("切換成功");
  }

     這裡希望能夠完整還原使用者之前的cookie結構, 所以填寫了很多內容, 值得注意的是url是必填項, 不填會報錯的, 這個一定要寫當前的url, 這樣防止其他域名下的cookie注入錯誤:

image.png

十一、 可分享的登入態(匯入匯出)

     當我們自己用過輸入賬號密碼的方式登陸了一個賬戶並記錄下來, 那麼其實其他同學就沒必要再走一遍這個流程了, 我們可以直接將我們儲存起來的使用者cookie資訊, 分享給其他人使用。

     原理就是當點選"匯出"按鈕時, 將我們外掛裡面的資訊複製到使用者的剪下板, 使用者點選匯入按鈕時將資訊貼上進來就ok了:

  function shareUser(key, user) {
    const obj = {
      key,
      userName: user.userName,
      cookies: user.cookies,
      createTime: user.createTime,
    };
    copy(JSON.stringify(obj));
    alert("複製成功! 匯入即可使用");
  }

  function handleExportData() {
    let exportData = prompt("輸入匯入資訊", "");
    if (exportData) {
      exportData = JSON.parse(exportData);
      if (!data[exportData.key]) data[exportData.key] = [];
      data[exportData.key].push({
        cookies: exportData.cookies,
        userName: exportData.userName,
        createTime: exportData.createTime,
      });
      chrome.storage.local.set({
        users: JSON.stringify(data),
      });
      data = data;
    }
  }

image.png

此時cookie資訊已經在我們的剪下板裡面了, 直接貼上即可:

image.png

十二、 刪除使用者

     當然要有刪除功能啦, 並且很可能因為外掛記憶體只有5M這個限制, 刪除功能比較重要的。


  function deleteUser(domain, index) {
    getUserData((users) => {
      users[domain].splice(index, 1);
      chrome.storage.local.set({
        users: JSON.stringify(users),
      });
      data = users;
    });
  }

     這裡根據domain進行刪除第n個元素, 因為我沒給它們生成id, 所以直接按位置刪除即可。

十三、 網站間隔離展示

     這裡就是個簡單的樣式優化啦, 將未匹配到的domain淡化顯示, 並且將切換按鈕隱藏, 將匹配到當前地址的url前置方便使用者使用。

image.png

十四、 登入狀態清除

     使用者需要切換賬戶來記錄不同的賬戶, 但是如果直接使用頁面上的退出登入理論上服務端會將使用者此時的登入認證資訊清除, 也就是我們記錄的cookie資訊被無效化了, 所以需要幫助使用者手動清除cookie, 這樣只要重新整理頁面就是自動跳到登入頁, 並且還保留了使用者登入態的有效性:

  function handleClearCookie() {
    chrome.cookies.getAll({ url }, function (cookies) {
      activeUser(cookies, true);
      alert("cookie 清理完畢請重新整理");
    });
  }

end

     這次就是這樣, 希望與你一起進步。

相關文章