最近发现一个有趣的 Java 验证码库,能够生成各种行为验证码:滑块、旋转、点选验证码。
github: https://github.com/tianaiyouqing/tianai-captcha
pom 依赖:
<dependency>
<groupId>cloud.tianai.captchagroupId>
<artifactId>tianai-captcha-springboot-starterartifactId>
<version>1.3.3version>
dependency>
<dependency>
<groupId>cloud.tianai.captchagroupId>
<artifactId>tianai-captchaartifactId>
<version>1.3.3version>
dependency>
注:以下代码大部分来自官方demo: https://gitee.com/tianai/tianai-captcha-demo
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"
跨域问题
@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是一小时
}
};
}
}
@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"));
}
}
@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);
}
}
通用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%;
}
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;
}
最终结果
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;
}
最终结果
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;
}
最终结果
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("" + clickCount + "")
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);
}
最终结果
前端代码部分主要是传递用户鼠标数据给后端,可以根据滑动和点选分为两种
滑动
滑动数据主要逻辑部分就在通用js代码中,主要就是三个函数,用户鼠标按下,移动,抬起三种操作的监听函数
点选
点选和滑动不一样,需要记录点击4个汉字的坐标,然后传递给后端
下面会对上述验证码后端生成,校验和存储进行源码探秘
滑块验证码
滑块验证码的图片生成部分可以见: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();
}
}