从 0 到上线:我如何用开源制打造一款密码管理 Chrome 插件
当 Google 密码管理器对内部系统无能为力时,我决定自己造一个轮子
前言
作为一个每天要登录十几个不同系统的开发者,我一直依赖 Chrome 自带的密码管理功能。它很方便,能自动记住密码、跨设备同步,直到我遇到了公司内部系统。
那些运行在 10.xxx.xxx.xxx:8080 上的老旧管理后台、内部 Governance 平台、BPM 工作流引擎......Chrome 要么压根不弹出保存密码的提示,要么保存了也无法自动填充。更糟的是,这些系统的密码还必须每 90 天更换一次,手动输入成了每天的痛苦。
我问自己:能不能做一个完全本地化、能识别任何表单、还能跨设备安全同步的密码管理器?
于是,这个开源项目诞生了。
一、项目定位与核心功能
我需要的是一个不依赖任何云服务、数据完全掌握在自己手里、且能暴力识别所有登录表单的 Chrome 扩展。
最终成品包含以下核心能力:
| 功能 | 描述 |
|---|---|
| 🔐 主密码加密 | 所有密码使用用户主密码 AES 类加密(示例实现),离开主密码无法解密 |
| 🧠 智能表单识别 | 通过多种 CSS 选择器暴力匹配用户名/密码输入框,支持独立输入框 |
| ⚡ 一键填充 | 在当前网站列出所有匹配账号,支持多选填充 |
| 💾 本地存储 | 数据仅存在 chrome.storage.local,不上传任何服务器 |
| ⏱️ 定时备份 | 按小时/天/周自动导出加密数据到下载文件夹 |
| 🔍 密码库搜索 | 实时过滤网站/用户名/备注 |
技术栈:Chrome Extension Manifest V3 + 原生 JavaScript + Chrome Storage API + Alarms API + Downloads API。
二、痛点驱动:为什么 Google 记不住内部系统?
Chrome 的密码管理器依赖标准的 <form> 结构和特定的 input 属性(如 autocomplete="username")。但企业内部系统往往:
- 使用非标准表单(甚至没有
<form>包裹) - 字段命名随意(
logonid、employeeNo、j_username) - 采用框架动态生成的输入框(React/Vue 异步渲染)
我的插件必须能"强行"识别这些野路子表单。
解决方案:暴力选择器 + DOM 监听
在 content.js 中,我定义了一组"万能选择器":
javascript
const usernameSelectors = [
'input[type="text"]',
'input[type="email"]',
'input[name*="user"]',
'input[name*="email"]',
'input[placeholder*="用户名" i]',
'input[autocomplete="username"]',
// ... 共十几种
];
遍历页面所有 <form> 以及独立输入框,只要同时匹配到用户名字段和密码字段,就判定为登录表单。同时利用 MutationObserver 监听 DOM 变化,单页应用动态添加的表单也能实时捕获。
三、架构设计
3.1 扩展的四个主要部分
bash
password-manager-extension/
├── manifest.json # 扩展配置(权限、脚本注入)
├── popup.html/js # 弹出窗口 UI,用户主要交互界面
├── background.js # Service Worker,管理定时器、消息路由
├── content.js # 注入到网页的内容脚本,负责表单检测和填充
├── options.html/js # 设置页面(主密码、备份、自动开关)
3.2 数据流
- 保存密码 :用户在 popup 手动添加或通过 content 检测表单保存 → popup 调用
chrome.storage.local.set存储加密后的数据。 - 填充密码:用户点击"填充" → popup 向当前 tab 的 content script 发送消息(包含解密后的用户名/密码)→ content script 操作真实 DOM 输入值并触发事件。
- 自动备份 :background 中设定
chrome.alarms定时器 → 到期后读取存储数据 → 生成 JSON 文件 → 调用chrome.downloads.download保存到本地。
3.3 安全性考虑
- 主密码从不保存明文 :用户设置主密码时,我们存储的是
simpleHash(masterPassword),加密时用该哈希值作为密钥。 - 加密算法:示例中使用 Base64 + 反转字符串(生产环境务必替换为 Web Crypto API 的 AES-GCM)。
- 备份文件:仅包含加密后的密码数据,不包含主密码。即使文件泄露,没有主密码也无法解密。
四、关键实现细节
4.1 表单检测的核心算法
content.js 中的 detectLoginForms(returnDOMElements) 函数:
javascript
function detectLoginForms(returnDOMElements) {
const detectedForms = [];
const forms = document.querySelectorAll('form');
forms.forEach((form) => {
const usernameInputs = findUsernameInputs(form, returnDOMElements);
const passwordInputs = findPasswordInputs(form, returnDOMElements);
if (usernameInputs.length && passwordInputs.length) {
detectedForms.push({
website: document.title,
url: location.href,
usernameInputs,
passwordInputs,
});
}
});
// 若没有找到 form 内的,再查找独立输入框...
return detectedForms;
}
findUsernameInputs 内部遍历上述选择器数组,收集所有匹配的 DOM 元素(或序列化后的信息,取决于 returnDOMElements 标志)。
这个标志很关键:用于 UI 展示时,我们只需要字段元数据;用于实际填充时,我们必须拿到真实 DOM 元素才能赋值。
4.2 自动填充如何"骗"过现代前端框架?
很多 React/Vue 组件监听 input 或 change 事件,直接修改 input.value 不会触发框架更新。因此填充时需要手动 dispatch 事件:
javascript
usernameInput.value = credential.username;
usernameInput.dispatchEvent(new Event('focus', { bubbles: true }));
usernameInput.dispatchEvent(new Event('input', { bubbles: true }));
usernameInput.dispatchEvent(new Event('change', { bubbles: true }));
usernameInput.dispatchEvent(new Event('blur', { bubbles: true }));
这四种事件基本覆盖了绝大多数框架的响应机制。
4.3 定时备份的实现(background.js)
javascript
chrome.alarms.create('autoBackup', {
periodInMinutes: intervalMap[backupInterval],
delayInMinutes: 1
});
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === 'autoBackup') performBackup();
});
function performBackup() {
chrome.storage.local.get(['passwords'], (result) => {
const backupData = {
passwords: result.passwords,
exportedAt: new Date().toISOString(),
version: '1.0'
};
const blob = new Blob([JSON.stringify(backupData, null, 2)]);
const url = URL.createObjectURL(blob);
chrome.downloads.download({
url: url,
filename: `password_backup_${timestamp}.json`,
saveAs: false
});
});
}
注意:需要 "permissions": ["alarms", "downloads"]。
五、那些踩过的坑与解决方案
坑 1:内容脚本与弹出窗口通信时,dispatchEvent is not a function
原因 :在 detectForms 返回给 popup 时,我序列化了 DOM 元素为普通对象,导致 popup 中拿到的是 {name, type, id...} 而不是真实元素。然后 popup 直接对这些对象调用 .dispatchEvent,自然报错。
解决 :增加 returnDOMElements 参数。在填充流程中,content script 自己重新调用 detectLoginForms(true) 获取真实 DOM 元素,而不是依赖 popup 传过来的序列化数据。
坑 2:某些网站的表单是动态加载的(如单页应用)
解决 :使用 MutationObserver 监听 document.body 的子节点变化,当新增节点时重新执行一次表单检测。
坑 3:定时备份不触发
原因 :Service Worker 可能被浏览器挂起,chrome.alarms 虽能唤醒,但需确保在 chrome.runtime.onInstalled 中注册,并在每次设置变化时重新创建 alarm。
解决 :在 background.js 中监听 onInstalled 和来自 options 页面的 updateBackupSchedule 消息,每次先 clear 再 create。
坑 4:密码列表长 URL 导致界面溢出
解决 :CSS 中添加 word-break: break-all,同时为长文本添加 title 属性,悬停显示完整内容。
六、如何安装与使用?
开发者模式安装
- 下载源码文件夹(包含
manifest.json等)。 - GitHub 访问
https://github.com/qingjie-li/password-manager下载本地 。 - Google设置 → 管理扩展程序 → 开启"开发者模式" → 点击"加载已解压的扩展程序" → 选择文件夹。
快速上手流程
- 设置主密码:打开扩展弹出窗口 → 设置标签 → 输入主密码。
- 添加密码:访问任意登录页 → 自动识别标签 → 开始检测 → 保存检测到的凭据。
- 填充密码:再次访问该网站 → 当前网站标签 → 选择账号 → 点击填充。
七、开源与未来计划
项目完全开源,代码可在此仓库找到(链接略)。欢迎 PR。
后续计划:
- 使用 Web Crypto API 替换当前简易加密
- 支持通过 WebDAV/坚果云 跨设备同步加密数据(可选)
- 增加导入 Chrome 原生密码 CSV 的功能
八、总结
做这个插件的最大感受是:"痛点是最好的产品经理"。当 Google 密码管理器无法满足我那些"野路子"内部系统时,自己动手造一个反而更高效。
如果你也受困于公司内部系统的密码管理,不妨试试这个插件,或者基于这个思路自己定制。代码是开源的,欢迎一起完善。
本文首发于掘金,作者:一杯猫 如果觉得有用,请点个赞 ❤️,让更多被内部系统折磨的开发者看到。