分享一个完美的devtools检测代码

107 阅读7分钟

只是用于大家讨论怎么检测devtools

开启devtools的方法如下

  1. 按键 F12 CTRL+SHIFT+I
  2. 网页右键 触发右键菜单, 点击'检查'
  3. 浏览器空白页面提前打开devtools然后进行url跳转

代码如下,单个html页面即可完美复现

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>反调试功能测试页面</title>
  <style>
    body {
      font-family: Arial, sans-serif;
      max-width: 800px;
      margin: 0 auto;
      padding: 20px;
      line-height: 1.6;
    }
    .container {
      text-align: center;
      padding: 40px 20px;
    }
    .warning {
      color: #dc3545;
      font-weight: bold;
      margin: 20px 0;
    }
    .instructions {
      text-align: left;
      background-color: #f8f9fa;
      padding: 20px;
      border-radius: 5px;
      margin-top: 30px;
    }
    .btn {
      padding: 10px 20px;
      background-color: #007bff;
      color: white;
      border: none;
      border-radius: 5px;
      cursor: pointer;
      margin-top: 20px;
    }
    .btn:hover {
      background-color: #0056b3;
    }
  </style>
</head>
<body>
<div class="container">
  <h1>反调试功能测试页面</h1>
  <p class="warning">此页面包含反调试保护功能,尝试打开开发者工具将触发保护机制</p>
  <p>请尝试以下操作测试反调试效果:</p>
  <ul class="instructions">
    <li>按F12键尝试打开开发者工具</li>
    <li>使用快捷键Ctrl+Shift+I (Windows) 或 Cmd+Opt+I (Mac)</li>
    <li>右键点击页面尝试打开上下文菜单</li>
    <li>尝试选中并复制页面文本</li>
  </ul>
  <button class="btn" onclick="showMessage()">测试按钮</button>
  <p id="message"></p>
</div>

