反复切换"账号"有些麻烦? 写个谷歌插件帮你快速切换用户登录态
背景
随着业务的不断壮大以及用户类型的不断增多, 我们要切换不同账号进行"调试" & "测试"。
想要做一个插件针对所有依赖cookie做登录态记录的网站进行登录态的切换, 在开发插件的过程中学到了不少知识, 所以这次将开发这个插件的过程分享出来。
一、当前项目切换账号的流程
第一步:
点击退出 (会加载3s以上), 因为不只是清除本地的cookie还要清除服务端的登录态。
第二步:
切换到正确的登录方式, 手机登录还是邮箱登录,输入账户密码, 当然浏览器会帮你记住密码 (会加载3s以上)
第三步:需要重新找目标页面 (2s以上)
因为登录成功会默认跳回home页面, 所以我们要重新跳转到目标页面。
总结
上述步骤虽然用时不长, 但是可以感知到其是有优化空间的, 比如需要手动选择用户以及密码, 并且这些账号密码没有一个语义化的命名, 需要自己去记忆哪些账号与邮箱下面对应的人是有什么权限的。
二、技术分析与技术目标
原理:
市面上一大批"用户登录"流程的原理是将用户"token"储存在"cookie"里, 并设置为"httpOnly"模式(防止前端代码改动cookie), 然后每次用户请求都会带上这个"cookie", "server"通过校验这个"cookie"来进行身份识别, 从而得知请求是哪个用户发送的。
当然也可以是后端返回给前端后, 前端储存在"localStorage", 每次请求时都在header里带上这个"token", 后续与上面流程一致。
并且这个"token"几乎都会有一个过期时间, 当"token"失效时"server"一般会返回特定的状态码, 比如返回401代表了登录态无效, 前端识别出状态码为401则跳转至登录流程。
分析:
所谓"登录态"其实可以抽象的理解为在前端储存的"token", 而我们模拟登录态就是获取到这个"token", 其实链路已经很明朗了, 复制用户当前的所有"cookie"就是记录了一个用户, 当用户登录了另一个账号想要切回之前的账户时, 我们将之前的"cookie"赋予给当前域名即可完成用户的切换。
比较难解决的问题就是如何获取"httpOnly"状态的数据, 并且将数据正确赋予回去。
目标
- 可以在登陆过的多个账号之间切换, 并且可以为登录的账号命名。
- 可以根据域名进行账号的分组, 可对账号增删改查。
- 可以将自己的登录信息"分享"给其他人。
- 可推广至所有使用cookie储存登录信息的场景下使用。
三、谷歌插件登场吧
推荐我之前写的几篇文章:
谷歌插件入门文章推荐(上)
谷歌插件入门文章推荐(下)
先把谷歌插件的权限开起来, 因为可能要调用所有的domain下面的cookie, 所以权限(permissions)不开到最大是做不到的:
manifest.json
{
"manifest_version": 2,
"version": "0.1",
"name": "切换用户",
"description": "通过记录用户登录信息, 快速切换用户",
"permissions": ["cookies", "", "tabs", "storage"],
"browser_action": {
"default_popup": "popup/index.html",
"default_icon": "images/logo.png"
}
}
新建popup文件夹, 内含index.html文件
Document
四、svelte开发页面
创建svelte项目名字叫dev_popup, 并如下方式对其配置rollup.config.js
output: {
sourcemap: false,
format: "iife",
name: "app",
file: "../popup/index.js",
},
在 App.svelte 文件里面随便写个结构, 并且给个基础样式:
你好
看到下图的效果代表引入成功了:
这个popup弹出框是每次"弹出"都会重新执行内部代码, 接下来我们就尝试获取所在页面的信息。
五、获取当前页面的url, domain
由于每次我们点击"弹出框"以外的区域都会造成弹出框的隐藏, 所以其实每次打开弹框都需要检测一次当前所处的页面是什么, 而所谓的"当前页面"就是当前处于激活"lastFocusedWindow"&"active", 最后获取焦点的窗口并且被激活的tab, 这个逻辑我们写在第一行即可:
chrome.tabs.query({ active: true, lastFocusedWindow: true }, (tabs) => {
console.log(tabs);
});
上述获取的数据没有给出相应的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可以传参的列表:
当时我第一次选择的是传入 domain 来获取cookie, 但是问题就是按domain获取到的数据实在太多了, 公司内部的网站可以获取到150+条数据, 因为我们要实现用户信息的持久储存, 并且谷歌插件的本地储存容量是 "5M", 当我们设置cookie的时候会很大一部分cookie是会报设置失败的错的, 所以我们一定是要更精准的获取到cookie。
chrome.cookies.getAll({ url }, function (cookies) {
console.log(cookies)
});
在图里我们可以看出, 这种方式是可以获取到 "httpOnly"为"true" 的值的, 并且在数据中我们可以发现domain的样子有些奇怪, 为什么分为两种: "www.baidu.com" & ".baidu.com" 这两种写法又有什么区别那, 看到了当然要研究一下。
七、domain 以"点"开头
比如当前在百度贴吧https://tieba.baidu.com
这样的页面, 其内有cookie为下图
在控制台读取cookie:
发送请求, 虽然domain为www.baidu.com
可以显示在application中, 但是读取不到并且请求不会携带:
并且无法设置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是原生的弹出输入框, 回调结果就是用户输入的内容, 这个我们让使用者为新增的"账号"起名:
并且按照domain分组, 储存用户的所有cookie信息, 数据结构如下图:
十、 切换用户
这里的本质就是将所有的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注入错误:
十一、 可分享的登录态(导入导出)
当我们自己用过输入账号密码的方式登陆了一个账户并记录下来, 那么其实其他同学就没必要再走一遍这个流程了, 我们可以直接将我们储存起来的用户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;
}
}
此时cookie信息已经在我们的剪切板里面了, 直接粘贴即可:
十二、 删除用户
当然要有删除功能啦, 并且很可能因为插件内存只有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前置方便用户使用。
十四、 登录状态清除
用户需要切换账户来记录不同的账户, 但是如果直接使用页面上的退出登录理论上服务端会将用户此时的登录认证信息清除, 也就是我们记录的cookie信息被无效化了, 所以需要帮助用户手动清除cookie, 这样只要刷新页面就是自动跳到登录页, 并且还保留了用户登录态的有效性:
function handleClearCookie() {
chrome.cookies.getAll({ url }, function (cookies) {
activeUser(cookies, true);
alert("cookie 清理完毕请刷新");
});
}
end
这次就是这样, 希望与你一起进步。