SpringBoot 学習概要スライダー検証コード生成ライブラリ tinai-captcha

序文

最近、スライダー、回転、クリックなどのさまざまな動作検証コードを生成できる興味深い 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>

注: 次のコードのほとんどは公式デモからのものです: https://gitee.com/tianai/tianai-captcha-demo

1.バックエンドスプリングブート

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 コントローラー

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

2、フロントエンド jQuery

2.1 一般的なコード

  • ユニバーサルjsコード

    Universal js は一般的なスライド検証コードですが、最も重要なのは、ユーザーのスライド軌跡を記録するための 3 つの関数 (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>
    
  • スライダー.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>
    
  • 回転.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. ソースコードの調査と要約

3.1 フロントエンドコード

フロントエンドコード部分は主にユーザーのマウスデータをバックエンドに送信します。スライドとクリックに応じて2つのタイプに分けられます。

  • 滑り台

    スライドデータのメインロジック部分は一般的なjsコードで、主に3つの機能、ユーザーのマウスを押す、動かす、持ち上げる3つの操作を監視する機能です。

  • クリック

    クリックはスライドとは異なります。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 メソッドで確認できます。y 座標として 1/4 ~ 3/4 の高さのランダムな値を選択します。これが切り取り部分で、画像を 2 つの部分に切り取り、上部はスライド可能です。x 座標としての 1/8 ~ 4/5 幅のランダムな値。これは 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);
    
    
  • 確認コードを確認する

    検証部分はスライドかクリックかによって2種類に分けられます

    • スライダー

      ソースコードの主な実装部分は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 で、ピクチャ クリックの定数を発見したため、続バージョンではピクチャ クリックの検証コードを追加する必要があります。これは非常に一般的ですが、はしごを使用して Google にアクセスすると、ユーザーに信号機やオートバイなどを選択するよう求められることがよくあります。
  • 最後に、このような素晴らしいライブラリをオープンソース化し、スターを付けてくれた作者に感謝します。

参考

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

おすすめ

転載: blog.csdn.net/qq_23091073/article/details/128361588