Skip to content

本文由 简悦 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

接前言来开发,先列下需求:

  1. 首先需要三个输入框,输入目标 ip 地址,允许的 cookie keys,以及 localStorage keys

  2. 插件还需要支持保存配置的功能

  3. 支持一键跳转到目标 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:回调函数,异步返回一个响应

const { config, sourceUrl } = message;const urlObj = new URL(sourceUrl);const domain = urlObj.hostname;chrome.cookies.getAll({ domain }, (cookies) => {})
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 等多种发展方向供员工选择,并辅以提供相应的技术力、专业力、通用力、领导力等培训课程。奇舞团以开放和求贤的心态欢迎各种优秀人才关注和加入奇舞团。