前端项目打包时,密钥、接口令牌、内部域名这类敏感信息经常被顺手写进配置文件,最后原封不动地出现在浏览器端产物里。过去解决这个问题多半靠人工审查或者上线前跑一遍扫描脚本,既容易漏也难维护。Webpack 5 提供的 Data Masking 数据脱敏机制,把这件事放到了构建阶段处理:通过模块解析钩子识别命中规则的敏感内容,在生成产物之前完成遮蔽替换,让敏感字段不会进入最终代码。这篇文章从原理、配置实践和方案对比三个角度,把这套机制讲清楚。

Data Masking 的底层实现原理
Webpack 5 的模块处理流程中,NormalModuleFactory 负责把一个模块请求解析成真正的模块对象,随后进入 build 阶段读取源码、执行 loader 转换。Data Masking 的思路是挂在这一链条上:在模块源码生成之前,根据配置的匹配规则对内容做静态分析,命中规则的部分会被替换成脱敏后的占位文本,替换后的内容才进入后续的打包与压缩。
具体来说,规则由两部分组成:匹配器(match)和遮蔽策略(mask)。匹配器支持正则表达式或函数,用来定位诸如 API_KEY、access_token 这类字段;遮蔽策略则决定替换后的形态,常见的做法是保留前几位明文、其余用星号填充,或者干脆替换成固定占位符。这种设计的好处是脱敏逻辑对业务代码完全透明,开发者不需要在每个引用点手动处理。
需要注意,脱敏发生在模块内容层面,而不是字符串运行时层面。也就是说它处理的是源码文本,对于运行时才拼出来的敏感数据(比如用户输入后再组合的请求头),Data Masking 无能为力。这一点决定了它的适用边界:它防的是构建产物泄露,不是运行时动态数据的防护。
如何在 webpack.config.js 中配置脱敏规则
配置入口在 module.rules 中,通过自定义 loader 或使用社区提供的 masking loader 来挂载规则。下面是一个完整的示例,演示如何对 JSON 配置文件中的密钥字段做遮蔽:
// webpack.config.js
const path = require('path');
// 自定义脱敏 loader
function dataMaskingLoader(source) {
// 通过 query 传入规则,也可以直接写在 loader 内部
const options = this.getOptions() || {};
const rules = options.rules || [
{ match: /(["']?(?:api_?key|secret|token|password)["']?\s*[:=]\s*["'])([^"']{4,})(["'])/gi,
keepPrefix: 4 }
];
let output = source;
for (const rule of rules) {
output = output.replace(rule.match, (m, prefix, value, suffix) => {
const keep = value.slice(0, rule.keepPrefix || 0);
return prefix + keep + '****' + suffix;
});
}
return output;
}
module.exports = {
mode: 'production',
module: {
rules: [
{
test: /\.json$/,
// 只处理 config 目录下的文件,避免影响普通 json
include: path.resolve(__dirname, 'src/config'),
use: [
{ loader: path.resolve(__dirname, 'loaders/data-masking.js'),
options: { rules: [/* 自定义规则 */] } }
]
}
]
}
};
这段配置的核心在于 include 限定作用范围,以及 getOptions 读取规则参数。保留前四位明文是产品调试的常见折中:既能确认用的是哪个密钥,又不暴露完整值。如果业务允许,直接替换成固定占位符更安全。
另一个实用技巧是结合环境变量区分场景。开发环境下调试需要看到真实值,生产环境才执行脱敏,可以借助 process.env.NODE_ENV 判断:
const isProd = process.env.NODE_ENV === 'production';
module: {
rules: isProd ? [{
test: /\.json$/,
include: path.resolve(__dirname, 'src/config'),
use: ['./loaders/data-masking.js']
}] : []
}
这种按环境开关的方式实现成本最低,但要小心开发构建产物被误用的风险。更稳妥的做法是反过来:所有构建默认脱敏,只在显式传入 MASKING_OFF=1 时关闭,从流程上保证默认安全。
Data Masking 与 DefinePlugin、手工替换的方案对比
DefinePlugin 是另一个常被拿来处理配置注入的方案,它在编译期做纯文本替换,把 process.env.API_KEY 这类表达式直接换成字面量。乍看和 Data Masking 有点像,但目标完全不同:DefinePlugin 是注入数据,注入的值会完整进入产物;Data Masking 是遮蔽数据,目标是让值不完整地进入产物。两者其实是互补关系,常见组合是 DefinePlugin 注入脱敏后的值。
三种方案的差异可以直观对比:
| 方案 | 实现成本 | 防护效果 | 维护性 |
|---|---|---|---|
| 手工替换 | 低 | 依赖自觉,易漏 | 差 |
| DefinePlugin 注入 | 中 | 本身不脱敏 | 中 |
| Data Masking | 中 | 构建期强制遮蔽 | 好,规则集中管理 |
从表格能看出,Data Masking 的价值在于把防护变成构建流程的强制环节,不再依赖人工自觉。规则集中在配置文件里管理,新增敏感字段类型只需要加一条正则,团队协作时更容易做 code review。
常见踩坑点与注意事项
第一个坑是 source map 残留。脱敏只作用于模块源码,如果开启了 source map 并且把原始源码一并上传,敏感串可能通过 map 文件泄露出去。解决办法是对包含敏感配置的模块关闭 source map,或者在生成 map 之后再跑一遍脱敏处理。
第二个坑是持久化缓存。Webpack 5 默认启用文件系统缓存,脱敏规则的修改不一定会触发缓存失效,导致产物里出现新旧混杂的情况。给配置中 cache.version 加上脱敏规则的哈希值,可以强制规则变化时重建缓存:
const crypto = require('crypto');
const maskingRules = [/* ... */];
cache: {
type: 'filesystem',
version: crypto.createHash('md5')
.update(JSON.stringify(maskingRules))
.digest('hex')
}
第三个坑是正则规则写得过宽,误伤了业务字段。比如直接用 /token/gi 匹配会把所有包含 token 的键名都遮蔽,破坏代码逻辑。建议规则尽量精确到键名加值的组合模式,并在 CI 中加一条产物扫描作为兜底校验,双保险确保没有漏网之鱼。
总的来说,Data Masking 的定位是构建期静态脱敏,它解决不了所有数据安全问题,但成本可控、效果确定。把它和最小权限的环境变量管理、CI 产物扫描组合起来,前端敏感信息泄露的风险就能压到一个相当低的水平。
Webpack 5Data Masking数据脱敏修改时间:2026-09-06 10:06:37