前端敏感信息防护实战技巧与避坑指南
我的写法,亲测靠谱
前端处理敏感信息这事儿,说白了就是别把不该暴露的东西扔到浏览器里。我之前做过一个后台管理系统,登录后用户的权限列表、个人信息全在前端存着,当时图省事直接挂在 window.userInfo 上,结果测试一跑安全扫描,直接挂红——人家用 DevTools 几下就改了权限,进了管理员页面。从那以后我就彻底清醒了:前端没有绝对安全的地方,但能做的防护一点都不能少。
我现在处理敏感数据的核心原则就一条:**绝不存储,能不传就不传,非得传就加密再传**。比如用户身份证号、手机号这种,页面上显示可以,但 JS 里拿到的是加密串,展示靠服务端返回脱敏后的值,或者前端用 placeholder 加映射机制动态渲染。
举个实际例子:我们有个订单页要显示用户手机号,但不能让爬虫或恶意用户批量抓取。我的做法是,接口返回的手机号字段本身就是星号脱敏过的,比如 138****1234,需要查看完整号码时,走一个独立的鉴权接口,带 token 和操作日志记录。关键代码如下:
// 获取脱敏数据
async function getOrders() {
const res = await fetch('/api/orders', {
headers: { Authorization: Bearer ${getToken()} }
});
return res.json();
}
// 触发查看敏感信息(需二次验证)
async function revealPhone(orderId) {
// 弹出验证码输入框或其他验证方式
const code = prompt('请输入验证码');
if (!code) return;
const res = await fetch(/api/order/${orderId}/phone, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: Bearer ${getToken()}
},
body: JSON.stringify({ verifyCode: code })
});
if (res.ok) {
const { phone } = await res.json();
// 更新视图,仅临时显示
document.getElementById(phone-${orderId}).textContent = phone;
} else {
alert('验证失败');
}
}
这个设计的好处是:敏感信息不在主列表接口中暴露,每次查看都有审计日志,而且前端不会长期持有明文。就算有人翻源码,也找不到“全局变量存手机号”的漏洞点。
这几种错误写法,别再踩坑了
我见过太多项目在这上面栽跟头,下面这几个反面案例,都是我在接手老项目时血泪总结出来的。
- 把 Token 存 localStorage,还明文挂在请求头里:这是最常见也最危险的操作。虽然方便调试,但 XSS 一打一个准。攻击者注入一段脚本,
localStorage.getItem('token')拿走,直接伪造请求。我之前维护的一个 H5 项目就是因为这个被刷了几千条虚假订单。 - 用注释标记敏感字段:比如写
// 注意!这里包含身份证信息,结果构建没去掉注释,生产环境源码里还能搜到。你以为是提醒队友,其实是提醒黑客从哪下手。 - 在前端做“假隐藏”:比如把敏感字段设为
display: none或加 class 隐藏,但 DOM 里照样存在。稍微懂点前端的人 F12 就能看到。更离谱的是有人用 JS 把值赋给不可见 input,以为这样安全,其实 Network 面板一眼就能看到响应体。 - 配置文件里写测试密钥:开发时为了快,直接在
config.js里写个测试 API Key,结果忘了删,git commit 提交上去了。GitHub 搜索一下jztheme.com/api+ “key”,一堆这样的公开仓库,真不是开玩笑。
还有一个我亲身经历的坑:有次我们用 Webpack DefinePlugin 注入环境变量,把某些调试开关打开,其中包含了 mock 数据路径和模拟用户 ID。上线后没关,导致外部人员通过翻源码找到了内部测试账号,直接登录体验系统搞了一波数据篡改。后来我们改成构建时根据 NODE_ENV 动态生成配置,敏感项全由 CI/CD 注入,本地只留默认空值。
实际项目中的坑
有些问题不到真实场景真发现不了。比如我们有个导出功能,前端拼 URL 带参数跳转,形如:
window.location.href = /export?userId=${userId}&type=full;
看起来没问题,但问题是这个链接会被记录在服务器 access log 里,而日志系统没做敏感字段过滤,运维查问题时随手一翻就看到了用户 ID。后来我们改成 POST 导出,用 Blob 下载,URL 不带参数,同时服务端对导出行为做权限校验和日志脱敏。
还有一次更尴尬:我们在 console.log 里打印了用户余额和积分详情,方便测试定位问题。结果构建忘记清掉 console,生产环境 sourcemap 又开着,别人反向还原代码一看,连计算逻辑都清楚。现在我们统一上了 babel 插件 babel-plugin-transform-remove-console,只保留 error 级别的输出。
另外提一句,很多人忽略 Source Map 的风险。你打包上传的 js.map 文件如果可访问,别人能还原你的原始代码结构,找到所有变量命名、API 路径、加密逻辑的位置。建议做法是在 CI 流程中把 source map 单独上传到监控平台(比如 Sentry),而不是放在公网可访问路径下。
再说个容易被忽视的点:**第三方 SDK**。我们集成过某个广告统计 SDK,文档说只收集设备信息,结果抓包发现它偷偷读了页面上的手机号元素(通过 class 名匹配)。后来我们对敏感区域加了 data-no-track 属性,并在全局 CSS 里设置 [data-no-track] { visibility: hidden; },既不影响展示,又能防采集。
加密不是万能的,但不用更不行
我知道有人会说:前端加密没意义,反正密钥也得暴露。这话没错,但不能因此就不做任何处理。至少可以增加攻击成本。
我现在对敏感字段的做法是:用 AES 在前端做一层临时加密,密钥由后端短期签发(类似 JWT 的思路),有效期几分钟。比如用户提交表单前,敏感字段先加密再发:
// 简化示例,实际应使用 Web Crypto API
async function encryptField(value, aesKey) {
const encoder = new TextEncoder();
const key = await crypto.subtle.importKey(
'raw',
encoder.encode(aesKey),
{ name: 'AES-GCM' },
false,
['encrypt']
);
const encrypted = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv: crypto.getRandomValues(new Uint8Array(12)) },
key,
encoder.encode(value)
);
return btoa(String.fromCharCode(...new Uint8Array(encrypted)));
}
// 使用
const encryptedId = await encryptField(idCardNumber, temporaryAesKey);
await fetch('/api/submit', {
method: 'POST',
body: JSON.stringify({ idCard: encryptedId })
});
虽然最终密钥还是可能被截获,但这至少防止了静态分析直接看到明文传输。而且结合 HTTPS + 请求频率限制,自动化爬虫很难批量破解。
最后的小技巧
几个实用的小建议,不一定完美,但有效:
- 敏感字段的变量名别叫
password、idCard这种直白的,混淆一下,比如叫field_x9k2,增加逆向难度。 - DOM 中不要用
data-属性存敏感值,比如data-user-id="123",网络爬虫专门扫这个。 - 考虑用
WeakMap临时存关联数据,避免全局对象污染,且能自动释放内存。 - 定期跑
npm audit和webpack-bundle-analyzer,看看有没有意外引入高危依赖或泄露信息。
以上是我踩坑后的总结,希望对你有帮助。这个领域没有一劳永逸的方案,只能不断加固。有更好的实现方式欢迎评论区交流。

暂无评论