出处:掘金

原作者:前端微白


为什么需要保护前端代码?

在开始技术细节前,让我们先看一组令人担忧的数据:

  • 75% 的现代网站存在敏感逻辑泄露在客户端代码中
  • API 密钥泄露是导致数据泄露的主要入口点(占所有泄露事件的 19%)
  • 代码窃取导致的年损失超过 400 亿美元
  • 一次成功的前端逆向工程平均耗时仅需 15 分钟
  • 由于前端页面会调用很多接口,有些接口会被别人爬虫分析,破解后获取数据

使用 CSS 和 JS 保护内容

例如,可以使用 CSS 让文本不可选中,或者使用 JS 定期检查 DOM 的改变,以防止调试

CSS 防止文本被选中:

body {
  -webkit-user-select: none; /* Chrome, Safari, Opera */
  -moz-user-select: none; /* Firefox */
  -ms-user-select: none; /* Internet Explorer/Edge */
  user-select: none; /* 非前缀版本,可以在支持的浏览器中使用 */
}

JS 定期检查 DOM 的改变:

const observer = new MutationObserver(mutations => {
  mutations.forEach(mutation => {
    console.log('Detected changes:', mutation);
    // 可以在这里添加逻辑来处理检测到的DOM变化
  });
});
 
observer.observe(document.body, {
  childList: true, // 监听子元素的变化
  attributes: true, // 监听属性的变化
  subtree: true // 监听所有后代元素的变化
});

绕过方法:禁用 JS 运行

禁用浏览器调试功能

// 开发者工具状态检测函数
const detectDevTools = () => {
  const threshold = 160; // 窗口大小变化的阈值
  let lastTime = Date.now();
  const interval = 1000
 
  setInterval(() => {
    const widthThreshold = window.outerWidth - window.innerWidth;
    const heightThreshold = window.outerHeight - window.innerHeight;
    
    if (widthThreshold > threshold || heightThreshold > threshold) {
      // 开发者工具可能在侧面或底部打开
      handleDevToolsOpen();
    }
    
    // 性能检测(开发者工具打开会降低性能)
    const currentTime = Date.now();
    if (currentTime - lastTime > interval + 100) {
      // 控制台打开时 Date.now() 调用会变慢
      handleDevToolsOpen();
    }
    lastTime = currentTime;
  }, interval);
};
 
// 检测到开发者工具时的处理
const handleDevToolsOpen = () => {
  // 1. 关闭当前窗口
  // window.close();
  
  // 2. 清空整个DOM
  // document.documentElement.innerHTML = '';
  
  // 3. 重定向到错误页面
  // window.location.replace('https://example.com/debugging-forbidden');
  
  // 4. 显示警告信息
  document.body.innerHTML = `
    <div class="anti-debug-warning">
      <h1>⛔ 安全警告</h1>
      <p>此页面禁止调试操作,请关闭开发者工具后刷新页面</p>
      <p>如您是本网站合法用户,请<a href="javascript:location.reload()">点击此处</a>重试</p>
    </div>
  `;
};
 
// 禁用调试快捷键
const disableShortcuts = (e) => {
  const forbiddenKeys = {
    'F12': true,
    'Ctrl+Shift+I': true, 
    'Ctrl+U': true,
    'Ctrl+S': true // 监听 Ctrl+S 是为了禁止保存至本地,避免被 Overrides
  };
  
  if (
    forbiddenKeys[e.key] || 
    (e.ctrlKey && e.shiftKey && e.key === 'I') || 
    (e.ctrlKey && e.key === 'u')
  ) {
    e.preventDefault();
    e.stopPropagation();
    return false;
  }
};
 
// 初始化检测
window.addEventListener('load', () => {
  detectDevTools();
  // 阻止键盘快捷键
  document.addEventListener('keydown', disableShortcuts);
  // 阻止鼠标右键
  document.addEventListener('contextmenu', e => e.preventDefault());
});

绕过方法:

  • 打开开发者工具后再打开网址,可跳过:阻止键盘快捷键、阻止鼠标右键
  • 将开发者工具以独立窗口形式打开,可跳过:窗口大小变化监测

使用更完善的库 disable-devtool

