|
上周帮朋友看一个前端项目,代码打开 main.js 我愣了几秒——满屏 _0x4a2f、混淆_0x3b8c 这种变量名,加密字符串全变成了十六进制转义,代码函数调用被包在一层又一层数组索引里。混淆朋友说这是加密外包团队交付的代码,他只想改个按钮颜色,代码结果找了半小时没找到按钮相关的混淆逻辑在哪。这就是加密代码混淆最直接的“效果”:让人读不懂。 但混淆和加密是代码两回事。加密需要密钥才能还原,混淆混淆不需要——浏览器能跑,加密人看不懂,代码靠的混淆是把可读的标识符替换成无意义字符、把字符串拆散重组、加密把控制流打平。像 javascript-obfuscator 这个库,默认配置下就会做变量名替换、字符串数组化、死代码注入。你写 function getUserInfo(),它给你变成 function _0x2e4a(_0x1b3c),所有字符串塞进一个数组,通过索引去取。 几种常见的混淆手段 字符串数组化是最普遍的。原始代码里的 "https://api.example.com" 会被抽到一个大数组里,变成 _0x5f2a[17]。更狠一点,数组还会做位移旋转,每次运行顺序不一样。控制流平坦化则是把 if/else 和 for 循环改写成 switch-case 加状态机,代码执行顺序看起来像一坨乱麻。还有一种叫“僵尸代码”,插入大量永远不会执行的分支,你手动删的时候一不小心就删错了。 我见过最极端的一个案例,某电商网站的促销活动页,整个逻辑被混淆成 3000 多行,实际有效代码不到 200 行。调试的时候 console.log 打出来的堆栈全是 _0x 开头,Chrome DevTools 的格式化按钮按下去也没用,因为变量名本身就失去了语义。 加密在 JS 里怎么玩 纯前端的加密意义有限,因为密钥必然要出现在客户端。常见做法是用 AES 加密一段核心逻辑的字符串,运行时解密再 eval 执行。比如把某个校验函数加密成 Base64 字符串,页面加载时用硬编码的密钥解密。这种方案能挡住直接看源码的人,但挡不住会断点调试的人——在 eval 那一行下个断点,解密后的代码就原形毕露了。 另一种思路是服务端加密、客户端解密执行。把关键逻辑放在服务端,通过接口返回加密后的 JS 片段,前端拿到后解密执行。这样每次返回的密文可以不同,逆向成本更高。但问题也很明显:网络请求暴露了接口,攻击者可以伪造请求拿到密文,再在本地慢慢解。 到底该不该用 我的判断是:前端混淆加密防的是“顺手改一改”的人,不是防“专门逆向”的人。如果你的目的是保护 API 密钥、防止别人直接复制你的 H5 小游戏逻辑,混淆有一定价值。但如果你的核心资产是算法模型、风控规则,放在前端本身就是错的,混淆只是给错误加了一层包装纸。 实际项目中,我倾向于用 terser 做基础压缩混淆就够了——变量名缩短、去掉注释和空格、压缩代码体积。真要上 javascript-obfuscator 的高强度配置,得先测性能。字符串数组化会显著增加内存占用,控制流平坦化会让 V8 引擎的优化失效,一个原本 50ms 执行完的函数可能变成 200ms。移动端低端机上,这个差距用户能感知到。 还有个坑:混淆后的代码报错信息完全不可读。线上出问题,Sentry 抓到的堆栈全是 _0x,你得保留 sourcemap 才能定位。但 sourcemap 一旦泄露,混淆等于白做。所以要么把 sourcemap 传到监控平台后删掉本地文件,要么接受线上排错靠猜。 代码混淆加密是个工具,不是保险箱。用之前先想清楚:你要防谁,愿意付出多少性能代价,线上出问题怎么排查。这三个问题答不上来,不如先把代码写清楚点。 |