用 Proxy hook setInterval 拦截反调试 debugger

初版的问题 常见的 setInterval debugger 拦截写法有几个缺陷: (function(){ window._setInterval = window.setInterval; // 污染全局 window.setInterval = function (fn, delay, ...args) { let code = typeof fn === "function" ? fn.toString() : fn; if (code.includes("debugger")) { return null; // ❌ 返回 null 会让部分站点报错 } return _setInterval(fn, delay, ...args); // ❌ _setInterval 在闭包外查找可能丢失 }; })()问题汇总:fn.toString() 可被重写 getter 绕过 返回 null 时,站点调用 clearInterval(null) 或存 timer ID 会报错 _setInterval 挂到 window 上,污染全局命名空间 没有 hook clearInterval,假 ID 泄漏改进版:Proxy + 假 timer ID (() => { const rawSetInterval = window.setInterval; const rawClearInterval = window.clearInterval; const fakeTimers = new Set(); let fakeId = 1_000_000; function shouldBlock(fn) { try { let code = ""; if (typeof fn === "function") { code = Function.prototype.toString.call(fn); // 绕过 toString 重写 } else if (typeof fn === "string") { code = fn; } if (!code) return false; if (/\bdebugger\b/.test(code)) { console.warn("拦截到 debugger interval:", code); return true; } if (/while\s*\(\s*true\s*\)/.test(code)) { console.warn("拦截到死循环 interval:", code); return true; } } catch (e) { console.error("检测 interval 出错:", e); } return false; } window.setInterval = new Proxy(rawSetInterval, { apply(target, thisArg, args) { const [fn] = args; if (shouldBlock(fn)) { const id = fakeId++; fakeTimers.add(id); return id; // 返回真实格式的假 ID,不返回 null } return Reflect.apply(target, thisArg, args); } }); window.clearInterval = new Proxy(rawClearInterval, { apply(target, thisArg, args) { const [id] = args; if (fakeTimers.has(id)) { fakeTimers.delete(id); // 吞掉对假 ID 的 clearInterval 调用 return; } return Reflect.apply(target, thisArg, args); } }); console.log("setInterval hook 已启动"); })();关键设计点 为什么用 Function.prototype.toString.call(fn) 而不是 fn.toString() 站点可以重写 fn.toString 或者在函数对象上挂 getter: const fn = () => {}; fn.toString = () => "() => {}"; // 伪造源码而 Function.prototype.toString 直接从函数底层获取源码,无法被实例属性覆盖。 为什么返回假 timer ID 而不是 null 很多站点会存 timer ID 用于后续 clearInterval: const tid = setInterval(check, 100); // 之后 clearInterval(tid);如果 tid === null,clearInterval(null) 在部分引擎下会抛异常,或者日志里产生噪声。返回 1_000_000 以上的数字既像真实 ID,又不会和浏览器分配的 ID 冲突。 为什么同步 hook clearInterval 假 ID 进入 fakeTimers Set 之后,站点持有这个 ID 并在后续调用 clearInterval(fakeId)。如果不 hook,这个调用会传给原生 clearInterval,可能误清真实定时器(原生 ID 碰巧相同)或产生错误。 为什么用 Proxy 而不是直接赋值 Proxy 保留了 length、name 等函数元信息,instanceof 检测也更接近原生。部分站点会用 typeof setInterval === "function" 或特征检测来判断是否被 hook,Proxy 比直接赋值的闭包函数更难被识别。 可扩展检测项 如果需要更宽泛的反调试拦截,可以在 shouldBlock 里补充: // 检测 setTimeout debugger(同理 hook setTimeout) // 检测 constructor/eval 执行 debugger 字符串 if (/\bFunction\s*\(/.test(code)) return true; if (/\beval\s*\(/.test(code)) return true;现代站点已经较少用 setInterval(debugger) 这种低级方式,更多见的是混淆后的字节码或 WebAssembly 版本的反调试,这类情况 setInterval hook 就无法覆盖了。

JS 把 img 元素转 Base64:canvas + 跨域 tainted 问题

前端要把一张 <img> 转成 Base64——通常是为了上传、缓存、内联到别的地方。最直接的方式是 canvas。 基础版:canvas.toDataURL function imgToBase64(img, type = "image/png") { const canvas = document.createElement("canvas"); canvas.width = img.naturalWidth; canvas.height = img.naturalHeight; const ctx = canvas.getContext("2d"); ctx.drawImage(img, 0, 0); return canvas.toDataURL(type); }const img = document.querySelector("img"); img.onload = () => console.log(imgToBase64(img));关键点:用 naturalWidth/naturalHeight,不是 width/height(后者是显示尺寸,会被 CSS 缩放) 图片必须加载完成才能 draw,判 img.complete 或者监听 onload type 可选 image/png、image/jpeg、image/webp JPEG 可以传第二个参数控制画质:canvas.toDataURL("image/jpeg", 0.8)只有 URL 没有 img 元素 async function urlToBase64(url) { const img = new Image(); img.crossOrigin = "anonymous"; // 关键,让 canvas 不 tainted img.src = url; await new Promise((res, rej) => { img.onload = res; img.onerror = rej; }); const canvas = document.createElement("canvas"); canvas.width = img.naturalWidth; canvas.height = img.naturalHeight; canvas.getContext("2d").drawImage(img, 0, 0); return canvas.toDataURL("image/png"); }跨域的 tainted 坑 跨域图片经 canvas 一处理,toDataURL 直接抛: SecurityError: Tainted canvases may not be exported原因:canvas 一旦画了跨域资源就"被污染",浏览器不让你导出数据(防止读取用户其它站的私有图片)。 要出数据,需要满足:图片服务器返回 Access-Control-Allow-Origin: *(或匹配你的域) <img> 元素设置 crossOrigin="anonymous"如果对方服务器没配 CORS,只能:走自己的 Nginx 反代加 CORS 头 fetch + blob + FileReader 绕开 canvas更稳的方案:fetch + blob + FileReader async function urlToBase64(url) { const res = await fetch(url); const blob = await res.blob(); return await new Promise(resolve => { const reader = new FileReader(); reader.onloadend = () => resolve(reader.result); reader.readAsDataURL(blob); }); }返回的就是: data:image/png;base64,iVBORw0KGgoAAAANS...优势:不经过 canvas,没有 tainted 问题 支持任何图片格式,包括 GIF 动图(canvas 会把动图变成第一帧) 保持原始质量,不会重新压缩但 fetch 依然受 CORS 约束——服务器不配 CORS 时仍然会失败,只是不再看到"tainted"的错误,而是看到网络请求被拦。 想跳过 CORS 的最后一招 在自己的服务端加个代理: 浏览器 → your-server/proxy?url=xxx → 目标图片服务端拿到图片,加上 CORS 头再回给浏览器。前端就走同源了。 Base64 有多大 Base64 编码后体积变大约 33%。10 KB 的图变 13.3 KB 左右。做本地缓存 / 内联小图(favicon、图标)合适;大图别用 Base64,走 URL 更省。 一句话总结 canvas.toDataURL 一行搞定,跨域首选 fetch → blob → FileReader。用 canvas 就得先解决 CORS,别硬撞 tainted 报错。

JavaScript 将 img 元素导出为 Base64:canvas.toDataURL 与跨域处理

基础方法:canvas.toDataURL 将页面中已有的 <img> 元素转为 Base64: function imgToBase64(img, type = 'image/png') { const canvas = document.createElement('canvas'); canvas.width = img.naturalWidth; canvas.height = img.naturalHeight; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0); return canvas.toDataURL(type); // 'data:image/png;base64,...' }// 等待图片加载后调用 const img = document.querySelector('img'); img.onload = () => { const base64 = imgToBase64(img); console.log(base64); };// 图片已加载时直接调用 if (img.complete) { const base64 = imgToBase64(img); }naturalWidth/naturalHeight 是图片的原始尺寸,避免取到 CSS 设置的显示尺寸。 跨域图片:crossOrigin='anonymous' 图片来自不同域名时,需要设置 crossOrigin 才能在 canvas 中使用: async function urlToBase64(url) { const img = new Image(); img.crossOrigin = 'anonymous'; img.src = url; await new Promise((resolve, reject) => { img.onload = resolve; img.onerror = reject; }); const canvas = document.createElement('canvas'); canvas.width = img.naturalWidth; canvas.height = img.naturalHeight; canvas.getContext('2d').drawImage(img, 0, 0); return canvas.toDataURL('image/png'); }服务端必须在响应头中返回 Access-Control-Allow-Origin: *,否则 canvas 被标记为"污染(tainted)",调用 toDataURL() 会报错: SecurityError: Failed to execute 'toDataURL' on 'HTMLCanvasElement': Tainted canvases may not be exported.跨域无 CORS 头:fetch + FileReader 当目标服务器没有设置 CORS 头时,可以走服务端代理,或在同源页面中用 fetch 请求(如果存在跨域会被拦截)。另一种方案是在已有权限的上下文(如油猴脚本设置 @grant GM_xmlhttpRequest)中绕过同源限制: // 同源或有权限的上下文 async function urlToBase64ViaFetch(url) { const res = await fetch(url); const blob = await res.blob(); return new Promise((resolve) => { const reader = new FileReader(); reader.onloadend = () => resolve(reader.result); reader.readAsDataURL(blob); }); }指定输出格式和质量 canvas.toDataURL('image/png'); // PNG,无损 canvas.toDataURL('image/jpeg', 0.8); // JPEG,质量 80% canvas.toDataURL('image/webp', 0.9); // WebP(非所有浏览器支持)JPEG 不支持透明通道,带透明背景的图片转 JPEG 后透明部分会变黑,建议用 PNG。

tomcat:7.0-jre7 镜像不存在了?三条替代路

抄一份老教程写 Dockerfile: FROM tomcat:7.0-jre7Error response from daemon: manifest for tomcat:7.0-jre7 not found不是拼错了——Docker Hub 官方镜像库把老 tag 清掉了,尤其 Java 7 / Tomcat 7 / 旧 Debian 基础镜像这些 EOL 很久的组合。 现状 Docker Hub 上现在能拉到的 Tomcat 7: docker pull tomcat:7.0.109-jdk8 # 最后一个 7.x + JDK 8 docker pull tomcat:7.0.109-jdk8-openjdk docker pull tomcat:7.0.103 # 老一点的备选拉不到的:所有 -jre7 tag。Java 7 的 openjdk 基础镜像早就下架,官方 tomcat 镜像自然也不能再基于它构建。 方案一:换用 JDK 8(推荐) 绝大多数 Servlet 2.5 项目 JDK 8 兼容良好: FROM tomcat:7.0.109-jdk8COPY target/myapp.war /usr/local/tomcat/webapps/ROOT.warEXPOSE 8080 CMD ["catalina.sh", "run"]大多数老项目这样跑就好。只有极少数依赖 Java 7 特有行为的(比如 SSL cipher 顺序、某些反射细节),才需要坚持 Java 7。 方案二:自己构建 JRE 7 + Tomcat 7 镜像 必须坚持 Java 7 的情况,自己拼一个。Alpine 有历史包: FROM alpine:3.10# Java 7 需要 32-bit 兼容层 RUN apk add --no-cache openjdk7-jre wget tarENV TOMCAT_VERSION=7.0.109 ENV CATALINA_HOME=/usr/local/tomcat ENV PATH=$CATALINA_HOME/bin:$PATHRUN wget -q https://archive.apache.org/dist/tomcat/tomcat-7/v${TOMCAT_VERSION}/bin/apache-tomcat-${TOMCAT_VERSION}.tar.gz \ && tar -xzf apache-tomcat-${TOMCAT_VERSION}.tar.gz -C /usr/local \ && mv /usr/local/apache-tomcat-${TOMCAT_VERSION} $CATALINA_HOME \ && rm apache-tomcat-${TOMCAT_VERSION}.tar.gzEXPOSE 8080 CMD ["catalina.sh", "run"]Alpine 3.10 的 openjdk7-jre 包还能装。更新的 Alpine 都下架了 Java 7。 或者用 Eclipse Temurin 归档(Adoptium 前身): FROM eclipse-temurin:7-jre-focal但目前 Adoptium 也不再维护 7.x 镜像,能拉到什么看运气。 方案三:第三方社区镜像 Docker Hub 上有些几年没维护的社区镜像还挂着: docker pull consol/tomcat-7.0这个镜像里是 Tomcat 7 + OpenJDK 7。缺点:9 年没更新,安全性堪忧 只适合内网 / 本地测试 生产用必须盯着 CVE我的推荐路线 按业务需求排序:能升 JDK 8 就直接升——tomcat:7.0.109-jdk8,80% 情况没兼容问题 必须 Java 7 且是内网/开发环境——用 Alpine 自建 Dockerfile 老 DSpace / 学术软件之类死绑 Java 7 的项目——尝试 consol/tomcat-7.0 或者自己拼迁移信号 看到这些错误就是要升级 JDK 了: Unsupported major.minor version 52.0 # class 是 JDK 8 编译的,JRE 7 跑不了反过来: Unsupported major.minor version 51.0 # class 是 JDK 7 编译的,JRE 6 跑不了老 JAR 里 class 版本对应表:Java 版本 class versionJava 6 50Java 7 51Java 8 52Java 11 55Java 17 61Java 21 65一句话总结 tomcat:7.0-jre7 已经从官方镜像库消失。首选 tomcat:7.0.109-jdk8,非要 Java 7 就自己拼 Alpine 镜像,社区镜像只做兜底。

Servlet 规范与 Tomcat 版本对照表 + JDK 兼容性

维护老 SSM/SSH 项目常遇到——web.xml 里 version="2.5",跑起来应该配哪个 Tomcat?JDK 8 能跑吗?先给对照表: Servlet 与 Tomcat 版本对照Servlet 版本 Tomcat 版本 官方支持 JDKServlet 2.4 Tomcat 5.0/5.5 Java 1.4 / 5Servlet 2.5 Tomcat 6.x Java 5 / 6Servlet 3.0 Tomcat 7.x Java 6 / 7Servlet 3.1 Tomcat 8.0 Java 7 / 8Servlet 3.1 Tomcat 8.5 Java 7 / 8Servlet 4.0 Tomcat 9.x Java 8+Jakarta Servlet 5 Tomcat 10.x Java 8+Jakarta Servlet 6 Tomcat 11.x Java 17+Tomcat 10 是分水岭——包名从 javax.servlet.* 改到 jakarta.servlet.*。老代码原封不动放 Tomcat 10 直接编译不过。 判断项目该用哪个 打开 web.xml,看头部: <web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"> </web-app>version="2.5" 就是 Servlet 2.5,对应 Tomcat 6+。 JDK 8 能跑 Servlet 2.5 项目吗 能。稳定组合:项 推荐Servlet 规范 2.5(保持 web.xml 不变)Tomcat 7.0.109(最后一个 7.x)JDK 1.8Spring 3.x / 4.xMyBatis 3.x理由:Tomcat 7 向下兼容 Servlet 2.5(Tomcat 是超集) Tomcat 7 完整支持 JDK 8,无 PermGen 问题 Tomcat 6 对 JDK 8 支持不完整,从 6.0.53 才勉强能跑,还是别用了迁移 Tomcat 6 到 Tomcat 7 要改什么 九成情况什么都不用改。真要改的坑: 1. web.xml schema 版本:可以不动。Tomcat 7 依然接受 web-app version="2.5"。 2. JSP 的 EL 表达式:Tomcat 7 默认 EL 2.2、Tomcat 6 是 EL 2.1。有些字符串比较写法(比如 ${x eq null})在两版本略有差异,一般不影响。 3. 端口 / 编码:server.xml 里 Connector 改: <Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" URIEncoding="UTF-8" redirectPort="8443" />URIEncoding="UTF-8" 治所有 GET 参数中文乱码。 4. session:Tomcat 7 默认关闭了 sessionId URL 追加(;jsessionid=xxx)。老 JSP 依赖 <c:url> 处理会不同,一般不影响。 Maven 里跑 Tomcat:tomcat7-maven-plugin 不装 Tomcat 也能跑,用 Maven 插件: <build> <plugins> <plugin> <groupId>org.apache.tomcat.maven</groupId> <artifactId>tomcat7-maven-plugin</artifactId> <version>2.2</version> <configuration> <port>8080</port> <path>/</path> <uriEncoding>UTF-8</uriEncoding> </configuration> </plugin> </plugins> </build>跑: mvn tomcat7:run这个插件已经没维护 10 多年了,但对 Servlet 2.5 老项目照样能跑。生产别用,本地开发够。 Tomcat 8/9 呢 Servlet 2.5 项目理论上放 Tomcat 8/9 也能跑(javax.servlet 包名不变)。但会遇到:老 Spring 3.x 和 Tomcat 8 的 WebSocket 冲突 一些 EL 2.2 / 3.0 的表达式行为差异 JSP 编译器更严格迁移建议:如果项目还在开发新功能,直接升到 Tomcat 9 + JDK 8 + Servlet 3.x(web.xml 头改一下);只是维护,稳留 Tomcat 7 + JDK 8。 Tomcat 10+ 的 jakarta 大坑 老代码: import javax.servlet.http.HttpServletRequest;Tomcat 10 只认: import jakarta.servlet.http.HttpServletRequest;想迁移,用官方工具批量替换: # Eclipse Transformer java -jar org.eclipse.transformer.cli-*.jar \ --input old.war --output new.war或者手工找替换: find . -name "*.java" -exec sed -i 's/javax\.servlet/jakarta.servlet/g' {} +改完还要处理 Spring 版本——Spring 5.x 只支持 javax.servlet,要 jakarta 得升到 Spring 6.x(同时 JDK 17+)。这是个大工程,不是小改。 一句话总结 老 Servlet 2.5 项目最稳的组合是 Tomcat 7.0.109 + JDK 8。Tomcat 10 起改包名 jakarta,迁移是大工程。开发用 tomcat7-maven-plugin 快。