该库有以下特性:

  1. 支持可配置是否禁用右键菜单
  2. 禁用 F12、Ctrl+Shift+I 等快捷键
  3. 支持识别从浏览器菜单栏打开开发者工具并关闭当前页面
  4. 开发者可以绕过禁用(URL 参数使用 TK 配合 MD5 加密)
  5. 多种监测模式,支持几乎所有浏览器(IE、360、QQ 浏览器、FireFox、Chrome、Edge…)
  6. 高度可配置、使用极简、体积小巧
  7. 支持 npm 引用和 script 标签引用(属性配置)
  8. 识别真移动端与浏览器开发者工具设置插件伪造的移动端,为移动端节省性能
  9. 支持识别开发者工具关闭事件
  10. 支持可配置是否禁用选择、复制、剪切、粘贴功能
  11. 支持识别 eruda 和 vconsole 调试工具
  12. 支持挂起和恢复探测器工作
  13. 支持配置 ignore 属性,用以自定义控制是否启用探测器
  14. 支持配置 iframe 中所有父页面的开发者工具禁用

无限 debuger

基础版

一定要使用匿名函数。否则别人直接在控制台重定义函数就完了,比如:function startDebug() {};

(() => {
    function ban() {
      setInterval(() => {
        debugger;
      }, 50);
    }
    try {
      ban();
    } catch (err) {}
})();

绕过方法:可以通过控制台中的 Deactivate breakpoints 按钮或者使用快捷键 Ctrl + F8 关闭无限 debugger

进阶版

如果将 setInterval 中的代码写在一行,就能禁止用户断点,即使添加 logpoint 为 false 也无用。当然即使有些人想到用左下角的格式化代码,将其变成多行也是没用的

(() => {
  function ban() {
    setInterval(() => { debugger; }, 50);
  }
  try {
    ban();
  } catch (err) { }
})();

绕过方法:通过添加 add script ignore list 需要忽略执行代码行或文件

终极版

可以通过将 debugger 改写成 Function("debugger")(); 的形式来应对。Function 构造器生成的 debugger 会在每一次执行时开启一个临时 js 文件。当然使用的时候,为了更加的安全,最好使用加密后的脚本

(() => {
  function ban() {
    setInterval(() => {
      Function('debugger')();
    }, 50);
  }
  try {
    ban();
  } catch (err) { }
})();
 
// 更加的晦涩难懂
(() => {
  function block() {
    setInterval(() => {
      (function () {
        return false;
      }
      ['constructor']('debugger')
      ['call']());
    }, 50);
  }
  try {
    block();
  } catch (err) { }
})();

绕过方法:

  • 替换 js 文件
  • 修改 Function
Function.prototype.constructor=function(){}
Function = function () {}

压缩、混淆、加密代码

使用工具来混淆 JS 代码,可以帮助隐藏代码的真实意图,并使得代码阅读和修改变得更加困难

实际开发中使用专业工具:

  • Obfuscator.io
  • JavaScript Obfuscator
  • UglifyJS

在线工具:

JS 加密:U 加密

编码式 JS 代码加密,将 JS 代码分割、Unicode 编码化存储到数组中,使代码全部成为密文状态显示,执行时解密、重新编码并 eval 调用运行。加密后的代码中存在大量的”u”字符(Unicode 编码),所以命名为“U 加密”。 U 加密后代码可直接运行,使用方式与功能与加密前无异

在线使用:https://www.jshaman.com/tools/u-jiami.html

“U加密”采用独特的双重安全机制:

  1. 预处理阶段:将 JS 脚本分解为代码片段
  2. 加密阶段:对每个片段进行 Unicode 序列化
  3. 存储阶段:以密文数组形式保存
  4. 执行阶段:通过动态解码器实时还原其显著的”uXXXX”编码特征既确保了代码不可读性,又形成了独特的技术标识

加密示例:

