Java验证码双形态实战:点击文字与拖动滑块选型及源码解析
简介本资源面向Java后端与全栈开发者提供一套可直接用于生产环境的用户行为验证码方案涵盖点击中文文字图片验证码与拖动/滑动图片验证码两种形态适合需要为登录、注册等场景增加人机校验能力的项目参考。压缩包共178个文件约8.92MB以68个java源码为核心辅以png、jpg、gif等图片素材、xml与yml配置、css与js前端资源及html页面结构完整、便于二次开发。资源基于JDK1.8、Maven3.3、Spring Boot 2.1.17与Redis构建核心技术涉及AWT的BufferedImage、Graphics2D、Font实现随机中文文字生成、随机抠图与拼图前端结合点击坐标、图片位置与滚动位置计算相对坐标并通过AES或DES加密传输坐标信息。目前已有3078人学习下载读者可获得源码、demo、单元测试与实现思路快速理解验证码生成、校验与前后端交互的完整链路。1. Java 验证码双形态落地点击文字与拖动滑块到底该怎么选做登录、注册、发短信接口验证码是绕不开的一环。图形字符验证码早就被 OCR 和打码平台打穿很多团队开始换成行为式验证码点击文字验证码和拖动/滑动图片验证码。前者给出一张图要求按提示顺序点中若干汉字或图标后者给出一张带缺口的背景图要求把滑块拖到缺口位置。两者都能在 Java 后端生成、校验前端配合交互且天然适合写单元测试来验证边界。这篇面向的是要在自己项目里落地这两种验证码的 Java 开发者尤其是需要源码级理解、能跑 demo、能补单元测试的人。我会把生成、存储、校验、防重放、参数调优、常见翻车点全部拆开讲代码可以直接抄进 Spring Boot 工程。读完你能判断点击文字和拖动滑块各自适合什么场景以及怎么用一套 Java 代码同时支撑两种形态。2. 点击文字验证码从字体绘制到坐标校验的完整链路点击文字验证码的核心不是“画几个字”而是“生成随机目标 记录正确点击顺序 校验坐标是否落在文字包围盒内”。很多 demo 只画字不校验坐标上线就被脚本刷穿。下面按可复现的顺序拆。2.1 生成画布与随机文字字体、旋转、干扰线的参数怎么定我一般用 Java2D 在 BufferedImage 上绘制。画布建议 300×180文字 4 个字号 2834旋转 -30°30°。旋转角度太大真人点不准太小OCR 容易切分。干扰线 35 条颜色与文字色差控制在 60100 之间太接近会让人眼也看不清。// 生成点击文字验证码底图返回图片和文字坐标 public CaptchaImage generateClickCaptcha() { int width 300, height 180; BufferedImage image new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); Graphics2D g image.createGraphics(); g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g.setColor(new Color(245, 245, 245)); g.fillRect(0, 0, width, height); String[] pool {春, 夏, 秋, 冬, 山, 水, 风, 云}; ListString targets new ArrayList(); Random random new Random(); // 随机选 4 个不重复文字 while (targets.size() 4) { String c pool[random.nextInt(pool.length)]; if (!targets.contains(c)) targets.add(c); } ListTextPoint points new ArrayList(); for (int i 0; i targets.size(); i) { String text targets.get(i); int fontSize 28 random.nextInt(7); g.setFont(new Font(SansSerif, Font.BOLD, fontSize)); g.setColor(new Color(30 random.nextInt(80), 30 random.nextInt(80), 30 random.nextInt(80))); int x 30 i * 65 random.nextInt(15); int y 60 random.nextInt(60); double angle Math.toRadians(-30 random.nextInt(61)); g.rotate(angle, x, y); g.drawString(text, x, y); g.rotate(-angle, x, y); // 记录包围盒用于后续坐标校验 FontMetrics fm g.getFontMetrics(); int w fm.stringWidth(text); int h fm.getHeight(); points.add(new TextPoint(text, x, y - h 8, w, h)); } // 干扰线 for (int i 0; i 4; i) { g.setColor(new Color(150 random.nextInt(80), 150 random.nextInt(80), 150 random.nextInt(80))); g.drawLine(random.nextInt(width), random.nextInt(height), random.nextInt(width), random.nextInt(height)); } g.dispose(); return new CaptchaImage(image, targets, points); }逻辑说明targets是正确点击顺序points记录每个字的包围盒。参数上x间距 65 保证文字不重叠y在 60120 之间浮动避免规律性。旋转后包围盒会略偏校验时给 812 像素容差。干扰线数量别超过 6否则真人误点率明显上升。2.2 坐标校验为什么不能只比对文字顺序只比对文字顺序脚本可以随机点四个位置碰运气。正确做法是前端把点击坐标按顺序传给后端后端判断每个坐标是否落在对应文字的包围盒内含容差。四个字全中才算通过。容差建议 10 像素太小真人点不中太大脚本容易蒙。// 校验点击坐标是否命中目标文字包围盒 public boolean verifyClick(ListPoint clicks, ListTextPoint targets, int tolerance) { if (clicks null || clicks.size() ! targets.size()) return false; for (int i 0; i targets.size(); i) { TextPoint tp targets.get(i); Point p clicks.get(i); boolean inX p.x tp.x - tolerance p.x tp.x tp.width tolerance; boolean inY p.y tp.y - tolerance p.y tp.y tp.height tolerance; if (!inX || !inY) return false; } return true; }参数说明tolerance我一般设 10移动端可放宽到 14。clicks的顺序必须和targets一致前端要按用户点击先后传。注意别把坐标存成相对百分比又忘了换算这是最常见的翻车点之一。2.3 用单元测试锁住边界四个必写的用例单元测试不是走形式。点击文字验证码至少覆盖坐标刚好在边界、容差外 1 像素、点击数量不对、顺序颠倒。用 JUnit 5 写不依赖 Spring 容器。Test void verifyClick_boundaryAndOrder() { ListTextPoint targets List.of( new TextPoint(春, 30, 60, 30, 34), new TextPoint(夏, 95, 70, 30, 34) ); // 刚好在边界内 assertTrue(verifier.verifyClick(List.of(new Point(30, 60), new Point(95, 70)), targets, 10)); // 容差外 1 像素 assertFalse(verifier.verifyClick(List.of(new Point(19, 60), new Point(95, 70)), targets, 10)); // 数量不对 assertFalse(verifier.verifyClick(List.of(new Point(30, 60)), targets, 10)); // 顺序颠倒 assertFalse(verifier.verifyClick(List.of(new Point(95, 70), new Point(30, 60)), targets, 10)); }这四个用例能挡住大部分回归。写测试时把tolerance作为参数传入别写死在方法里否则边界用例没法调。3. 拖动/滑动图片验证码缺口生成、轨迹校验与防脚本拖动滑块验证码的难点不在画图而在“怎么判断这是人拖的”。只校验最终位置脚本直接设置坐标就过了。要结合轨迹、耗时、点击次数一起判断。3.1 缺口位置随机化与阴影绘制别让缺口固定在右边缺口 x 坐标建议在画布宽度的 15%85% 之间随机y 坐标在 10%80% 之间随机。缺口形状用圆形或拼图形半径 1824。阴影用半透明黑色偏移 2 像素方便人眼识别。背景图可以用本地图片池避免每次请求外部图源。public SliderCaptcha generateSliderCaptcha(BufferedImage bg) { int width bg.getWidth(), height bg.getHeight(); int r 20; Random random new Random(); int gapX (int) (width * 0.15) random.nextInt((int) (width * 0.7)); int gapY (int) (height * 0.1) random.nextInt((int) (height * 0.7)); BufferedImage canvas new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); Graphics2D g canvas.createGraphics(); g.drawImage(bg, 0, 0, null); // 绘制缺口阴影 g.setColor(new Color(0, 0, 0, 90)); g.fillOval(gapX 2, gapY 2, r * 2, r * 2); // 挖出缺口用背景色近似填充 g.setColor(new Color(230, 230, 230)); g.fillOval(gapX, gapY, r * 2, r * 2); g.dispose(); return new SliderCaptcha(canvas, gapX, gapY, r); }参数说明gapX范围保证缺口不贴边r取 20 时拖动容差可设 812 像素。阴影透明度 90 左右比较自然。注意背景图尺寸要统一否则前端滑块轨道长度对不上。3.2 轨迹校验耗时、步数、加速度三个阈值后端收到前端上报的轨迹点列表后先算总耗时、步数、最大加速度。真人拖动耗时通常 6003000ms步数 1580加速度有正有负。脚本往往耗时极短、步数极少、匀速直线。public boolean verifyTrack(ListTrackPoint track, int targetX, int tolerance) { if (track null || track.size() 10) return false; long duration track.get(track.size() - 1).t - track.get(0).t; if (duration 500 || duration 5000) return false; int finalX track.get(track.size() - 1).x; if (Math.abs(finalX - targetX) tolerance) return false; // 检查是否有负向移动真人会回拉 boolean hasBack false; for (int i 1; i track.size(); i) { if (track.get(i).x track.get(i - 1).x) { hasBack true; break; } } return hasBack; }参数说明tolerance设 10duration下限 500ms 能挡掉大部分瞬移脚本。hasBack要求至少有一次回拉真人几乎都会有。注意别把阈值卡太死否则触屏设备容易误杀。3.3 单元测试轨迹校验的四个反例轨迹校验的单元测试重点在反例空轨迹、步数不足、耗时过短、无回拉。Test void verifyTrack_rejectScriptLike() { // 空轨迹 assertFalse(verifier.verifyTrack(null, 200, 10)); // 步数不足 assertFalse(verifier.verifyTrack(List.of(new TrackPoint(0, 0, 0)), 200, 10)); // 耗时过短 ListTrackPoint fast new ArrayList(); for (int i 0; i 20; i) fast.add(new TrackPoint(i * 10, 0, i * 10)); assertFalse(verifier.verifyTrack(fast, 200, 10)); // 无回拉 ListTrackPoint straight new ArrayList(); for (int i 0; i 20; i) straight.add(new TrackPoint(i * 10, 0, i * 50)); assertFalse(verifier.verifyTrack(straight, 200, 10)); }这些用例能保证阈值调整时不会把校验逻辑改坏。测试数据里的时间戳单位要统一我一般用毫秒。4. 避坑与排查验证码上线后最容易翻车的五件事4.1 现象本地 demo 正常上线后点击总是失败原因前端传的坐标是相对图片的 CSS 像素后端按原图像素校验图片被缩放后坐标对不上。解决前端传坐标前乘以原图宽 / 显示宽或者后端统一按比例换算。我一般让前端传原图坐标后端不做缩放假设。4.2 现象滑块验证码在部分手机上一拖就过原因触屏事件和鼠标事件混用touchend没阻止默认行为导致轨迹点重复或缺失。解决统一用 Pointer Events或在 touch 事件里preventDefault。轨迹点去重相邻点距离小于 2 像素的合并。4.3 现象验证码图片在 Redis 里存了但校验时取不到原因key 拼错、过期时间太短、序列化方式不一致。解决key 用captcha:click:{uuid}这种固定前缀过期时间设 120 秒存 JSON 字符串而不是 Java 对象。单元测试里用嵌入式 Redis 或 mock 掉存储层。4.4 现象单元测试在本地过CI 上随机失败原因验证码生成用了Random且测试依赖具体坐标。解决把随机源抽成接口测试时注入固定种子或固定实现。坐标断言用范围而不是精确值。4.5 现象点击文字验证码被 OCR 识别原因文字太清晰、无旋转、无干扰。解决加旋转、加干扰线、文字颜色和背景对比度降低到 4:1 左右。更彻底的做法是点击文字和滑块二选一随机下发增加脚本适配成本。5. 进阶技巧一套接口同时支撑两种验证码并做灰度落地时不必写两套接口。我一般定义一个CaptchaService内部根据配置返回CLICK或SLIDER类型前端按类型渲染不同组件。灰度阶段按用户 ID 哈希分流比如 10% 走滑块90% 走点击观察通过率和投诉率再调整比例。验证方法上除了单元测试我会加一个定时任务用脚本模拟点击和拖动统计被拦截率。如果拦截率低于 95%说明阈值太松高于 99.9%可能误杀真人。这个比例因业务而异登录场景可以严一点注册场景松一点。public CaptchaResult generate(String userId) { // 按用户哈希灰度10% 滑块 boolean useSlider Math.abs(userId.hashCode()) % 100 10; if (useSlider) { return sliderService.generate(userId); } return clickService.generate(userId); }参数说明灰度比例写在配置中心别硬编码。userId为空时默认走点击。生成结果里带type字段前端据此渲染。血泪经验验证码的存储和校验一定要放在同一层别一个存 Redis 一个查数据库。我见过因为 Redis 主从延迟导致校验失败的案例后来统一走主节点读。单元测试里把存储层 mock 掉能省很多环境问题。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →