文章目录
- 前言
- 一、后端springboot
- 1.1 yml 配置
- 1.2 跨域配置
- 1.3 资源配置
- 1.4 Controller
- 二、前端jquery
- 2.1 通用代码
- 2.2 滑动验证码
- 2.3 旋转验证码
- 2.4 滑动还原验证码
- 2.5 文字点选验证码
- 三、源码探秘和总结
- 3.1 前端代码
- 3.2 后端代码
- 3.3 总结
- 参考
前言
最近发现一个有趣的 Java 验证码库,能够生成各种行为验证码:滑块、旋转、点选验证码。
github: https://github.com/tianaiyouqing/tianai-captcha
pom 依赖:
<!-- springboot -->
<dependency>
<groupId>cloud.tianai.captcha</groupId>
<artifactId>tianai-captcha-springboot-starter</artifactId>
<version>1.3.3</version>
</dependency>
<!-- 非springboot -->
<dependency>
<groupId>cloud.tianai.captcha</groupId>
<artifactId>tianai-captcha</artifactId>
<version>1.3.3</version>
</dependency>
注:以下代码大部分来自官方demo: https://gitee.com/tianai/tianai-captcha-demo
一、后端springboot
1.1 yml 配置
captcha:
# 如果项目中使用到了redis,滑块验证码会自动把验证码数据存到redis中, 这里配置redis的key的前缀,默认是captcha:slider
prefix: captcha
# 验证码过期时间,默认是2分钟,单位毫秒, 可以根据自身业务进行调整
expire:
# 默认缓存时间 2分钟
default: 10000
# 针对 点选验证码 过期时间设置为 2分钟, 因为点选验证码验证比较慢,把过期时间调整大一些
WORD_IMAGE_CLICK: 20000
# 使用加载系统自带的资源, 默认是 false
init-default-resource: false
cache:
# 缓存控制, 默认为false不开启
enabled: true
# 验证码会提前缓存一些生成好的验证数据, 默认是20
cacheSize: 20
# 缓存拉取失败后等待时间 默认是 5秒钟
wait-time: 5000
# 缓存检查间隔 默认是2秒钟
period: 2000
secondary:
# 二次验证, 默认false 不开启
enabled: false
# 二次验证过期时间, 默认 2分钟
expire: 120000
# 二次验证缓存key前缀,默认是 captcha:secondary
keyPrefix: "captcha:secondary"
1.2 跨域配置
跨域问题
@Configuration
public class CorsConfig {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
// 重写父类提供的跨域请求处理的接口
@Override
public void addCorsMappings(CorsRegistry registry) {
// 添加映射路径
registry.addMapping("/**")
.allowedOrigins("*") // 放行哪些域名,可以多个
.allowCredentials(true) // 是否发送Cookie信息
.allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS", "PATCH") // 放行哪些请求方式
.allowedHeaders("*") // 放行哪些原始域(头部信息)
.exposedHeaders("Header1", "Header2") // 暴露哪些头部信息(因为跨域访问默认不能获取全部头部信息)
.maxAge(3600); // 预请求的结果有效期,默认1800分钟,3600是一小时
}
};
}
}
1.3 资源配置
@Component
public class MyResourceStore extends DefaultResourceStore {
public MyResourceStore() {
// 滑块验证码 模板 (系统内置)
Map<String, Resource> template1 = new HashMap<>(4);
template1.put(SliderCaptchaConstant.TEMPLATE_ACTIVE_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME, DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/1/active.png")));
template1.put(SliderCaptchaConstant.TEMPLATE_FIXED_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME, DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/1/fixed.png")));
template1.put(SliderCaptchaConstant.TEMPLATE_MATRIX_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME, DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/1/matrix.png")));
Map<String, Resource> template2 = new HashMap<>(4);
template2.put(SliderCaptchaConstant.TEMPLATE_ACTIVE_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME, DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/2/active.png")));
template2.put(SliderCaptchaConstant.TEMPLATE_FIXED_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME, DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/2/fixed.png")));
template2.put(SliderCaptchaConstant.TEMPLATE_MATRIX_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME, DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/2/matrix.png")));
// 旋转验证码 模板 (系统内置)
Map<String, Resource> template3 = new HashMap<>(4);
template3.put(SliderCaptchaConstant.TEMPLATE_ACTIVE_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME, StandardSliderImageCaptchaGenerator.DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/3/active.png")));
template3.put(SliderCaptchaConstant.TEMPLATE_FIXED_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME, StandardSliderImageCaptchaGenerator.DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/3/fixed.png")));
template3.put(SliderCaptchaConstant.TEMPLATE_MATRIX_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME, StandardSliderImageCaptchaGenerator.DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/3/matrix.png")));
// 1. 添加一些模板
addTemplate(CaptchaTypeConstant.SLIDER, template1);
addTemplate(CaptchaTypeConstant.SLIDER, template2);
addTemplate(CaptchaTypeConstant.ROTATE, template3);
// 2. 添加自定义背景图片
addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/a.jpg"));
addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/b.jpg"));
addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/c.jpg"));
addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/d.jpg"));
addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/e.jpg"));
addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/g.jpg"));
addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/h.jpg"));
addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/i.jpg"));
addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/j.jpg"));
addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/01.jpg"));
addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/02.jpg"));
addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/03.jpg"));
addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/04.jpg"));
addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/05.jpg"));
addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/06.jpg"));
addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/07.jpg"));
addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/08.jpg"));
addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/09.jpg"));
addResource(CaptchaTypeConstant.ROTATE, new Resource("classpath", "bgimages/10.jpg"));
addResource(CaptchaTypeConstant.ROTATE, new Resource("classpath", "bgimages/48.jpg"));
addResource(CaptchaTypeConstant.CONCAT, new Resource("classpath", "bgimages/10.jpg"));
addResource(CaptchaTypeConstant.CONCAT, new Resource("classpath", "bgimages/48.jpg"));
addResource(CaptchaTypeConstant.WORD_IMAGE_CLICK, new Resource("classpath", "bgimages/02.jpg"));
addResource(CaptchaTypeConstant.WORD_IMAGE_CLICK, new Resource("classpath", "bgimages/03.jpg"));
addResource(CaptchaTypeConstant.WORD_IMAGE_CLICK, new Resource("classpath", "bgimages/06.jpg"));
addResource(CaptchaTypeConstant.WORD_IMAGE_CLICK, new Resource("classpath", "bgimages/08.jpg"));
}
}
1.4 Controller
@RestController
@RequestMapping("/")
public class CaptchaDemoController {
@Autowired
private ImageCaptchaApplication application;
@GetMapping("/index")
public ResponseEntity index(String type) {
CaptchaResponse<ImageCaptchaVO> res = application.generateCaptcha(type);
return ResponseEntity.ok(res);
}
@PostMapping ("/check")
public ResponseEntity check(@RequestBody CaptchaRequest<Map> request) {
boolean match = application.matching(request.getId(), request.getCaptchaTrack());
return ResponseEntity.ok(match);
}
}
二、前端jquery
2.1 通用代码
-
通用js 代码
通用js这块代码滑动验证码通用,最主要的就是提供记录用户滑动轨迹的三个函数,分别为down,move和up
var currentCaptchaConfig; /** 是否打印日志 */ var isPrintLog = false; function clearPreventDefault(event) { if (event.preventDefault) { event.preventDefault(); } } function clearAllPreventDefault($div) { $div.each(function (index, el) { el.addEventListener('touchmove', clearPreventDefault, false); }); } function reductionAllPreventDefault($div) { $div.each(function (index, el) { el.removeEventListener('touchmove', clearPreventDefault); }); } function printLog(...params) { if (isPrintLog) { console.log(JSON.stringify(params)); } } function initConfig(bgImageWidth, bgImageHeight, sliderImageWidth, sliderImageHeight, end) { currentCaptchaConfig = { startTime: new Date(), trackArr: [], movePercent: 0, bgImageWidth, bgImageHeight, sliderImageWidth, sliderImageHeight, end } printLog("init", currentCaptchaConfig); return currentCaptchaConfig; } function down(event) { let targetTouches = event.originalEvent ? event.originalEvent.targetTouches : event.targetTouches; let startX = event.pageX; let startY = event.pageY; if (startX === undefined) { startX = Math.round(targetTouches[0].pageX); startY = Math.round(targetTouches[0].pageY); } currentCaptchaConfig.startX = startX; currentCaptchaConfig.startY = startY; const pageX = currentCaptchaConfig.startX; const pageY = currentCaptchaConfig.startY; const startTime = currentCaptchaConfig.startTime; const trackArr = currentCaptchaConfig.trackArr; trackArr.push({ x: pageX - startX, y: pageY - startY, type: "down", t: (new Date().getTime() - startTime.getTime()) }); printLog("start", startX, startY) // pc window.addEventListener("mousemove", move); window.addEventListener("mouseup", up); // 手机端 window.addEventListener("touchmove", move, false); window.addEventListener("touchend", up, false); doDown(currentCaptchaConfig); } function move(event) { if (event instanceof TouchEvent) { event = event.touches[0]; } let pageX = Math.round(event.pageX); let pageY = Math.round(event.pageY); const startX = currentCaptchaConfig.startX; const startY = currentCaptchaConfig.startY; const startTime = currentCaptchaConfig.startTime; const end = currentCaptchaConfig.end; const bgImageWidth = currentCaptchaConfig.bgImageWidth; const trackArr = currentCaptchaConfig.trackArr; let moveX = pageX - startX; const track = { x: pageX - startX, y: pageY - startY, type: "move", t: (new Date().getTime() - startTime.getTime()) }; trackArr.push(track); if (moveX < 0) { moveX = 0; } else if (moveX > end) { moveX = end; } currentCaptchaConfig.moveX = moveX; currentCaptchaConfig.movePercent = moveX / bgImageWidth; doMove(currentCaptchaConfig); printLog("move", track) } function up(event) { window.removeEventListener("mousemove", move); window.removeEventListener("mouseup", up); window.removeEventListener("touchmove", move); window.removeEventListener("touchend", up); if (event instanceof TouchEvent) { event = event.changedTouches[0]; } currentCaptchaConfig.stopTime = new Date(); let pageX = Math.round(event.pageX); let pageY = Math.round(event.pageY); const startX = currentCaptchaConfig.startX; const startY = currentCaptchaConfig.startY; const startTime = currentCaptchaConfig.startTime; const trackArr = currentCaptchaConfig.trackArr; const track = { x: pageX - startX, y: pageY - startY, type: "up", t: (new Date().getTime() - startTime.getTime()) } trackArr.push(track); printLog("up", track) valid(currentCaptchaConfig); }
-
通用css 样式
common.css 通用样式
.slider { background-color: #fff; width: 278px; height: 285px; z-index: 999; box-sizing: border-box; padding: 9px; border-radius: 6px; box-shadow: 0 0 11px 0 #999999; } .slider .content { width: 100%; height: 159px; position: relative; } .bg-img-div { width: 100%; height: 100%; position: absolute; transform: translate(0px, 0px); } .slider-img-div { height: 100%; position: absolute; transform: translate(0px, 0px); } .bg-img-div img { width: 100%; } .slider-img-div img { height: 100%; } .slider .slider-move { height: 60px; width: 100%; margin: 11px 0; position: relative; } .slider .bottom { height: 19px; width: 100%; } .refresh-btn, .close-btn, .slider-move-track, .slider-move-btn { background: url(https://static.geetest.com/static/ant/sprite.1.2.4.png) no-repeat; } .refresh-btn, .close-btn { display: inline-block; } .slider-move .slider-move-track { line-height: 38px; font-size: 14px; text-align: center; white-space: nowrap; color: #88949d; -moz-user-select: none; -webkit-user-select: none; user-select: none; } .slider { user-select: none; } .slider-move .slider-move-btn { transform: translate(0px, 0px); background-position: -5px 11.79625%; position: absolute; top: -12px; left: 0; width: 66px; height: 66px; } .slider-move-btn:hover, .close-btn:hover, .refresh-btn:hover { cursor: pointer } .bottom .close-btn { width: 20px; height: 20px; background-position: 0 44.86874%; } .bottom .refresh-btn { width: 20px; height: 20px; background-position: 0 81.38425%; }
2.2 滑动验证码
-
html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>滑动验证码</title> <link rel="stylesheet" type="text/css" href="common.css"> </head> <body> <div class="slider"> <div class="content"> <div class="bg-img-div"> <img id="bg-img" src="" alt/> </div> <div class="slider-img-div" id="slider-img-div"> <img id="slider-img" src="" alt/> </div> </div> <div class="slider-move"> <div class="slider-move-track"> 拖动滑块完成拼图 </div> <div class="slider-move-btn" id="slider-move-btn"></div> </div> <div class="bottom"> <div class="close-btn" id="slider-close-btn"></div> <div class="refresh-btn" id="slider-refresh-btn"></div> </div> </div> <script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script> <script src="index.js"></script> <script type="text/javascript" src="slider.js"></script> </body> </html>
-
slider.js
let currentCaptchaId = null; $(function () { clearAllPreventDefault($(".slider")); refreshCaptcha(); }) $("#slider-move-btn").mousedown(down); $("#slider-move-btn").on("touchstart", down); $("#slider-close-btn").click(() => { }); $("#slider-refresh-btn").click(() => { refreshCaptcha(); }); function valid(captchaConfig) { let data = { 'id' : currentCaptchaId, 'captchaTrack': { bgImageWidth: captchaConfig.bgImageWidth, bgImageHeight: captchaConfig.bgImageHeight, sliderImageWidth: captchaConfig.sliderImageWidth, sliderImageHeight: captchaConfig.sliderImageHeight, startSlidingTime: captchaConfig.startTime, endSlidingTime: captchaConfig.stopTime, trackList: captchaConfig.trackArr } } $.ajax({ type:"POST", url:"http://localhost:8080/check", contentType: "application/json", dataType:"json", data:JSON.stringify(data), success:function (res) { if (res) { alert("验证成功!!!"); } refreshCaptcha(); } }) } function refreshCaptcha() { $.get("http://localhost:8080/index?type=SLIDER", function (data) { reset(); currentCaptchaId = data.id; const bgImg = $("#bg-img"); const sliderImg = $("#slider-img"); bgImg.attr("src", data.captcha.backgroundImage); sliderImg.attr("src", data.captcha.sliderImage); initConfig(bgImg.width(), bgImg.height(), sliderImg.width(), sliderImg.height(), 206); }) } function doDown() { $("#slider-move-btn").css("background-position", "-5px 31.0092%") } function doMove(currentCaptchaConfig) { const moveX = currentCaptchaConfig.moveX; $("#slider-move-btn").css("transform", "translate(" + moveX + "px, 0px)") $("#slider-img-div").css("transform", "translate(" + moveX + "px, 0px)") } function reset() { $("#slider-move-btn").css("background-position", "-5px 11.79625%") $("#slider-move-btn").css("transform", "translate(0px, 0px)") $("#slider-img-div").css("transform", "translate(0px, 0px)") currentCaptchaId = null; }
-
最终结果
2.3 旋转验证码
-
html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>旋转验证码</title> <link rel="stylesheet" type="text/css" href="common.css"> <style> .after { color: #88949d; } .rotate-img-div { height: 100%; position: absolute; transform: rotate(0deg); margin-left: 50px; } .rotate-img-div img { height: 100%; } </style> </head> <body> <div class="slider rotate"> <div class="content"> <div class="bg-img-div"> <img id="rotate-bg-img" src="" alt/> </div> <div class="rotate-img-div"> <img id="rotate-image" src="" alt/> </div> </div> <div class="slider-move"> <div class="slider-move-track"> 拖动滑块旋转正确位置 </div> <div class="slider-move-btn" id="rotate-move-btn"></div> </div> <div class="bottom"> <div class="close-btn" id="rotate-close-btn"></div> <div class="refresh-btn" id="rotate-refresh-btn"></div> </div> </div> <script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script> <script src="index.js"></script> <script src="rotate.js"></script> </body> </html>
-
rotate.js
$(function () { refreshCaptcha(); clearAllPreventDefault($(".slider")) }) // 旋转 let currentCaptchaId = null; function refreshCaptcha() { $.get("http://localhost:8080/index?type=ROTATE", function (data) { reset(); currentCaptchaId = data.id; const bgImg = $("#rotate-bg-img"); const sliderImg = $("#rotate-image"); bgImg.attr("src", data.captcha.backgroundImage); sliderImg.attr("src", data.captcha.sliderImage); initConfig(206, bgImg.height(), sliderImg.width(), sliderImg.height(), 206); }) } $("#rotate-move-btn").mousedown(down); $("#rotate-move-btn").on("touchstart", down); function doDown() { $("#slider-move-btn").css("background-position", "-5px 31.0092%") } function doMove(currentCaptchaConfig) { const moveX = currentCaptchaConfig.moveX; $("#rotate-move-btn").css("transform", "translate(" + moveX + "px, 0px)") $(".rotate-img-div").css("transform", "rotate(" + (moveX / (currentCaptchaConfig.end / 360)) + "deg)") } function valid(captchaConfig) { let data = { bgImageWidth: captchaConfig.bgImageWidth, bgImageHeight: captchaConfig.bgImageHeight, sliderImageWidth: captchaConfig.sliderImageWidth, sliderImageHeight: captchaConfig.sliderImageHeight, startSlidingTime: captchaConfig.startTime, endSlidingTime: captchaConfig.stopTime, // 官方demo 这里有个语法错误 trackList: captchaConfig.trackArr }; let sendData = { 'id' : currentCaptchaId, 'captchaTrack': data } $.ajax({ type:"POST", url:"http://localhost:8080/check", contentType: "application/json", dataType:"json", data:JSON.stringify(sendData), success:function (res) { if (res) { alert("验证成功!!!"); } refreshCaptcha(); } }) } $("#slider-close-btn").click(() => { }); $("#rotate-refresh-btn").click(() => { refreshCaptcha(); }); function reset() { $("#rotate-move-btn").css("background-position", "-5px 11.79625%") $("#rotate-move-btn").css("transform", "translate(0px, 0px)") $(".rotate-img-div").css("transform", "rotate(0deg)") currentCaptchaId = null; }
-
最终结果
2.4 滑动还原验证码
-
html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>滑动还原验证码</title> <link rel="stylesheet" type="text/css" href="common.css"> <style> .bg-img-div { width: 100%; height: 100%; position: absolute; transform: translate(0px, 0px); background-size: 100% 159px; background-image: none; background-position: 0 0; z-index: 0; } .slider-img-div { height: 100%; width: 100%; background-size: 100% 159px; position: absolute; transform: translate(0px, 0px); /*border-bottom: 1px solid blue;*/ z-index: 1; } </style> </head> <body> <div class="slider"> <div class="content"> <div class="slider-img-div" id="slider-img-div"> <img id="slider-img" src="" alt/> </div> <div class="bg-img-div"> </div> </div> <div class="slider-move"> <div class="slider-move-track"> 拖动滑块完成拼图 </div> <div class="slider-move-btn" id="slider-move-btn"></div> </div> <div class="bottom"> <div class="close-btn" id="slider-close-btn"></div> <div class="refresh-btn" id="slider-refresh-btn"></div> </div> </div> <script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script> <script src="index.js"></script> <script src="concat.js"></script> </body> </html>
-
js
var currentCaptchaId; $(function () { refreshCaptcha(); clearAllPreventDefault($(".slider")); }) $("#slider-move-btn").mousedown(down); $("#slider-move-btn").on("touchstart", down); function doDown() { $("#slider-move-btn").css("background-position", "-5px 31.0092%") } function doMove(config) { const moveX = config.moveX; $("#slider-move-btn").css("transform", "translate(" + moveX + "px, 0px)") $("#slider-img-div").css("background-position-x", moveX + "px"); } $("#slider-close-btn").click(() => { }); $("#slider-refresh-btn").click(() => { refreshCaptcha(); }); function valid(captchaConfig) { let data = { 'id' : currentCaptchaId, 'captchaTrack': { bgImageWidth: captchaConfig.bgImageWidth, bgImageHeight: captchaConfig.bgImageHeight, sliderImageWidth: captchaConfig.sliderImageWidth, sliderImageHeight: captchaConfig.sliderImageHeight, startSlidingTime: captchaConfig.startTime, endSlidingTime: captchaConfig.stopTime, trackList: captchaConfig.trackArr } } $.ajax({ type:"POST", url:"http://localhost:8080/check", contentType: "application/json", dataType:"json", data:JSON.stringify(data), success:function (res) { if (res) { alert("验证成功!!!"); } refreshCaptcha(); } }) } function refreshCaptcha() { $.get("http://localhost:8080/index?type=CONCAT", function (data) { reset(); currentCaptchaId = data.id; const bgImg = $(".bg-img-div"); const sliderImg = $(".slider-img-div"); bgImg.css("background-image", "url(" + data.captcha.backgroundImage + ")"); sliderImg.css("background-image", "url(" + data.captcha.backgroundImage + ")"); sliderImg.css("background-position", "0px 0px"); var backgroundImageHeight = data.captcha.backgroundImageHeight; var height = ((backgroundImageHeight - data.captcha.data) / backgroundImageHeight) * 159; $(".slider-img-div").css("height", height); initConfig(bgImg.width(), bgImg.height(), sliderImg.width(), sliderImg.height(), 206); }) } function reset() { $("#slider-move-btn").css("background-position", "-5px 11.79625%") $("#slider-move-btn").css("transform", "translate(0px, 0px)") $("#slider-img-div").css("transform", "translate(0px, 0px)") currentCaptchaId = null; }
-
最终结果
2.5 文字点选验证码
-
html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>文字点选验证码</title> <link rel="stylesheet" type="text/css" href="common.css"> <style> .tip-img { width: 130px; position: absolute; right: 5px; } .slider-move-span { font-size: 18px; display: inline-block; height: 40px; line-height: 40px; } .click-span { position: absolute; left: 0; top: 0; border-radius: 50px; background-color: #409eff; width: 20px; height: 20px; text-align: center; line-height: 20px; color: #fff; border: 2px solid #fff; } .submit-btn { height: 40px; width: 120px; line-height: 40px; text-align: center; background-color: #409eff; color: #fff; font-size: 15px; box-sizing: border-box; border: 1px solid #409eff; float: right; border-radius: 5px; } </style> </head> <body> <div class="slider"> <div class="slider-move"> <span class="slider-move-span">请依次点击:</span><img src="" class="tip-img"> </div> <div class="content"> <div class="bg-img-div"> <img id="bg-img" src="" alt/> </div> <div class="bg-click-div"> </div> </div> <div class="bottom"> <div class="close-btn" id="slider-close-btn"></div> <div class="refresh-btn" id="slider-refresh-btn"></div> </div> </div> <script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script> <script src="word-click.js"></script> </body> </html>
-
js
let start = 0; let startY = 0; let currentCaptchaId = null; let movePercent = 0; const bgImgWidth = $(".bg-img-div").width(); let end = 206; let startSlidingTime; let entSlidingTime; const trackArr = []; let clickCount = 0; $(function () { refreshCaptcha(); }) $(".content").click(function (event) { console.log(event); clickCount++; if (clickCount === 1) { startSlidingTime = new Date(); // move 轨迹 window.addEventListener("mousemove", move); } trackArr.push({ x: event.offsetX, y: event.offsetY, type: "click", t: (new Date().getTime() - startSlidingTime.getTime()) }); const left = event.offsetX - 10; const top = event.offsetY - 10; $(".bg-click-div").append("<span class='click-span' style='left:" + left + "px;top: " + top + "px'>" + clickCount + "</span>") if (clickCount === 4) { // 校验 entSlidingTime = new Date(); window.removeEventListener("mousemove", move); valid(); } }); function move(event) { if (event instanceof TouchEvent) { event = event.touches[0]; } console.log("x:", event.offsetX, "y:", event.offsetY, "time:" ,new Date().getTime() - startSlidingTime.getTime()); trackArr.push({x: event.offsetX, y:event.offsetY, t: (new Date().getTime() - startSlidingTime.getTime()), type: "move"}); } $("#slider-close-btn").click(() => { }); $("#slider-refresh-btn").click(() => { refreshCaptcha(); }); function valid() { let data = { 'id': currentCaptchaId, 'captchaTrack': { bgImageWidth: $(".bg-img-div").width(), bgImageHeight: $(".content").height(), sliderImageWidth: -1, sliderImageHeight: -1, startSlidingTime: startSlidingTime, entSlidingTime: entSlidingTime, trackList: trackArr } }; $.ajax({ type:"POST", url:"http://localhost:8080/check", contentType: "application/json", dataType:"json", data:JSON.stringify(data), success:function (res) { if (res) { alert("验证成功!!!"); } refreshCaptcha(); } }) } function refreshCaptcha() { $.get("http://localhost:8080/index?type=WORD_IMAGE_CLICK", function (data) { reset(); currentCaptchaId = data.id; $("#bg-img").attr("src", data.captcha.backgroundImage); $("#slider-img").attr("src", data.captcha.sliderImage); $(".tip-img").attr("src", data.captcha.sliderImage); }) } function reset() { $("#slider-move-btn").css("background-position", "-5px 11.79625%") $("#slider-move-btn").css("transform", "translate(0px, 0px)") $("#slider-img-div").css("transform", "translate(0px, 0px)") start = 0; startSlidingTime = null; entSlidingTime = null; trackArr.length = 0; $(".bg-click-div span").remove(); clickCount = 0; movePercent = 0; currentCaptchaId = null; startY = 0; window.removeEventListener("mousemove", move); }
-
最终结果
三、源码探秘和总结
3.1 前端代码
前端代码部分主要是传递用户鼠标数据给后端,可以根据滑动和点选分为两种
-
滑动
滑动数据主要逻辑部分就在通用js代码中,主要就是三个函数,用户鼠标按下,移动,抬起三种操作的监听函数
-
点选
点选和滑动不一样,需要记录点击4个汉字的坐标,然后传递给后端
3.2 后端代码
下面会对上述验证码后端生成,校验和存储进行源码探秘
-
滑块验证码
滑块验证码的图片生成部分可以见:StandardSliderImageCaptchaGenerator.doGenerateCaptchaImage方法,随机从模板获取背景图片,再用滑块的图片,选择随机x,y坐标覆盖掉
// 获取随机的 x 和 y 轴 int randomX = ThreadLocalRandom.current().nextInt(fixedTemplate.getWidth() + 5, targetBackground.getWidth() - fixedTemplate.getWidth() - 10); int randomY = ThreadLocalRandom.current().nextInt(targetBackground.getHeight() - fixedTemplate.getHeight()); CaptchaImageUtils.overlayImage(targetBackground, fixedTemplate, randomX, randomY); if (obfuscate) { // 加入混淆滑块 int obfuscateX = randomObfuscateX(randomX, fixedTemplate.getWidth(), targetBackground.getWidth()); CaptchaImageUtils.overlayImage(targetBackground, fixedTemplate, obfuscateX, randomY); } BufferedImage cutImage = CaptchaImageUtils.cutImage(cutBackground, fixedTemplate, randomX, randomY); CaptchaImageUtils.overlayImage(cutImage, activeTemplate, 0, 0); CaptchaImageUtils.overlayImage(matrixTemplate, cutImage, 0, randomY); return wrapSliderCaptchaInfo(randomX, randomY, targetBackground, matrixTemplate, param);
-
旋转验证码
生成部分可以见:StandardRotateImageCaptchaGenerator.doGenerateCaptchaImage 方法,选择居中部分抠出旋转随机角度
// 算出居中的x和y int x = targetBackground.getWidth() / 2 - fixedTemplate.getWidth() / 2; int y = targetBackground.getHeight() / 2 - fixedTemplate.getHeight() / 2; CaptchaImageUtils.overlayImage(targetBackground, fixedTemplate, x, y); // 抠图部分 BufferedImage cutImage = CaptchaImageUtils.cutImage(cutBackground, fixedTemplate, x, y); CaptchaImageUtils.overlayImage(cutImage, activeTemplate, 0, 0); // 随机旋转抠图部分 // 随机x, 转换为角度 int randomX = ThreadLocalRandom.current().nextInt(fixedTemplate.getWidth() + 10, targetBackground.getWidth() - 10); double degree = 360d - randomX / ((targetBackground.getWidth()) / 360d); CaptchaImageUtils.centerOverlayAndRotateImage(matrixTemplate, cutImage, degree); return wrapRotateCaptchaInfo(degree, randomX, targetBackground, matrixTemplate, param);
-
滑块还原验证码
生成部分可以见:StandardConcatImageCaptchaGenerator.doGenerateCaptchaImage 方法, 选择1/4-3/4高度随机值作为y坐标,这是切割部分,将图片切断成两部分,上面是可滑动的。1/8-4/5宽度随机值作为x坐标,这是x轴分隔点
Resource resourceImage = imageCaptchaResourceManager.randomGetResource(param.getType()); InputStream resourceInputStream = imageCaptchaResourceManager.getResourceInputStream(resourceImage); inputStreams.add(resourceInputStream); BufferedImage bgImage = wrapFile2BufferedImage(resourceInputStream); int spacingY = bgImage.getHeight() / 4; int randomY = ThreadLocalRandom.current().nextInt(spacingY, bgImage.getHeight() - spacingY); BufferedImage[] bgImageSplit = splitImage(randomY, true, bgImage); int spacingX = bgImage.getWidth() / 8; int randomX = ThreadLocalRandom.current().nextInt(spacingX, bgImage.getWidth() - bgImage.getWidth() / 5); BufferedImage[] bgImageTopSplit = splitImage(randomX, false, bgImageSplit[0]); BufferedImage sliderImage = concatImage(true, bgImageTopSplit[0].getWidth() + bgImageTopSplit[1].getWidth(), bgImageTopSplit[0].getHeight(), bgImageTopSplit[1], bgImageTopSplit[0]); bgImage = concatImage(false, bgImageSplit[1].getWidth(), sliderImage.getHeight() + bgImageSplit[1].getHeight(), sliderImage, bgImageSplit[1]); return wrapConcatCaptchaInfo(randomX, randomY, bgImage, param);
-
文字点选验证码
通用生成部分可以见:AbstractClickImageCaptchaGenerator.doGenerateCaptchaImage 方法, 具体文字生成部分可以见StandardRandomWordClickImageCaptchaGenerator.genTipImage。
生成的文字个数由变量 checkClickCount=4控制,已在代码中写死,后面是否可以做成配置?
List<ClickImageCheckDefinition> clickImageCheckDefinitionList = new ArrayList<>(interferenceCount); int allImages = interferenceCount + checkClickCount; int avg = bgImage.getWidth() / allImages; List<String> imgTips = randomGetClickImgTips(allImages); if (allImages < imgTips.size()) { throw new IllegalStateException("随机生成点击图片小于请求数量, 请求生成数量=" + allImages + ",实际生成数量=" + imgTips.size()); } for (int i = 0; i < allImages; i++) { // 随机获取点击图片 ImgWrapper imgWrapper = getClickImg(imgTips.get(i)); BufferedImage image = imgWrapper.getImage(); int clickImgWidth = image.getWidth(); int clickImgHeight = image.getHeight(); // 随机x int randomX; if (i == 0) { randomX = 1; } else { randomX = avg * i; } // 随机y int randomY = ThreadLocalRandom.current().nextInt(10, bgImage.getHeight() - clickImgHeight); // 通过随机x和y 进行覆盖图片 CaptchaImageUtils.overlayImage(bgImage, imgWrapper.getImage(), randomX, randomY); ClickImageCheckDefinition clickImageCheckDefinition = new ClickImageCheckDefinition(); clickImageCheckDefinition.setTip(imgWrapper.getTip()); clickImageCheckDefinition.setX(randomX + clickImgWidth / 2); clickImageCheckDefinition.setY(randomY + clickImgHeight / 2); clickImageCheckDefinition.setWidth(clickImgWidth); clickImageCheckDefinition.setHeight(clickImgHeight); clickImageCheckDefinitionList.add(clickImageCheckDefinition); } List<ClickImageCheckDefinition> checkClickImageCheckDefinitionList = getCheckClickImageCheckDefinitionList(clickImageCheckDefinitionList,checkClickCount); return wrapClickImageCaptchaInfo(param, bgImage, checkClickImageCheckDefinitionList);
-
校验验证码
校验部分可以根据滑动还是点选分为两种
-
滑块
源码主要实现部分位于SimpleImageCaptchaValidator.doValidSliderCaptcha方法,目前只校验最后一个轨迹是否到达缺口处,没有对所有轨迹进行行为校验
public boolean doValidSliderCaptcha(ImageCaptchaTrack imageCaptchaTrack, Map<String, Object> sliderCaptchaValidData, Float tolerant, String type) { Float oriPercentage = getFloatParam(PERCENTAGE_KEY, sliderCaptchaValidData); if (oriPercentage == null) { // 没读取到百分比 return false; } List<ImageCaptchaTrack.Track> trackList = imageCaptchaTrack.getTrackList(); // 取最后一个滑动轨迹 ImageCaptchaTrack.Track lastTrack = trackList.get(trackList.size() - 1); // 计算百分比 float calcPercentage = calcPercentage(lastTrack.getX(), imageCaptchaTrack.getBgImageWidth()); // 校验百分比 return checkPercentage(calcPercentage, oriPercentage, tolerant); }
-
点选
源码实现部分位于SimpleImageCaptchaValidator.doValidClickCaptcha方法,可以看到按照顺序对点选轨迹依次进行XY坐标进行百分比校验,默认有2%的容错
public boolean doValidClickCaptcha(ImageCaptchaTrack imageCaptchaTrack, Map<String, Object> sliderCaptchaValidData, Float tolerant, String type) { String validStr = getStringParam(PERCENTAGE_KEY, sliderCaptchaValidData, null); if (ObjectUtils.isEmpty(validStr)) { return false; } String[] splitArr = validStr.split(";"); List<ImageCaptchaTrack.Track> trackList = imageCaptchaTrack.getTrackList(); if (trackList.size() < splitArr.length) { return false; } // 取出点击事件的轨迹数据 List<ImageCaptchaTrack.Track> clickTrackList = trackList .stream() .filter(t -> TrackTypeConstant.CLICK.equalsIgnoreCase(t.getType())) .collect(Collectors.toList()); if (clickTrackList.size() != splitArr.length) { return false; } for (int i = 0; i < splitArr.length; i++) { ImageCaptchaTrack.Track track = clickTrackList.get(i); String posStr = splitArr[i]; String[] posArr = posStr.split(","); float xPercentage = Float.parseFloat(posArr[0]); float yPercentage = Float.parseFloat(posArr[1]); float calcXPercentage = calcPercentage(track.getX(), imageCaptchaTrack.getBgImageWidth()); float calcYPercentage = calcPercentage(track.getY(), imageCaptchaTrack.getBgImageHeight()); if (!checkPercentage(calcXPercentage, xPercentage, tolerant) || !checkPercentage(calcYPercentage, yPercentage, tolerant)) { return false; } } return true; }
-
-
存储部分
验证码存储部分主要接口为 CacheStore,如果项目中引入了redis ,那么会使用redis来存储,否则会使用本地存储
@Configuration(proxyBeanMethods = false) @ConditionalOnClass(StringRedisTemplate.class) @Import({RedisAutoConfiguration.class}) @AutoConfigureAfter({RedisAutoConfiguration.class}) public static class RedisCacheStoreConfiguration { @Bean @ConditionalOnBean(StringRedisTemplate.class) @ConditionalOnMissingBean(CacheStore.class) public CacheStore redis(StringRedisTemplate redisTemplate) { return new RedisCacheStore(redisTemplate); } } @Configuration(proxyBeanMethods = false) @AutoConfigureAfter({RedisCacheStoreConfiguration.class}) @Import({RedisCacheStoreConfiguration.class}) public static class LocalCacheStoreConfiguration { @Bean @ConditionalOnMissingBean(CacheStore.class) public CacheStore local() { return new LocalCacheStore(); } }
3.3 总结
- 目前这个库只提供4种行为验证码,不过在后端源码验证码类型CaptchaTypeConstant 中发现了图片点选的常量,后续版本应该会加上图片点选的验证码,这种很常见,使用梯子访问谷歌时经常会碰到这种,要用户选择红绿灯、摩托车等等。
- 最后,感谢作者将如此棒的库开源,已 starred
参考
- https://gitee.com/tianai/tianai-captcha-demo