// JS 源码
(function (){
    var domain = "jshaman.com";
    var from_year = 2017;
    var copyright = function(){
        return "(c)" + from_year + "-" + (new Date).getFullYear() + "," + domain;
    };
    var console_log = console.log;
    console_log(copyright())
})();
// 加密后的 JS 代码
var u=[`\u0028\u0066\u0075\u006e\u0063\u0074\u0069\u006f\u006e`,`\u0028\u0029\u007b`,`\u0076\u0061\u0072`,`\u0064\u006f\u006d\u0061\u0069\u006e`,`\u0022\u006a\u0073\u0068\u0061\u006d\u0061\u006e\u002e\u0063\u006f\u006d\u0022\u003b`,`\u0066\u0072\u006f\u006d\u005f\u0079\u0065\u0061\u0072`,`\u0032\u0030\u0031\u0037\u003b`,`\u0063\u006f\u0070\u0079\u0072\u0069\u0067\u0068\u0074`,`\u0066\u0075\u006e\u0063\u0074\u0069\u006f\u006e\u0028\u0029\u007b`,`\u0072\u0065\u0074\u0075\u0072\u006e`,`\u0022\u0028\u0063\u0029\u0022`,`\u0022\u002d\u0022`,`\u0028\u006e\u0065\u0077`,`\u0044\u0061\u0074\u0065\u0029\u002e\u0067\u0065\u0074\u0046\u0075\u006c\u006c\u0059\u0065\u0061\u0072\u0028\u0029`,`\u0022\u002c\u0022`,`\u0064\u006f\u006d\u0061\u0069\u006e\u003b`,`\u0063\u006f\u006e\u0073\u006f\u006c\u0065\u005f\u006c\u006f\u0067`,`\u0063\u006f\u006e\u0073\u006f\u006c\u0065\u002e\u006c\u006f\u0067\u003b`,`\u0063\u006f\u006e\u0073\u006f\u006c\u0065\u005f\u006c\u006f\u0067\u0028\u0063\u006f\u0070\u0079\u0072\u0069\u0067\u0068\u0074\u0028\u0029\u0029`,`\u007d\u0029\u0028\u0029\u003b`];var u2=[0,1,2,3,5,7,9,11,13,14,15,19,21,22,24,26,29,31,32,33];var u3=`u6[0] u6[1]u6[2] u6[3] = u6[5]u6[2] u6[7] = u6[9]u6[2] u6[11] = u6[13]u6[14] u6[15] + u6[7] + u6[19] + u6[21] u6[22] + u6[24] + u6[26]};u6[2] u6[29] = u6[31]u6[32]u6[33]`;for(u5=0; u5<u.length; u5++){u3 = u3.replace(new RegExp("u6\\["+u2[u5]+"\\]","g"), u[u5].replace("`","").replace("`",""));}eval(u3);

JS 加密:欧零加密

适用于需要轻量级保护的场景

“碎片化阵列加密” —— 一种基于词法拆解的 JS 混淆技术

在线使用:https://www.jshaman.com/tools/o0-jiami.html

核心原理:

  1. 词法解析:将源代码按语法单元(标识符/运算符/字面量等)拆解为碎片
  2. 密文存储:所有代码片段以乱序形式存入加密数组
  3. 动态重构:通过 eval 执行时按特定算法重组数组元素元素

技术优势:

  • 实现词法级混淆(比普通压缩更安全)
  • 保持精确还原性(无执行误差)
  • 对抗自动化调试(无法直接断点跟踪)

加密示例:

// JS 源码
(function (){
    var domain = "jshaman.com";
    var from_year = 2017;
    var copyright = function(){
        return "(c)" + from_year + "-" + (new Date).getFullYear() + "," + domain;
    };
    var console_log = console.log;
    console_log(copyright())
})();
// 加密后的 JS 代码
(function(){let e = eval;let x = "(_o_____o_2 (){_o__0__o_____o_0 _o_____o_3 = _0__0__o_____o_4._o_____o_5_0__0_;_o_____o_0 _o_____o_6__o_____o_7 = _o_____o_8;_o_____o_0 _o_____o_9 = _o_____o_2(){_o__0__o_____o_10 _0__0_(_o_____o_11)_0__0_ + _o_____o_6__o_____o_7 + _0__0_-_0__0_ + (_o_____o_12 _o_____o_13)._o_____o_14() + _0__0_,_0__0_ + _o_____o_3;};_o_____o_0 _o_____o_1__o_____o_15 = _o_____o_1._o_____o_15;_o_____o_1__o_____o_15(_o_____o_9())_o__0_})();";let z = "";[["_9__6_","\\"] , ["_o__0_","\n"] , ["_0__0_","\""] , ["_0___o_","'"]].map(function(z){ x = x.replace(RegExp(z[0], "g"), z[1]);});z = "var,console,function,domain,jshaman,com,from,year,2017,copyright,return,c,new,Date,getFullYear,log".split(",");let y = z.length - 1;while(y > -1){ x = x.replace(RegExp("_o_____o_" + y, "g"), z[y]); y--;}e(x);}())

JS 加密:32 进制加密

“进制编码加密” —— 一种基于数值进制转换的 JS 关键字混淆技术

在线使用:https://www.jshaman.com/tools/base32-encode.html

加密原理:

  1. 字符转换:将 JS 关键字(如 eval / alert)每个字母转换为 ASCII 码
  2. 进制编码:使用 (数字).toString(32) 将 ASCII 码转为 32 进制字符串符串
  3. 动态拼接:运行时重组这些进制编码字符串还原原始函数名

技术特点:

  • 关键字隐藏:核心 API 名称被替换为数值表达式
  • 抗格式化:无法通过代码美化还原原始名称
  • 可控强度:可调整进制基数(16/32/36 等)

加密示例:

