SpringBoot learning summary slider verification code generation library tianai-captcha

foreword

I recently discovered an interesting Java verification code library that can generate various behavior verification codes: slider, rotation, and click verification codes.

github: https://github.com/tianaiyouqing/tianai-captcha

pom dependencies:

<!-- 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>

Note: Most of the following codes are from the official demo: https://gitee.com/tianai/tianai-captcha-demo

1. Backend springboot

1.1 yml configuration

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 Cross-domain configuration

cross-domain issues

@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 Resource allocation

@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);
    }
}

Two, front-end jquery

2.1 General code

  • Universal js code

    Universal js is a common sliding verification code. The most important thing is to provide three functions to record the user's sliding track, which are down, move and 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);
    }
    
  • common css styles

    common.css common styles

    .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 Sliding verification code

  • 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;
    }
    
  • Final Results

    Slide verification code

2.3 Rotation Captcha

  • 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;
    }
    
  • Final Results

    Rotate Captcha

2.4 Slide to restore verification code

  • 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;
    }
    
  • Final Results

Swipe to restore verification code

2.5 Verification code for text selection

  • 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);
    }
    
  • Final Results

    text click verification code

3. Source code exploration and summary

3.1 Front-end code

The front-end code part mainly transmits the user's mouse data to the back-end, which can be divided into two types according to sliding and clicking

  • slide

    The main logic part of the sliding data is in the general js code, mainly three functions, the monitoring function of the user's mouse pressing, moving, and lifting three operations

  • click on

    Clicking is not the same as sliding, you need to record the coordinates of clicking 4 Chinese characters, and then pass it to the backend

3.2 Backend code

The following will explore the source code of the above verification code backend generation, verification and storage

  • slider captcha

    The image generation part of the slider verification code can be seen in: StandardSliderImageCaptchaGenerator.doGenerateCaptchaImage method, randomly obtain the background image from the template, and then use the slider image to select random x, y coordinates to overwrite

    // 获取随机的 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);
    
  • Rotate Captcha

    The generation part can be seen: StandardRotateImageCaptchaGenerator.doGenerateCaptchaImage method, select the center part to cut out the random angle of rotation

    // 算出居中的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);
    
  • Slider Restore Captcha

    The generation part can be seen: StandardConcatImageCaptchaGenerator.doGenerateCaptchaImage method, select 1/4-3/4 height random value as the y coordinate, this is the cutting part, cut the picture into two parts, and the top is slidable. 1/8-4/5 width random value as x-coordinate, this is the x-axis separation point

    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);
    
  • text click verification code

    For the general generation part, see: AbstractClickImageCaptchaGenerator.doGenerateCaptchaImage method, and for the specific text generation part, see StandardRandomWordClickImageCaptchaGenerator.genTipImage.

    The number of generated text is controlled by the variable checkClickCount=4, which has been hard-coded in the code. Can it be configured later?

    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);
    
    
  • Verify verification code

    The verification part can be divided into two types according to sliding or clicking

    • slider

      The main implementation part of the source code is located in the SimpleImageCaptchaValidator.doValidSliderCaptcha method. At present, it only verifies whether the last trajectory reaches the gap, and does not perform behavior verification on all trajectories.

      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);
      }
      
    • click on

      The source code implementation part is located in the SimpleImageCaptchaValidator.doValidClickCaptcha method. You can see that the XY coordinates of the clicked tracks are checked in order by percentage, and the default is 2% fault tolerance.

      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;
      }
      
  • storage part

    The main interface of the verification code storage part is CacheStore. If redis is introduced in the project, then redis will be used for storage, otherwise local storage will be used

    @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 Summary

  • At present, this library only provides 4 kinds of behavior verification codes, but in the back-end source code verification code type CaptchaTypeConstant, we found the constant of picture click, and the follow-up version should add the verification code of picture click, which is very common, use the ladder to access Google often encounters this, asking users to choose traffic lights, motorcycles, and so on.
  • Finally, thanks to the author for open-sourcing such a great library, which has been starred

reference

  1. https://gitee.com/tianai/tianai-captcha-demo

Guess you like

Origin blog.csdn.net/qq_23091073/article/details/128361588