本文由 简悦 SimpRead 转码, 原文地址 mp.weixin.qq.com
前言:由于私有化的时候需要用到 idass(统一登录),本地 ip 为:127.0.0.1:8080,统一登录地址为:11.123.456.10,统一登录后,idass 服务无法跨 ip 种植 cookie,于是乎,想自己写一个浏览器插件自动种植所需页面的 cookie 和 token 等凭证(每次调试手动种植 cookie 太费劲了😭)
什么是浏览器插件
想象一下,你的浏览器是个老实巴交的打工仔,每天只会按部就班地干活。插件就是给它装备的 "外挂"
比如:
屏蔽鸡汤文弹窗,装个广告拦截
想在电商网站自动比价,写个比价插件
替换网页所有的图片
从零开始造轮子
核心文件结构
my-extension/├── manifest.json # 插件配置文件├── icons/ # 图标(16x16, 48x48, 128x128)用于工具栏小图标,扩展管理页图标,商店展示大图标├── popup/ # 弹出窗口├── content-scripts/ # 注入页面的脚本└── background/ # 后台脚本1. manifest.json
{ // 必须字段:清单版本(Chrome 推荐使用 V3)"manifest_version": 3,// 插件名称(展示在商店和浏览器工具栏)"name": "My Extension",// 插件版本(格式为 x.x.x)"version": "1.0",// 插件描述(简洁说明功能)"description": "A demo extension",// 图标配置(不同尺寸用于不同场景)"icons": { "16": "icons/icon16.png", // 工具栏小图标 "48": "icons/icon48.png", // 扩展管理页图标 "128": "icons/icon128.png"// 商店展示大图标 },// 权限申请(访问浏览器API的权限列表)"permissions": [ "activeTab", // 访问当前激活标签页 "storage", // 使用本地存储(chrome.storage) "scripting" // 动态执行脚本(Manifest V3 新增) ],// 浏览器工具栏按钮配置"action": { "default_icon": "icons/icon16.png", // 默认图标 "default_popup": "popup/popup.html", // 点击弹出的页面 "default_title": "点击我" // 鼠标悬停提示 },// 后台脚本(Manifest V3 使用 Service Worker)"background": { "service_worker": "background/sw.js", "type": "module"// 可选:支持ES模块 },// 内容脚本(注入到匹配的网页中)"content_scripts": [ { "matches": ["https://*.example.com/*"], // 匹配的URL模式 "js": ["content-scripts/main.js"], // 注入的JS文件 "css": ["content-scripts/style.css"], // 注入的CSS文件 "run_at": "document_end" // 注入时机(document_idle/document_start) } ],// 选项页(用户配置页面)"options_page": "options/options.html"}2. Content Script
- 内容脚本用于操作当前网页的 dom,比如自动高亮页面的关键词
// content-scripts/main.jsdocument.querySelectorAll('p').forEach(p => { p.innerHTML = p.innerHTML.replace(/重要/g, '<span style="background:yellow">重要</span>');});3. Popup
- 点击浏览器工具栏图标展示的一个弹窗
<!DOCTYPE html><html> <head> <title>Webpage Demo</title> </head> <body> <h3>hello world</h3> </body></html>这样写的话点击浏览器插件就会出现 hello world
4. Background Script
在浏览器的后台运行
用于监听浏览器事件,跨标签通信等全局事件
比如写一个最简单的通过本地存储去存储 comment 列表
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {if (message.action === 'getComments') { chrome.storage.local.get(['comments'], (result) => { const comments = result.comments || [] sendResponse(comments); }); }if (message.action === 'saveComment') { chrome.storage.local.get(['comments'], (result) => { const comments = result.comments || [] comments.push(message.comment); chrome.storage.local.set({ ['comments']: comments }, () => { sendResponse({ status: 'saved' }); }); }); }});1. chrome.runtime.onMessage.addListener(...)
给整个插件注册一个 “消息监听器”。
任何从 popup.js、content-script 或其他地方用 chrome.runtime.sendMessage 发送到后台(background.js)的消息,都会进入这里
2. if (message.action === 'getComments')
触发条件:收到一个 action 字段是'getComments' 的消息。
功能:
从 chrome.storage.local 里读取键名是'comments' 的数据。
读取成功后,通过 sendResponse 把结果返回给发送消息的一方
3. if (message.action === 'saveComment')
触发条件:收到一个 action 字段是'saveComment' 的消息。
功能:
先读取现有的'comments'。
把新的评论 message.comment 加到 comments 里面。
然后重新用 chrome.storage.local.set 把更新后的 comments 保存回去。
保存成功后,调用 sendResponse({status: 'saved'}) 告知保存成功。
实战 demo
接前言来开发,先列下需求:
首先需要三个输入框,输入目标 ip 地址,允许的 cookie keys,以及 localStorage keys
插件还需要支持保存配置的功能
支持一键跳转到目标 ip 页面,并且会携带当前页面允许的 cookie keys 以及 localStorage keys 种植到目标页面去
首先需要一个可视化的操作弹窗:
1. popup.html
<body><h3>添加新配置</h3><input type="text" id="target-ip" placeholder="目标 IP 地址 (例如: 192.168.1.2)" /><textarea id="cookie-keys" placeholder="允许的 Cookie keys(用英文逗号 , 分隔)"></textarea><textarea id="storage-keys" placeholder="允许的 LocalStorage keys(用英文逗号 , 分隔)"></textarea><button id="save-config">保存配置</button><h3>配置列表</h3><div id="config-list"></div><script src="popup.js"></script></body>接下来是处理弹窗逻辑的 js 文件
2. popup.js
// 获取元素const targetIpInput = document.getElementById('target-ip');const cookieKeysInput = document.getElementById('cookie-keys');const storageKeysInput = document.getElementById('storage-keys');const configListDiv = document.getElementById('config-list');const saveButton = document.getElementById('save-config');// 保存新配置saveButton.addEventListener('click', () => {const targetIp = targetIpInput.value.trim();const cookieKeys = cookieKeysInput.value.split(',').map(k => k.trim()).filter(k => k);const storageKeys = storageKeysInput.value.split(',').map(k => k.trim()).filter(k => k);if (!targetIp) { alert('请输入目标 IP 地址'); return; }const newConfig = { targetIp, cookieKeys, storageKeys }; chrome.storage.local.get(['configs'], (result) => { const configs = result.configs || []; configs.push(newConfig); chrome.storage.local.set({ configs }, () => { renderConfigs(); targetIpInput.value = ''; cookieKeysInput.value = ''; storageKeysInput.value = ''; }); });});// 渲染已有配置function renderConfigs() { configListDiv.innerHTML = ''; chrome.storage.local.get(['configs'], (result) => { const configs = result.configs || []; configs.forEach((config, index) => { const div = document.createElement('div'); div.className = 'config-item'; div.innerHTML = ` <div><strong>IP:</strong> ${config.targetIp}</div> <div><strong>Cookies:</strong> ${config.cookieKeys.join(', ')}</div> <div><strong>LocalStorage:</strong> ${config.storageKeys.join(', ')}</div> <button data-index="${index}" class="run-btn">一键跳转</button> <button data-index="${index}" class="delete-btn">删除</button> `; configListDiv.appendChild(div); }); // 绑定按钮 document.querySelectorAll('.run-btn').forEach(btn => { btn.addEventListener('click', (e) => { const idx = e.target.dataset.index; startCopy(configs[idx]); }); }); document.querySelectorAll('.delete-btn').forEach(btn => { btn.addEventListener('click', (e) => { const idx = e.target.dataset.index; configs.splice(idx, 1); chrome.storage.local.set({ configs }, renderConfigs); }); }); });}// 开始同步并跳转function startCopy(config) { chrome.tabs.query({ active: true, currentWindow: true }, ([tab]) => { chrome.runtime.sendMessage({ action: 'copyData', config, sourceUrl: tab.url tabId: tab.id }, (response) => { if (!response.success) { alert('同步失败!'); } }); });}// 初始化renderConfigs();给按钮添加点击事件以及渲染配置都是很基础的,重点说一下 startCopy 这个方法
chrome.tabs.query({active: true, currentWindow: true}, ([tab]) => {...})
通过 chrome.tabs.query 方法,查询当前激活的标签页(active tab)
参数 {active: true, currentWindow: true}:
active: true —— 只找当前窗口中正在激活中的 tab(比如你打开的页面)。
currentWindow: true —— 限制只在当前浏览器窗口里找,不跨窗口
返回的是一个数组,通常数组里只有一个元素,目的是拿到当前页面的 url
chrome.runtime.sendMessage({action: 'copyData', config, sourceUrl: tab.url}, (response) => {...})
拿到当前页面后,就用 chrome.runtime.sendMessage 发送一个消息到后台脚本 background.js
参数会携带配置的 url,以及允许的 keys
然后就是会处理 background 回应的错误信息,这里主要是发消息,主要逻辑都在 background.js 内部
3. background.js
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {if (message.action === 'copyData') { const { config, sourceUrl, tabId } = message; const urlObj = new URL(sourceUrl); const domain = urlObj.hostname; // 读取 cookie chrome.cookies.getAll({ domain }, (cookies) => { const allowedCookies = cookies.filter(c => config.cookieKeys.includes(c.name)); // 设置到目标 IP allowedCookies.forEach(cookie => { chrome.cookies.set({ url: `http://${config.targetIp}`, name: cookie.name, value: cookie.value, path: cookie.path, secure: cookie.secure, httpOnly: cookie.httpOnly, sameSite: cookie.sameSite }); }); // 存储 localStorage chrome.tabs.sendMessage(tabId, { action: 'storeData', config }); // 发送消息给 content script 处理 localStorage chrome.tabs.create({ url: `http://${config.targetIp}` }, (tab) => { // 注意:要等新页面加载完成后才能注入 localStorage 操作 chrome.tabs.onUpdated.addListener(function listener(tab_id, info) { if (tab_id === tab.id && info.status === 'complete') { chrome.tabs.onUpdated.removeListener(listener); // 移除监听,避免多次触发 // 给目标tabId发送消息 chrome.tabs.sendMessage(tab_id, { action: 'copyLocalStorage', config }); } }); }); sendResponse({ success: true }); }); returntrue; // 异步 }});整体功能
✅ 从当前页面读取 cookie(指定 keys)
✅ 把这些 cookie 写到目标 IP
✅ 新开一个 tab 跳转到目标 IP
✅ 在目标 IP 页面注入 localStorage 数据
1. 监听消息
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {chrome.runtime.onMessage.addListener:监听插件内部发送的消息
message:收到的消息内容(action, config, sourceUrl)
sender:发送消息的 tab 信息(sender.tab)
sendResponse:回调函数,异步返回一个响应
2. 取出配置和源地址,并且读取 cookie
const { config, sourceUrl } = message;const urlObj = new URL(sourceUrl);const domain = urlObj.hostname;chrome.cookies.getAll({ domain }, (cookies) => {})3. 把 cookie 写到目标 IP
allowedCookies.forEach(cookie => { chrome.cookies.set({ url: `http://${config.targetIp}`, name: cookie.name, value: cookie.value, path: cookie.path, secure: cookie.secure, httpOnly: cookie.httpOnly, sameSite: cookie.sameSite });});4. 打开新标签页跳转到目标 IP
chrome.tabs.create({ url: `http://${config.targetIp}` }, (tab) => {})5. 等新页面加载完成,再注入 localStorage
chrome.tabs.onUpdated.addListener(function listener(tabId, info) { if (tabId === tab.id && info.status === 'complete') { chrome.tabs.onUpdated.removeListener(listener); chrome.tabs.sendMessage(tabId, { action: 'copyLocalStorage', config }); }});6. return true;
最后返回 ture 是因为:
- Chrome 规定:如果 sendResponse 是异步调用,必须 return true,否则页面收不到回调
注意点
下面就是处理注入 localStorage 的逻辑了,为什么不能在 backgroud 里直接种植呢?
因为 backgroud.js 相当于一个大脑,他能调用插件的所有 api,但是他不能操作页面的内容
它无法直接读取网页里的 DOM、localStorage、执行页面里的 JS
下面就是 content-script.js 了
4. content-script.js
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {if (message.action === 'copyLocalStorage') { // 当页面跳转到目标 IP 后,恢复 localStorage if (window.location.hostname.match(/^(\d{1,3}\.){3}\d{1,3}$/)) { console.log('页面跳转到目标 IP'); chrome.storage.local.get(['tempStorageData'], (result) => { console.log(result, 'result'); const data = result.tempStorageData || {}; Object.keys(data).forEach(key => { localStorage.setItem(key, data[key]); }); chrome.storage.local.remove('tempStorageData'); }); } }if (message.action === 'storeData') { const { storageKeys } = message.config; const storageData = {}; storageKeys.forEach(key => { console.log(localStorage.getItem(key)); const value = localStorage.getItem(key); if (value !== null) { storageData[key] = value; } }); console.log(storageData,'storageData'); chrome.storage.local.set({ tempStorageData: storageData }); }});这里面的逻辑很简单了,利用了一个临时缓存 tempStorageData 去种植 localStorage
5. 附上配置文件
{ "manifest_version": 3, // 指定使用 Manifest V3,这是 Chrome 插件目前推荐的新版本"name": "unlogin-plug-in", // 插件的名称"version": "1.0.0", // 插件版本号"description": "unlogin-plug-in", // 插件描述"permissions": [ "storage", // 允许使用 chrome.storage API,本地存储数据 "cookies", // 允许读取和设置 cookie "tabs", // 允许访问和操作浏览器标签页信息 "scripting"// 允许使用 chrome.scripting 注入脚本 ],"host_permissions": [ "<all_urls>"// 允许访问所有网站的页面,比如读取 cookie、注入脚本等 ],"action": { "default_popup": "popup.html"// 点击插件图标时,弹出的界面 HTML 文件 },"background": { "service_worker": "background.js"// 后台脚本,使用 service worker 方式运行 },"content_scripts": [ { "matches": ["<all_urls>"], // 指定在哪些页面注入 content-script,这里是所有页面 "js": ["content-script.js"] // 被注入的脚本文件,处理当前页面的 localStorage、DOM 操作等 } ]}小小测试了一下,功能大体是 OK 的😄
在这里插入图片描述
-END -
如果您关注前端 + AI 相关领域可以扫码进群交流
添加小编微信进群😊
关于奇舞团
奇舞团是 360 集团最大的大前端团队,非常重视人才培养,有工程师、讲师、翻译官、业务接口人、团队 Leader 等多种发展方向供员工选择,并辅以提供相应的技术力、专业力、通用力、领导力等培训课程。奇舞团以开放和求贤的心态欢迎各种优秀人才关注和加入奇舞团。