// JS 源码
eval(alert(1));
// 加密后的 JS 代码(32 进制编码)
eval(
  (10).toString(32) + // a
  (21).toString(32) + // l
  (14).toString(32) + // e
  (27).toString(32) + // r
  (29).toString(32)   // t
  + "(1)"
);

HTML 加密

将 HTML 源码转化成 Unicode 编码形式,以此实现源码加密,而功能保持正常

在线使用:https://www.jshaman.com/tools/html-jiami.html

<html>
    <head><title>测试</title></head>
    <body>
        <h1>Html源码加密</h1>
        <script>
            alert("加密测试");
        </script>
    </body>
</html>

加密:

<script>
function decodeUnicodeEntities(encodedStr){
return encodedStr.replace(/&#x([0-9a-fA-F]+);/g, function(match, hexValue){
return String.fromCharCode(parseInt(hexValue, 16));
});
}
let encodedString = "<html> <head><title>测试</title></head> <body> <h1>Html源码加密</h1> <script> alert("加密测试"); </script> </body> </html> ";
let decodedString = decodeUnicodeEntities(encodedString);
document.write(decodedString);
</script>

JSON 加密

  • JSON 对像 Key 值标准化
  • 字符串 Unicode 化
  • 数值字面量转二元表达式
  • 布尔字面量转一元表达

JS 代码中的 JSON 对像,或单独外部 JSON 文件,都可进行加密。加密后的 JSON,可以直接使用,与加密前一样

{
    key1: [true, false, null],
    //comment
    "\u006B\u0065\u0079\u0032": {
        "key2Sub": [1, 1.2, 2, "3", 1e10, 1e-3]
    },
    "key3": false,
    "key4": "jshaman.com"
}

加密:

{"\u006B\u0065\u0079\u0031":[!![],![],null],"\u006B\u0065\u0079\u0032":{"\u006B\u0065\u0079\u0032\u0053\u0075\u0062":[456093^456092,1.2,950527^950525,"\u0033",1e10,1e-3]},"\u006B\u0065\u0079\u0033":![],"\u006B\u0065\u0079\u0034":"\u006A\u0073\u0068\u0061\u006D\u0061\u006E\u002E\u0063\u006F\u006D"}

其他

JS 语法标准化

将前端浏览器特有的语法进行标准化、统一化处理

例如:alertconsole 转化为 window.alertwindow.console

为什么要这样做? 

  • 统一规范化之后,源码更工整、易维护
  • 利于 JS 代码混淆加密
    • 例如 alert(1),如仅对此一句代码加密,由于它会被示例全局顶层函数,出于代码稳定性考虑,通常是无法进行加密的,它不利于混淆加密
    • 显示的将其写为 window.alert(1),如此它便成了 window 的成员函数调用方式,便可进行加密

转 Unicode

将字串转化为 Unicode 转义序列形式。可用来 eval 等语句结合,实现代码加密、反调试

例如 var key = 123 转化为 Unicode 形式为:

\u0076\u0061\u0072\u0020\u006b\u0065\u0079\u0020\u003d\u0020\u0031\u0032\u0033\u003b\u000a

eval 可以执行

转 Base64

将代码进行 Base64 编码后,用 evalatob 执行

new 表达式加密

“构造器隐写加密” —— 一种针对 new 操作符的轻量级混淆方案方案

技术实现:

// 原始代码
const now = new Date();
 
// 加密变体 A(数组索引式)
const now = [Date, Object, RegExp][0]();
 
// 加密变体 B(逗号表达式式)
const now = (RegExp, Object, Date)();

核心逻辑保护

WebAssembly

将关键算法移植到 WebAssembly,前端调用方式:

WebAssembly.instantiateStreaming(fetch('security.wasm'))
  .then(obj => {
    const protectLogic = obj.instance.exports.protectLogic;
    
    // 调用受保护的逻辑
    const result = protectLogic(123, 456);
    console.log('安全计算结果:', result);
  });

使用服务器端渲染

将网页的渲染过程放在服务器端,只返回最终渲染结果给客户端,隐藏源代码和逻辑

定制浏览器和客户端

对于极其敏感的业务场景,考虑开发定制浏览器或客户端应用作为替代方案,将关键代码从浏览器环境中解放出来

不可避免的局限性

  1. 完全防护是不可能的:客户端代码最终都在用户设备执行
  2. 性能成本:安全措施会增加资源消耗
  3. 误报风险:可能影响合法用户
  4. 对抗升级:存在专业反调试工具可绕过大多数保护

“前端安全的本质是增加攻击成本,而不是追求绝对防御” —— 安全专家