<script>
  /**
   * 反调试核心配置参数
   * @typedef {Object} AntiDebugConfig
   * @property {number} interval - 检测间隔时间(毫秒),默认500ms
   * @property {boolean} disableMenu - 是否禁用右键菜单(防止通过右键打开开发者工具)
   * @property {boolean} clearIntervalWhenDevOpenTrigger - 检测到调试行为后是否停止检测
   * @property {number[]|"all"} detectors - 启用的探测器类型数组,"all"表示启用所有探测器
   * @property {boolean} clearLog - 是否定期清除控制台日志(防止通过日志分析)
   * @property {boolean} disableSelect - 是否禁用文本选择功能
   * @property {boolean} disableCopy - 是否禁用复制功能
   * @property {boolean} disableCut - 是否禁用剪切功能
   * @property {boolean} disablePaste - 是否禁用粘贴功能
   * @property {string} rewriteHTML - 检测到调试行为时重写页面的HTML内容
   */

  /**
   * 反调试核心配置(仅保留与DevTools监测相关参数)
   * @type {AntiDebugConfig}
   */
  const config = {
    interval: 500,
    disableMenu: true,
    clearIntervalWhenDevOpenTrigger: false,
    detectors: [1, 3, 4, 5, 6, 7],
    clearLog: true,
    disableSelect: false,
    disableCopy: false,
    disableCut: false,
    disablePaste: false,
    rewriteHTML: "<h1>检测到调试行为!</h1><p>已阻止开发者工具使用。</p>"
  };

  /**
   * 浏览器环境信息
   * @typedef {Object} BrowserInfo
   * @property {boolean} pc - 是否为桌面端
   * @property {boolean} firefox - 是否为Firefox浏览器
   * @property {boolean} macos - 是否运行在macOS系统
   * @property {boolean} iosChrome - 是否为iOS版Chrome浏览器
   * @property {boolean} iosEdge - 是否为iOS版Edge浏览器
   * @property {boolean} chrome - 是否为Chrome或兼容内核浏览器
   * @property {boolean} mobile - 是否为移动端设备
   */

  /**
   * 检测浏览器环境信息(仅保留与调试监测相关的判断)
   * @returns {BrowserInfo} 浏览器环境信息对象
   */
  const browser = (() => {
    const ua = navigator.userAgent.toLowerCase();
    const isMobile = /(iphone|ipad|ipod|ios|android)/i.test(ua);
    return {
      pc: !isMobile,
      firefox: ua.includes("firefox"),
      macos: ua.includes("macintosh"),
      iosChrome: ua.includes("crios"),
      iosEdge: ua.includes("edgios"),
      chrome: ua.includes("chrome") || ua.includes("crios"),
      mobile: isMobile
    };
  })();

  /**
   * 探测器类型枚举
   * @enum {number}
   */
  const DetectorType = {
    Unknown: -1,
    RegToString: 0,
    DefineId: 1,
    Size: 2,
    DateToString: 3,
    FuncToString: 4,
    Debugger: 5,
    Performance: 6,
    DebugLib: 7
  };

  /**
   * 基础探测器类,所有具体探测器的父类
   */
  class Detector {
    /**
     * 创建探测器实例
     * @param {Object} options - 探测器配置
     * @param {DetectorType} options.type - 探测器类型
     * @param {boolean} [options.enabled=true] - 是否启用该探测器
     */
    constructor({ type, enabled = true }) {
      /** @type {DetectorType} 探测器类型 */
      this.type = type;
      /** @type {boolean} 是否启用探测器 */
      this.enabled = enabled;

      if (this.enabled) {
        this.init();
      }
    }

    /**
     * 检测到开发者工具打开时触发的回调函数
     */
    onDevToolOpen() {
      console.warn(`检测到DevTools打开 [类型: ${this.type}]`);
      handleDevToolOpen(this.type);
    }

    /**
     * 初始化探测器(子类可重写此方法)
     */
    init() {}

    /**
     * 执行检测逻辑(子类需重写此方法)
     */
    detect() {}
  }

  /**
   * DOM属性劫持探测器
   * 通过劫持DOM元素的属性访问器,当开发者工具检查元素时触发检测
   * @extends Detector
   */
  class DefineIdDetector extends Detector {
    /**
     * 创建DOM属性劫持探测器实例
     */
    constructor() {
      super({ type: DetectorType.DefineId });
      /** @type {HTMLDivElement|null} 用于检测的DOM元素 */
      this.div = null;
    }

    /**
     * 初始化探测器,创建隐藏的div并劫持其id属性
     */
    init() {
      this.div = document.createElement("div");
      const self = this;

      // 劫持div的id属性getter,当开发者工具访问该属性时触发检测
      Object.defineProperty(this.div, "id", {
        get: function() {
          self.onDevToolOpen();
          return "";
        }
      });
    }

    /**
     * 执行检测,通过console.log触发属性访问
     */
    detect() {
      console.log(this.div);
    }
  }

  /**
   * 窗口尺寸差探测器
   * 通过检测窗口内外尺寸差异判断开发者工具是否打开
   * @extends Detector
   */
  class SizeDetector extends Detector {
    /**
     * 创建窗口尺寸差探测器实例
     */
    constructor() {
      super({
        type: DetectorType.Size,
        enabled: !browser.edge // Edge浏览器中尺寸检测兼容性较差
      });
    }

    /**
     * 初始化探测器,绑定窗口大小变化事件
     */
    init() {
      this.checkWindowSizeUneven();
      window.addEventListener("resize", () => {
        setTimeout(() => this.checkWindowSizeUneven(), 100);
      });
    }

    /**
     * 检查窗口内外尺寸差异是否超过阈值
     * 开发者工具打开时通常会导致明显的尺寸差异
     */
    checkWindowSizeUneven() {
      const ratio = window.devicePixelRatio || 1;
      const widthDiff = window.outerWidth - window.innerWidth * ratio > 200;
      const heightDiff = window.outerHeight - window.innerHeight * ratio > 300;

      if (widthDiff || heightDiff) {
        this.onDevToolOpen();
      }
    }
  }

  /**
   * Date对象toString探测器
   * 通过监测Date对象toString方法的调用次数判断开发者工具是否打开
   * @extends Detector
   */
  class DateToStringDetector extends Detector {
    /**
     * 创建Date对象toString探测器实例
     */
    constructor() {
      super({
        type: DetectorType.DateToString,
        enabled: !browser.iosChrome && !browser.iosEdge
      });
      /** @type {number} 方法调用计数器 */
      this.count = 0;
      /** @type {Date|null} 用于检测的Date对象 */
      this.date = null;
    }

    /**
     * 初始化探测器,重写Date对象的toString方法
     */
    init() {
      this.date = new Date();
      const self = this;

      this.date.toString = function() {
        self.count++;
        return "";
      };
    }

    /**
     * 执行检测,通过console.log触发toString调用
     * 开发者工具打开时会导致多次调用
     */
    detect() {
      this.count = 0;
      console.log(this.date);
      if (this.count >= 2) {
        this.onDevToolOpen();
      }
    }
  }

  /**
   * 函数toString探测器
   * 通过监测函数toString方法的调用次数判断开发者工具是否打开
   * @extends Detector
   */
  class FuncToStringDetector extends Detector {
    /**
     * 创建函数toString探测器实例
     */
    constructor() {
      super({
        type: DetectorType.FuncToString,
        enabled: !browser.iosChrome && !browser.iosEdge
      });
      /** @type {number} 方法调用计数器 */
      this.count = 0;
      /** @type {Function|null} 用于检测的函数 */
      this.func = null;
    }

    /**
     * 初始化探测器,重写函数的toString方法
     */
    init() {
      this.func = function() {};
      const self = this;

      this.func.toString = function() {
        self.count++;
        return "";
      };
    }

    /**
     * 执行检测,通过console.log触发toString调用
     * 开发者工具打开时会导致多次调用
     */
    detect() {
      this.count = 0;
      console.log(this.func);
      if (this.count >= 2) {
        this.onDevToolOpen();
      }
    }
  }

  /**
   * Debugger语句探测器
   * 通过debugger语句的执行耗时判断开发者工具是否打开
   * @extends Detector
   */
  class DebuggerDetector extends Detector {
    /**
     * 创建Debugger语句探测器实例
     */
    constructor() {
      super({
        type: DetectorType.Debugger,
        enabled: browser.iosChrome || browser.iosEdge
      });
    }

    /**
     * 执行检测,通过debugger语句的耗时判断是否有调试行为
     * 开发者工具打开时会暂停执行,导致耗时增加
     */
    detect() {
      const start = performance.now();
      debugger;
      const end = performance.now();
      if (end - start > 100) {
        this.onDevToolOpen();
      }
    }
  }

  /**
   * 性能耗时探测器
   * 通过打印大型对象的性能差异判断开发者工具是否打开
   * @extends Detector
   */
  class PerformanceDetector extends Detector {
    /**
     * 创建性能耗时探测器实例
     */
    constructor() {
      super({
        type: DetectorType.Performance,
        enabled: browser.chrome || !browser.mobile
      });
      /** @type {number} 连续触发计数器 */
      this.count = 0;
      /** @type {Object[]|null} 用于性能测试的大型对象数组 */
      this.largeObjectArray = null;
    }

    /**
     * 初始化探测器,创建大型对象数组
     */
    init() {
      this.largeObjectArray = Array(50).fill().map(() => {
        const obj = {};
        for (let i = 0; i < 500; i++) {
          obj[i] = i.toString();
        }
        return obj;
      });
    }

    /**
     * 执行检测,比较不同打印方式的性能差异
     * 开发者工具打开时解析大型对象会导致显著的性能差异
     */
    detect() {
      /**
       * 测量函数执行时间
       * @param {Function} fn - 要执行的函数
       * @returns {number} 执行时间(毫秒)
       */
      const measureTime = (fn) => {
        const start = performance.now();
        fn();
        return performance.now() - start;
      };

      const normalTime = measureTime(() => console.log(this.largeObjectArray));
      const devTime = measureTime(() => console.table(this.largeObjectArray));

      if (devTime > 10 * normalTime) {
        this.count++;
        if (this.count >= 2) {
          this.onDevToolOpen();
        }
      }
    }
  }

  /**
   * 探测器类型与类的映射关系
   * @type {Object.<DetectorType, typeof Detector>}
   */
  const detectorMap = {
    [DetectorType.DefineId]: DefineIdDetector,
    [DetectorType.Size]: SizeDetector,
    [DetectorType.DateToString]: DateToStringDetector,
    [DetectorType.FuncToString]: FuncToStringDetector,
    [DetectorType.Debugger]: DebuggerDetector,
    [DetectorType.Performance]: PerformanceDetector
  };

  /**
   * 处理检测到开发者工具打开的情况
   * @param {DetectorType} type - 触发的探测器类型
   */
  function handleDevToolOpen(type) {
    if (config.clearIntervalWhenDevOpenTrigger) {
      clearInterval(intervalId);
    }

    if (config.rewriteHTML) {
      try {
        document.documentElement.innerHTML = config.rewriteHTML;
      } catch (e) {
        document.documentElement.innerText = config.rewriteHTML;
      }
    }
  }

  /**
   * 禁用可能打开开发者工具的入口点
   * 包括右键菜单、快捷键等
   */
  function disableDebugEntryPoints() {
    // 禁用右键菜单
    if (config.disableMenu) {
      document.addEventListener("contextmenu", (e) => {
        e.preventDefault();
        return false;
      });
    }

    // 禁用常用调试快捷键
    document.addEventListener("keydown", (e) => {
      const key = e.keyCode || e.which;

      if (key === 123 || // F12
        (e.ctrlKey && e.shiftKey && (key === 73 || key === 74)) || // Ctrl+Shift+I/J
        (browser.macos && e.metaKey && e.altKey && (key === 73 || key === 74))) { // Mac Cmd+Opt+I/J
        e.preventDefault();
        return false;
      }
    });

    // 禁用复制/剪切/粘贴
    const events = [];
    if (config.disableCopy) events.push("copy");
    if (config.disableCut) events.push("cut");
    if (config.disablePaste) events.push("paste");

    events.forEach(event => {
      document.addEventListener(event, (e) => {
        e.preventDefault();
        return false;
      });
    });

    // 禁用文本选择
    if (config.disableSelect) {
      document.addEventListener("selectstart", (e) => {
        e.preventDefault();
        return false;
      });
    }
  }

  /** @type {number|null} 检测循环的定时器ID */
  let intervalId = null;

  /**
   * 初始化反调试功能
   * @param {Partial<AntiDebugConfig>} [options={}] - 自定义配置选项
   */
  function initAntiDebug(options = {}) {
    // 合并配置
    Object.assign(config, options);

    // 初始化探测器
    const detectors = [];
    const detectorTypes = config.detectors === "all"
      ? Object.values(DetectorType).filter(v => typeof v === "number" && v >= 0)
      : config.detectors;

    detectorTypes.forEach(type => {
      const DetectorClass = detectorMap[type];
      if (DetectorClass) {
        detectors.push(new DetectorClass());
      }
    });

    // 禁用调试入口
    disableDebugEntryPoints();

    // 启动检测循环
    intervalId = setInterval(() => {
      detectors.forEach(detector => {
        if (detector.enabled) {
          try {
            detector.detect();
          } catch (e) {}
        }
      });
      if (config.clearLog) {
        console.clear();
      }
    }, config.interval);
  }

  /**
   * 页面测试按钮点击事件处理函数
   */
  function showMessage() {
    document.getElementById("message").textContent = "按钮点击成功!当前时间: " + new Date().toLocaleTimeString();
  }

  // 初始化反调试保护
  initAntiDebug();
</script>
</body>
</html>