资讯动态

一键复制Cookie与Token:浏览器插件开发实战与踩坑记录

发布时间:2026/10/14 5:27:55 来源:尧图企业网站定制
简介这是一款面向前端开发者、爬虫工程师及日常调试网页人群的浏览器扩展工具解决手动从控制台逐条复制 Cookie、Token、LocalStorage、SessionStorage 的低效问题。插件支持一键复制当前网站的全部 Cookie并能智能识别 JWT、Bearer Token 等常见鉴权信息同时覆盖本地存储和会话存储的快速提取。压缩包共 6 个文件以 JS 脚本与 HTML 弹出界面为主辅以 manifest.json 配置、图标 SVG 及 README 使用说明整体仅 7KB轻量易部署。已有 1207 人学习下载。通过阅读源码和说明文档读者可以理解浏览器扩展的清单配置、内容脚本与弹出页交互逻辑也可以直接加载插件来提升接口调试和数据查看效率对学习扩展开发或需要频繁复制登录态的用户很有实用价值。1. 写接口文档和调接口之间缺的就是这一步做前后端联调的人大概都有这种经历打开页面登录一次然后打开开发者工具在 Network 里找到某个请求把 Cookie 复制出来再跑到本地调试工具里粘贴成 Header如果接口还要求 Token又得去 Application 面板翻 LocalStorage找到那个 key手动把值抠出来。动作不难但每天重复几十次之后你一定会想找一个能一键完成这件事的插件。这个标题里的项目做的就是把「打开调试工具 → 层层展开 → 手动复制 → 回去粘贴」压缩成一个点击动作。它面向的是所有被接口调试、爬虫脚本、自动化测试折磨的开发者适合那些一天要切换好几个环境、好几套账号凭证的人。我按这个思路做了一版可用的插件整个实现不复杂但权限配置、Token 的获取路径、剪贴板写入这几个环节都有各自的坑下面把能直接抄走的方案和踩过的坑一起写出来。2. 搭好插件骨架Manifest V3 里同时管住 Cookie 与 Token 的权限模型2.1 为什么选 Manifest V3以及它对 Cookie 读取的限制现在的 Chrome 系浏览器已经全面转向 Manifest V3新提交到应用商店的插件也必须用它。MV3 相比 V2 最大的变化是后台脚本从常驻的 background page 换成了 event-driven 的 Service Worker很多原本在后台随便干的活现在有了生命周期限制同时权限模型更严格host_permissions必须在 manifest 里显式声明不能在运行时通过用户授权补齐。这对做 Cookie 采集的插件来说影响很直接读取 Cookie 必须用chrome.cookiesAPI而这个 API 的访问范围完全由host_permissions决定。比如你只声明了https://example.com/*那你就只能读到这个域名下的 Cookie其他域名一概没有权限。如果声明成all_urls浏览器会弹出权限警告「可以读取你在所有网站上的数据」安装时用户的顾虑会明显增加。我的做法是默认给all_urls因为做调试工具的人目标就是「打开哪个页面就复制哪个页面的凭证」但如果你只想分发给团队内用建议收紧到你们自己的域名列表审核和信任成本都低一些。2.2 manifest.json 的最小配置权限、图标与弹窗入口直接给一份能跑的配置文件。我一般会用 Vite 或者 esbuild 来打包插件但开发调试阶段完全可以不引入构建工具直接写原生 JS目录结构保持浏览器插件最传统的形态即可。{ manifest_version: 3, name: Cookie Token Quick Copy, version: 1.0.0, description: 一键复制当前站点的 Cookie 与 Token面向接口调试场景, permissions: [cookies, activeTab, scripting, clipboardWrite], host_permissions: [all_urls], action: { default_popup: popup.html, default_title: 点击复制当前站点凭证 }, background: { service_worker: background.js }, icons: { 16: icons/icon16.png, 48: icons/icon48.png, 128: icons/icon128.png } }这里逐个说下权限字段的含义。cookies是读取 Cookie 的必备权限没有它chrome.cookies接口直接不可用activeTab让你在用户点击插件图标时获得当前活动标签页的临时访问权主要用于向页面注入脚本scripting配合activeTab使用用来执行代码获取页面里的 TokenclipboardWrite是在扩展环境中写剪贴板的正式途径后面会讲为什么不是所有场景都依赖它。host_permissions用all_urls是最省事的做法但它会触发浏览器的「读取所有网站数据」警告。如果你只调试某几个环境改成具体域名会干净很多。注意action.default_popup决定了点击图标弹出来的界面这个 popup 本身就是我们主要的交互入口所有复制功能都从这里触发。2.3 popup、background 与 content script 的分工这个插件涉及三种执行环境它们的职责边界一开始就要理清。popup 是用户点击图标后看到的弹窗它只在弹窗打开期间存活关掉就销毁background Service Worker 负责全局事件和调用需要扩展权限的 API比如chrome.cookiescontent script 则注入到普通页面里能访问页面的 DOM 和 JavaScript 运行环境但权限受限。我的分工是popup 负责 UI 和用户操作入口点击按钮后通过chrome.runtime.sendMessage通知 backgroundbackground 用chrome.cookies.getAll拿 Cookie同时用chrome.scripting.executeScript向当前页面注入代码去读 Tokencontent script这里更多是临时注入的函数负责从localStorage、sessionStorage或者页面全局变量里把 Token 挖出来再通过回调把值送回给 background。不要把读取 Cookie 的逻辑写在 content script 里因为 content script 拿不到chrome.cookiesAPI。也不要把 Token 的读取逻辑全部依赖 popup 里的 JS 直接执行因为 popup 的上下文跟目标页面完全隔离访问不到页面里的localStorage。各环境各司其职后面排错会省很多事。3. 用 chrome.cookies API 采集 Cookie最小实现与三种导出格式3.1 从 getAll 到格式化输出的完整链路核心代码在 background 里popup 发消息过来时我们获取当前标签页的 URL从 URL 里解出域名再用这个域名去过滤 Cookie。看下面的实现// background.js chrome.runtime.onMessage.addListener((message, sender, sendResponse) { if (message.type COPY_COOKIE) { handleCopyCookie(message.url) .then((result) sendResponse({ ok: true, data: result })) .catch((err) sendResponse({ ok: false, error: err.message })); return true; // 保持消息通道开启等待异步响应 } }); async function handleCopyCookie(pageUrl) { const url new URL(pageUrl); // 用 hostname 做域名匹配避免把子域名的 Cookie 混进来 const cookies await chrome.cookies.getAll({ domain: url.hostname }); if (!cookies.length) { throw new Error(当前域名下没有读取到 Cookie); } const kvPairs cookies.map((c) ${c.name}${c.value}); return { // header 格式用于直接粘贴到调试工具的 Cookie Header 里 header: kvPairs.join(; ), // json 格式便于脚本或代码里解析 json: JSON.stringify(cookies.reduce((acc, c) { acc[c.name] c.value; return acc; }, {}), null, 2), // 原始数组给爬虫场景用 raw: JSON.stringify(cookies, null, 2) }; }这段代码有几个需要注意的点。chrome.cookies.getAll支持按domain、name、url过滤我这里传的是domain因为getAll的domain匹配规则是「域名结尾匹配」也就是说传api.example.com能匹配到example.com下的相关 Cookie反过来传example.com也能拿到子域名的 Cookie。实际调试时这通常是你想要的但如果只想精确拿主域名就需要手动过滤一遍c.domain url.hostname的项。sendResponse前面那个return true很容易被漏掉。Service Worker 里如果监听器里发起的是异步操作你必须同步返回true告诉浏览器「这个响应我会稍后再给」否则消息通道会提前关闭popup 那边拿到的就是undefined。这是 MV3 下最常见的通信问题比 V2 时代敏感得多。3.2 三种导出格式分别用在什么场景我最终在插件里做了三种复制格式而不是只给一个「一键复制全部」。原因是不同场景对格式的要求完全不同接口调试工具里粘贴 Cookie Header需要的是key1value1; key2value2这种拼好的字符串直接塞进请求头就能用写脚本或者做自动化测试时往往要把它解析成字典结构所以生成 JSON 格式而抓包工具里可能需要原样的 Cookie 列表原始数组格式就派上用场。这三类需求在 team 里都是真实存在的。最开始我只做了一种格式结果同事反馈说从抓包工具里复制出来的内容还要再手动转一次后来才补全了三种。在 popup 里我用三个按钮分别触发这样用户不需要先复制再选择格式一步到位。// popup.js 里绑定按钮事件 document.getElementById(copy-header).addEventListener(click, async () { const [tab] await chrome.tabs.query({ active: true, currentWindow: true }); const response await chrome.runtime.sendMessage({ type: COPY_COOKIE, url: tab.url }); if (response.ok) { await copyText(response.data.header); } });这里求当前标签页的tab.url有个前提插件需要activeTab权限并且用户确实点击了插件图标。如果你用快捷键自动打开 popuptab.url可能因为权限不足拿不到返回的是undefined。遇到这种情况可以参考第 5 章里的排查方法。3.3 Cookie 过期时间与 HttpOnly 标记的处理chrome.cookies.getAll返回的 Cookie 对象里有两个字段值得留意expirationDate和httpOnly。expirationDate是 Unix 时间戳如果这个字段不存在说明是会话级 Cookie浏览器关掉就失效。对调试工具来说复制的时候不需要向用户展示过期时间但我在界面上加了一行提示展示当前有效 Cookie 数量和是否有 HttpOnly 标记的项。为什么要关心 HttpOnly正常用户通过浏览器访问网站时JavaScript 是读不到 HttpOnly Cookie 的所以就算你在页面里执行document.cookie也拿不全。但chrome.cookiesAPI 是浏览器扩展层面的接口不受 HttpOnly 限制能拿到这些值。反过来如果你是想用页面内脚本去采集 Cookie那 HttpOnly 的项天然缺失只能拿非 HttpOnly 的部分。这个差异在接爬虫项目的时候会直接影响最终数据是否完整提前知道比到时候对数据更稳妥。4. Token 获取的三条路Storage 直读、请求头截获与页面级注入4.1 第一条路从 LocalStorage 和 SessionStorage 里读Token 的存放位置没有统一标准不同站点、不同前端框架的做法差别很大。最常见的两种是存在localStorage的某个固定 key 下比如token、access_token、authorization或者存在sessionStorage里关闭标签页就丢。对前者我们可以直接用注入脚本去读后者同样可行但要注意时序问题。读取的方法很直接向当前页面注入一段脚本让它把两个 storage 里的内容都返回回来// 通过 scripting.executeScript 注入到目标页面 async function readTokenFromStorage(tabId) { const results await chrome.scripting.executeScript({ target: { tabId: tabId }, func: () { const all {}; for (let i 0; i localStorage.length; i) { const key localStorage.key(i); all[key] localStorage.getItem(key); } for (let i 0; i sessionStorage.length; i) { const key sessionStorage.key(i); all[session: key] sessionStorage.getItem(key); } return all; } }); return results[0].result; }这段代码没有直接返回某个固定 key而是把整个 storage 空间都列出来了。原因很现实你根本不知道目标站点的 token 存在哪个 key 下面与其猜不如先全部拿出来看一眼。我第一次给某内部系统做适配的时候以为 token 一定叫token结果人家叫user_info里面塞了一整段 Base64 编码的 JSONtoken 只是其中的一个字段。拿到所有存储项之后你需要一个「候选 key 匹配」逻辑遍历所有 key遇到包含token、auth、jwt、api_key字样的就优先展示。我在插件的界面上会把匹配到的项列出来让用户自己确认哪一条是当前要用的避免误复制。这里不做自动判断的另一个原因是很多站点的 token 是动态刷新的key 不变但值在变你没法保证规则永远正确。4.2 第二条路监听网络请求从 Authorization Header 里截有些站点的 token 从不进 storage只在内存变量里带着每次请求从闭包变量里取出来塞进 Header。这种情况下页面里读不到、storage 里也没有但它们发出去的请求里一定带着。用chrome.webRequest监听请求头是绕过去的办法。// background.js 中注册监听 chrome.webRequest.onSendHeaders.addListener( (details) { const authHeader details.requestHeaders.find( (h) h.name.toLowerCase() authorization ); if (authHeader) { // 在这里把 token 缓存到内存 latestToken authHeader.value.replace(/^Bearer\s/i, ); } }, { urls: [all_urls] }, [requestHeaders] );这个方案有两个明显代价。一是webRequest的监听在 MV3 里被限制普通扩展只能以「观察者」方式监听不能阻塞修改请求但onSendHeaders读取请求头仍然可用二是 Chrome 商店对webRequest权限审核很严格除非在描述里把用途写清楚否则大概率被拒。这是我在正式分发时遇到的实际问题。如果你只想在本地开发用这个方案没问题因为本地加载未打包扩展不受商店审核限制。我的建议是本地调试用 webRequest 截获发布到商店前移除相关权限把 Token 获取切换到前两种方案上。不要为一个调试工具去和审核规则硬碰。4.3 第三条路从页面全局变量或内存对象里挖最后一个比较「脏」的办法是直接在页面的 JS 上下文里翻全局对象。有些应用把用户信息挂载到window上比如window.__INITIAL_STATE__、window._appConfig或者某个全局变量里。我们可以在注入的函数里遍历window的顶层 key找到像包含当前用户信息或 token 的对象。// 注入页面执行从合理的全局变量里找 token func: () { const candidates []; for (const key of Object.keys(window)) { try { const val window[key]; if (!val) continue; const str JSON.stringify(val); if (str str.length 10000 (str.includes(token) || str.includes(access_token))) { candidates.push({ key, value: val }); } } catch (e) { // 跨域对象访问会抛错跳过即可 } } return candidates; }这段代码有几个边界要注意。Object.keys(window)拿到的 key 数量在大型站点上可能成百上千逐个JSON.stringify性能堪忧所以我加了字符串长度限制只分析长度不超过 10000 的值JSON.stringify如果遇到循环引用会抛错必须用try...catch包起来否则注入脚本直接挂掉读不到任何数据。还需要说明的是这条方案有 20% 的概率是要落空的因为现代前端框架很少再把 token 暴露到全局变量上压缩混淆之后的变量名也基本没有规律可循。我的插件里把这作为最后的手段排在 storage 直读之后。出现误判时用户可以在弹出面板里手动查看候选列表而不是傻傻的等一个永远为空的结果。5. 一键复制插件的 5 个常见坑从剪贴板失效到 Token 过期5.1 剪贴板 API 在 popup 场景失效怎么兜底现象在 popup 里直接调用navigator.clipboard.writeText()控制台不报错但粘贴出来是空的。原因navigator.clipboardAPI 在非用户手势触发的环境里会被浏览器拦截。popup 里的按钮点击本身算用户手势但如果你的代码里有await异步操作手势上下文可能在等待期间丢失Chrome 对「短暂用户激活」的窗口期规定很严格超过一定时间后调用就静默失败。解决给clipboardWrite权限加上还不算完我最终的写法是「先用navigator.clipboard.writeText尝试失败时回退到旧版document.execCommand(copy)」。注意旧版 API 需要一个可选中文本的输入框所以要在 popup 页面里临时插入一个textarea再复制复制完移除。实际测试中 execCommand 的兼容性和成功率反而更让人放心。5.2 跨域 iframe 里的 Cookie 读不到现象页面主体域名的 Cookie 复制正常但页面里嵌入的第三方 iframe比如支付模块的 Cookie 拿不到。原因chrome.cookies.getAll({ domain: parentHost })只匹配了当前标签页的主域名。跨域 iframe 的 Cookie 属于 iframe 的来源域名要读取它必须知道那个子域名的地址并单独匹配。解决用chrome.cookies.getAll({})不加条件获取所有 Cookie然后按domain字段分组展示。对调试场景来说这更实用一次点击把当前页面域、接口域名、CDN 域名的 Cookie 全部拿到用户自己挑需要的。代价是输出内容变多所以在 UI 上我把每个域名下的 Cookie 做成可折叠的分组。5.3 Token 复制成功但粘贴后发现已经过期现象从页面里拿到 token 的时候值是新鲜的几秒后粘贴到调试工具里发请求返回 401。原因很多应用的 token 有效期很短尤其是一些二次验证后的临时 token另外部分站点会定期自动刷新 token旧的访问令牌在刷新后立即失效。你复制的时机卡在刷新边界附近拿到的是刚被废弃的值。解决插件不做缓存每次点击都实时从 storage 或页面里重新读取再在复制按钮旁边显示「读取时间」如果超过 1 分钟用户应该重新获取。还有一层更隐蔽的问题某些站点存的是加密 token需要前端密钥解密后才真正可用这种情况下插件拿到的密文直接复制过去是发不了请求的需要和站点开发者确认 token 的存放格式。5.4 页面是 SPA 路由跳转Token 刚读到就丢现象在登录页复制 Token 正常但登录后页面跳到其他路由再点复制就空了。原因SPA 的单页应用切换路由不会刷新页面但很多框架会在路由变化时重新初始化状态sessionStorage里的值可能被清掉或者 token 被转移到内存对象里而不再持久化。解决监听页面路由变化变化后自动重新读取并更新缓存。在 content script 里用history.pushState的包装捕获路由变化或者轮询 storage 中的 token 值变化时记录时间戳。这个机制保证你任何时候点复制拿到的都是当前路由下最新的值避免拿到一份陈旧数据。5.5 导出 cURL 时引号和特殊符号被破坏现象复制出来的 header 内容粘贴到命令行里直接报错尤其当 token 值里带了特殊字符。原因Cookie 值或 Token 值里可能包含$、、\、空格等 shell 特殊字符。直接拼接成字符串输出拷贝到终端执行时会被错误解析。解决在做 cURL 格式输出时对每个值做转义处理并且整体用单引号包裹。这里提供一段处理逻辑function buildCurlHeader(cookies) { const headerParts cookies.map((c) { // 对单引号和反斜杠做转义其余特殊字符由 shell 处理 const name c.name.replace(/\\/g, \\\\).replace(//g, \\); const value c.value.replace(/\\/g, \\\\).replace(//g, \\); return ${name}${value}; }); return Cookie: ${headerParts.join(; )}; }转义规则按 Bash 的单引号语义处理单引号字符串里只有和\需要特殊处理其他特殊字符原样保留。如果你平时用的是 Windows 的 cmd 或 PowerShell转义规则又完全不同所以插件里我把 cURL 导出和纯文本导出分开cURL 格式默认按 Linux 环境处理Windows 用户选择纯文本格式。6. 把插件做成调试工具箱模板格式自定义与自动化验证技巧6.1 在 popup 里加入格式模板让输出适应不同工具做到最后这个插件的价值不只是「一键复制」而是变成你日常调试的伴随工具。我加了一个非常轻量的模板功能在 popup 里预留三个自定义格式槽位每个槽位允许用户填写一段模板字符串里面用占位符引用变量比如{cookie}、{token}、{domain}。复制时按照模板拼接输出适配不同工具各自要求的格式。// 模板渲染逻辑 function renderTemplate(template, data) { return template .replace(/\{cookie\}/g, data.cookieHeader) .replace(/\{token\}/g, data.token || ) .replace(/\{domain\}/g, data.domain); }这个机制看似简单实际很解决问题。比如有的内部系统要批量生成一组测试账号的 Cookie模板写成「账号:{username} Cookie:{cookie}」你只需要在页面上切换账号每次点复制出来的内容直接就是能入脚本的文本。我建议把模板存在chrome.storage.local里后续打开插件时还能记住不用每次都重新填。6.2 用「复制后校验」对抗静默失败读回剪贴板内容剪贴板的静默失败是最让人头疼的代码不抛错、界面显示成功但内容没进去。我在插件里加了一步强制校验——复制完成后延迟 100ms 再去读剪贴板对比看内容是否一致。async function copyTextAndVerify(text) { await copyText(text); await new Promise((r) setTimeout(r, 100)); const read await navigator.clipboard.readText?.().catch(() null); return read text; }如果校验失败界面上直接给红色警示而不是显示的默认成功提示。这个功能几乎是我日常排查问题的救星很多用户说「插件坏了」其实大多数是复制被浏览器静默拦截通过校验至少能让人知道问题出在哪一层而不是在调试工具里粘贴出空内容后才怀疑插件。注意这里需要clipboardRead权限如果不想加这个权限可以退化为提示「复制已触发请手动粘贴确认」把确认动作留给用户。6.3 验证插件的最后一道习惯造一个固定响应头的本地测试页插件写完之后别急着拿到真实站点上测试先做一个本地测试页来验证基本功能。用一个本地服务器返回固定的一组 Cookie 和一段写死在页面 storage 里的 token模拟目标环境然后逐一测试三种复制格式、跨域 iframe 场景、以及路由切换后是否还能正确读取。这个验证用例保证后续每次改动代码时都能快速回归不用依赖某个远程站点的可用性。我自己的习惯是给测试页放一个 100ms 延迟刷新 token 的接口专门用来验证 5.3 里提到的过期问题。遇到 token 复制出去但请求失败的情况第一反应先看插件显示的处理时间如果超过 3 秒大概率是 token 本身已经刷新了。这个细节在实际联调中帮我少走很多弯路希望也能帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