SpringBoot学习小结之滑块验证码生成库tianai-captcha

news2024/12/22 20:51:57

文章目录

  • 前言
  • 一、后端springboot
    • 1.1 yml 配置
    • 1.2 跨域配置
    • 1.3 资源配置
    • 1.4 Controller
  • 二、前端jquery
    • 2.1 通用代码
    • 2.2 滑动验证码
    • 2.3 旋转验证码
    • 2.4 滑动还原验证码
    • 2.5 文字点选验证码
  • 三、源码探秘和总结
    • 3.1 前端代码
    • 3.2 后端代码
    • 3.3 总结
  • 参考

前言

最近发现一个有趣的 Java 验证码库,能够生成各种行为验证码:滑块、旋转、点选验证码。

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

pom 依赖:

<!-- springboot -->
<dependency>
    <groupId>cloud.tianai.captcha</groupId>
    <artifactId>tianai-captcha-springboot-starter</artifactId>
    <version>1.3.3</version>
</dependency>

<!-- 非springboot -->
<dependency>
    <groupId>cloud.tianai.captcha</groupId>
    <artifactId>tianai-captcha</artifactId>
    <version>1.3.3</version>
</dependency>

注:以下代码大部分来自官方demo: https://gitee.com/tianai/tianai-captcha-demo

一、后端springboot

1.1 yml 配置

captcha:
  # 如果项目中使用到了redis,滑块验证码会自动把验证码数据存到redis中, 这里配置redis的key的前缀,默认是captcha:slider
  prefix: captcha
  # 验证码过期时间,默认是2分钟,单位毫秒, 可以根据自身业务进行调整
  expire:
    # 默认缓存时间 2分钟
    default: 10000
    # 针对 点选验证码 过期时间设置为 2分钟, 因为点选验证码验证比较慢,把过期时间调整大一些
    WORD_IMAGE_CLICK: 20000
  # 使用加载系统自带的资源, 默认是 false
  init-default-resource: false
  cache:
    # 缓存控制, 默认为false不开启
    enabled: true
    # 验证码会提前缓存一些生成好的验证数据, 默认是20
    cacheSize: 20
    # 缓存拉取失败后等待时间 默认是 5秒钟
    wait-time: 5000
    # 缓存检查间隔 默认是2秒钟
    period: 2000
    secondary:
      # 二次验证, 默认false 不开启
      enabled: false
      # 二次验证过期时间, 默认 2分钟
      expire: 120000
      # 二次验证缓存key前缀,默认是 captcha:secondary
      keyPrefix: "captcha:secondary"

1.2 跨域配置

跨域问题

@Configuration
public class CorsConfig {
    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurer() {
            // 重写父类提供的跨域请求处理的接口
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                // 添加映射路径
                registry.addMapping("/**")
                        .allowedOrigins("*")                         // 放行哪些域名,可以多个
                        .allowCredentials(true)                             // 是否发送Cookie信息
                        .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS", "PATCH") // 放行哪些请求方式
                        .allowedHeaders("*")                                // 放行哪些原始域(头部信息)
                        .exposedHeaders("Header1", "Header2")               // 暴露哪些头部信息(因为跨域访问默认不能获取全部头部信息)
                        .maxAge(3600);                                      // 预请求的结果有效期,默认1800分钟,3600是一小时
            }
        };
    }
}

1.3 资源配置

@Component
public class MyResourceStore extends DefaultResourceStore {

    public MyResourceStore() {

        // 滑块验证码 模板 (系统内置)
        Map<String, Resource> template1 = new HashMap<>(4);
        template1.put(SliderCaptchaConstant.TEMPLATE_ACTIVE_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME, DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/1/active.png")));
        template1.put(SliderCaptchaConstant.TEMPLATE_FIXED_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME, DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/1/fixed.png")));
        template1.put(SliderCaptchaConstant.TEMPLATE_MATRIX_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME, DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/1/matrix.png")));
        Map<String, Resource> template2 = new HashMap<>(4);
        template2.put(SliderCaptchaConstant.TEMPLATE_ACTIVE_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME, DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/2/active.png")));
        template2.put(SliderCaptchaConstant.TEMPLATE_FIXED_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME, DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/2/fixed.png")));
        template2.put(SliderCaptchaConstant.TEMPLATE_MATRIX_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME, DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/2/matrix.png")));
        // 旋转验证码 模板 (系统内置)
        Map<String, Resource> template3 = new HashMap<>(4);
        template3.put(SliderCaptchaConstant.TEMPLATE_ACTIVE_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME, StandardSliderImageCaptchaGenerator.DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/3/active.png")));
        template3.put(SliderCaptchaConstant.TEMPLATE_FIXED_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME, StandardSliderImageCaptchaGenerator.DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/3/fixed.png")));
        template3.put(SliderCaptchaConstant.TEMPLATE_MATRIX_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME, StandardSliderImageCaptchaGenerator.DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/3/matrix.png")));

        // 1. 添加一些模板
        addTemplate(CaptchaTypeConstant.SLIDER, template1);
        addTemplate(CaptchaTypeConstant.SLIDER, template2);
        addTemplate(CaptchaTypeConstant.ROTATE, template3);

        // 2. 添加自定义背景图片
        addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/a.jpg"));
        addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/b.jpg"));
        addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/c.jpg"));
        addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/d.jpg"));
        addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/e.jpg"));
        addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/g.jpg"));
        addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/h.jpg"));
        addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/i.jpg"));
        addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/j.jpg"));
        addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/01.jpg"));
        addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/02.jpg"));
        addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/03.jpg"));
        addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/04.jpg"));
        addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/05.jpg"));
        addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/06.jpg"));
        addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/07.jpg"));
        addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/08.jpg"));
        addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/09.jpg"));

        addResource(CaptchaTypeConstant.ROTATE, new Resource("classpath", "bgimages/10.jpg"));
        addResource(CaptchaTypeConstant.ROTATE, new Resource("classpath", "bgimages/48.jpg"));

        addResource(CaptchaTypeConstant.CONCAT, new Resource("classpath", "bgimages/10.jpg"));
        addResource(CaptchaTypeConstant.CONCAT, new Resource("classpath", "bgimages/48.jpg"));

        addResource(CaptchaTypeConstant.WORD_IMAGE_CLICK, new Resource("classpath", "bgimages/02.jpg"));
        addResource(CaptchaTypeConstant.WORD_IMAGE_CLICK, new Resource("classpath", "bgimages/03.jpg"));
        addResource(CaptchaTypeConstant.WORD_IMAGE_CLICK, new Resource("classpath", "bgimages/06.jpg"));
        addResource(CaptchaTypeConstant.WORD_IMAGE_CLICK, new Resource("classpath", "bgimages/08.jpg"));
    }
}

1.4 Controller

@RestController
@RequestMapping("/")
public class CaptchaDemoController {

    @Autowired
    private ImageCaptchaApplication application;

    @GetMapping("/index")
    public ResponseEntity index(String type) {
            CaptchaResponse<ImageCaptchaVO> res = application.generateCaptcha(type);
            return ResponseEntity.ok(res);
    }

    @PostMapping ("/check")
    public ResponseEntity check(@RequestBody CaptchaRequest<Map> request) {
        boolean match = application.matching(request.getId(), request.getCaptchaTrack());
        return ResponseEntity.ok(match);
    }
}

二、前端jquery

2.1 通用代码

  • 通用js 代码

    通用js这块代码滑动验证码通用,最主要的就是提供记录用户滑动轨迹的三个函数,分别为down,move和up

    var currentCaptchaConfig;
    /** 是否打印日志 */
    var isPrintLog = false;
    
    function clearPreventDefault(event) {
        if (event.preventDefault) {
            event.preventDefault();
        }
    }
    
    function clearAllPreventDefault($div) {
        $div.each(function (index, el) {
            el.addEventListener('touchmove', clearPreventDefault, false);
        });
    }
    
    function reductionAllPreventDefault($div) {
        $div.each(function (index, el) {
            el.removeEventListener('touchmove', clearPreventDefault);
        });
    }
    
    function printLog(...params) {
        if (isPrintLog) {
            console.log(JSON.stringify(params));
        }
    }
    
    function initConfig(bgImageWidth, bgImageHeight, sliderImageWidth, sliderImageHeight, end) {
        currentCaptchaConfig = {
            startTime: new Date(),
            trackArr: [],
            movePercent: 0,
            bgImageWidth,
            bgImageHeight,
            sliderImageWidth,
            sliderImageHeight,
            end
        }
        printLog("init", currentCaptchaConfig);
        return currentCaptchaConfig;
    }
    
    function down(event) {
        let targetTouches = event.originalEvent ? event.originalEvent.targetTouches : event.targetTouches;
        let startX = event.pageX;
        let startY = event.pageY;
        if (startX === undefined) {
            startX = Math.round(targetTouches[0].pageX);
            startY = Math.round(targetTouches[0].pageY);
        }
        currentCaptchaConfig.startX = startX;
        currentCaptchaConfig.startY = startY;
    
        const pageX = currentCaptchaConfig.startX;
        const pageY = currentCaptchaConfig.startY;
        const startTime = currentCaptchaConfig.startTime;
        const trackArr = currentCaptchaConfig.trackArr;
        trackArr.push({
            x: pageX - startX,
            y: pageY - startY,
            type: "down",
            t: (new Date().getTime() - startTime.getTime())
        });
        printLog("start", startX, startY)
        // pc
        window.addEventListener("mousemove", move);
        window.addEventListener("mouseup", up);
        // 手机端
        window.addEventListener("touchmove", move, false);
        window.addEventListener("touchend", up, false);
        doDown(currentCaptchaConfig);
    }
    
    function move(event) {
        if (event instanceof TouchEvent) {
            event = event.touches[0];
        }
        let pageX = Math.round(event.pageX);
        let pageY = Math.round(event.pageY);
        const startX = currentCaptchaConfig.startX;
        const startY = currentCaptchaConfig.startY;
        const startTime = currentCaptchaConfig.startTime;
        const end = currentCaptchaConfig.end;
        const bgImageWidth = currentCaptchaConfig.bgImageWidth;
        const trackArr = currentCaptchaConfig.trackArr;
        let moveX = pageX - startX;
        const track = {
            x: pageX - startX,
            y: pageY - startY,
            type: "move",
            t: (new Date().getTime() - startTime.getTime())
        };
        trackArr.push(track);
        if (moveX < 0) {
            moveX = 0;
        } else if (moveX > end) {
            moveX = end;
        }
        currentCaptchaConfig.moveX = moveX;
        currentCaptchaConfig.movePercent = moveX / bgImageWidth;
        doMove(currentCaptchaConfig);
        printLog("move", track)
    }
    
    function up(event) {
        window.removeEventListener("mousemove", move);
        window.removeEventListener("mouseup", up);
        window.removeEventListener("touchmove", move);
        window.removeEventListener("touchend", up);
        if (event instanceof TouchEvent) {
            event = event.changedTouches[0];
        }
        currentCaptchaConfig.stopTime = new Date();
        let pageX = Math.round(event.pageX);
        let pageY = Math.round(event.pageY);
        const startX = currentCaptchaConfig.startX;
        const startY = currentCaptchaConfig.startY;
        const startTime = currentCaptchaConfig.startTime;
        const trackArr = currentCaptchaConfig.trackArr;
    
        const track = {
            x: pageX - startX,
            y: pageY - startY,
            type: "up",
            t: (new Date().getTime() - startTime.getTime())
        }
    
        trackArr.push(track);
        printLog("up", track)
        valid(currentCaptchaConfig);
    }
    
  • 通用css 样式

    common.css 通用样式

    .slider {
        background-color: #fff;
        width: 278px;
        height: 285px;
        z-index: 999;
        box-sizing: border-box;
        padding: 9px;
        border-radius: 6px;
        box-shadow: 0 0 11px 0 #999999;
    }
    
    .slider .content {
        width: 100%;
        height: 159px;
        position: relative;
    }
    
    .bg-img-div {
        width: 100%;
        height: 100%;
        position: absolute;
        transform: translate(0px, 0px);
    }
    
    .slider-img-div {
        height: 100%;
        position: absolute;
        transform: translate(0px, 0px);
    }
    
    .bg-img-div img {
        width: 100%;
    }
    
    .slider-img-div img {
        height: 100%;
    }
    
    .slider .slider-move {
        height: 60px;
        width: 100%;
        margin: 11px 0;
        position: relative;
    }
    
    .slider .bottom {
        height: 19px;
        width: 100%;
    }
    
    .refresh-btn, .close-btn, .slider-move-track, .slider-move-btn {
        background: url(https://static.geetest.com/static/ant/sprite.1.2.4.png) no-repeat;
    }
    
    .refresh-btn, .close-btn {
        display: inline-block;
    }
    
    .slider-move .slider-move-track {
        line-height: 38px;
        font-size: 14px;
        text-align: center;
        white-space: nowrap;
        color: #88949d;
        -moz-user-select: none;
        -webkit-user-select: none;
        user-select: none;
    }
    
    .slider {
        user-select: none;
    }
    
    .slider-move .slider-move-btn {
        transform: translate(0px, 0px);
        background-position: -5px 11.79625%;
        position: absolute;
        top: -12px;
        left: 0;
        width: 66px;
        height: 66px;
    }
    
    .slider-move-btn:hover, .close-btn:hover, .refresh-btn:hover {
        cursor: pointer
    }
    
    .bottom .close-btn {
        width: 20px;
        height: 20px;
        background-position: 0 44.86874%;
    }
    
    .bottom .refresh-btn {
        width: 20px;
        height: 20px;
        background-position: 0 81.38425%;
    }
    

2.2 滑动验证码

  • html

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>滑动验证码</title>
        <link rel="stylesheet" type="text/css" href="common.css">
    </head>
    
    <body>
    <div class="slider">
        <div class="content">
            <div class="bg-img-div">
                <img id="bg-img" src="" alt/>
            </div>
            <div class="slider-img-div" id="slider-img-div">
                <img id="slider-img" src="" alt/>
            </div>
        </div>
        <div class="slider-move">
            <div class="slider-move-track">
                拖动滑块完成拼图
            </div>
            <div class="slider-move-btn" id="slider-move-btn"></div>
        </div>
        <div class="bottom">
            <div class="close-btn" id="slider-close-btn"></div>
            <div class="refresh-btn" id="slider-refresh-btn"></div>
        </div>
    </div>
    <script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
    <script src="index.js"></script>
    <script type="text/javascript" src="slider.js"></script>
    </body>
    </html>
    
  • slider.js

    let currentCaptchaId = null;
    $(function () {
        clearAllPreventDefault($(".slider"));
        refreshCaptcha();
    })
    
    $("#slider-move-btn").mousedown(down);
    $("#slider-move-btn").on("touchstart", down);
    
    $("#slider-close-btn").click(() => {
    });
    
    $("#slider-refresh-btn").click(() => {
        refreshCaptcha();
    });
    
    function valid(captchaConfig) {
    
        let data = {
            'id' : currentCaptchaId,
            'captchaTrack': {
                bgImageWidth: captchaConfig.bgImageWidth,
                bgImageHeight: captchaConfig.bgImageHeight,
                sliderImageWidth: captchaConfig.sliderImageWidth,
                sliderImageHeight: captchaConfig.sliderImageHeight,
                startSlidingTime: captchaConfig.startTime,
                endSlidingTime: captchaConfig.stopTime,
                trackList: captchaConfig.trackArr
            }
        }
       
        $.ajax({
            type:"POST",
            url:"http://localhost:8080/check",
            contentType: "application/json", 
            dataType:"json",
            data:JSON.stringify(data),
            success:function (res) {
                if (res) {
                    alert("验证成功!!!");
                }
                refreshCaptcha();
            }
        })
    }
    
    function refreshCaptcha() {
        $.get("http://localhost:8080/index?type=SLIDER", function (data) {
            reset();
            currentCaptchaId = data.id;
            const bgImg = $("#bg-img");
            const sliderImg = $("#slider-img");
            bgImg.attr("src", data.captcha.backgroundImage);
            sliderImg.attr("src", data.captcha.sliderImage);
            initConfig(bgImg.width(), bgImg.height(), sliderImg.width(), sliderImg.height(), 206);
        })
    }
    
    function doDown() {
        $("#slider-move-btn").css("background-position", "-5px 31.0092%")
    }
    
    function doMove(currentCaptchaConfig) {
        const moveX = currentCaptchaConfig.moveX;
        $("#slider-move-btn").css("transform", "translate(" + moveX + "px, 0px)")
        $("#slider-img-div").css("transform", "translate(" + moveX + "px, 0px)")
    }
    function reset() {
        $("#slider-move-btn").css("background-position", "-5px 11.79625%")
        $("#slider-move-btn").css("transform", "translate(0px, 0px)")
        $("#slider-img-div").css("transform", "translate(0px, 0px)")
        currentCaptchaId = null;
    }
    
  • 最终结果

    滑动验证码

2.3 旋转验证码

  • html

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>旋转验证码</title>
        <link rel="stylesheet" type="text/css" href="common.css">
        <style>
            .after {
                color: #88949d;
            }
    
            .rotate-img-div {
                height: 100%;
                position: absolute;
                transform: rotate(0deg);
                margin-left: 50px;
            }
    
            .rotate-img-div img {
                height: 100%;
            }
        </style>
    </head>
    
    <body>
    <div class="slider rotate">
        <div class="content">
            <div class="bg-img-div">
                <img id="rotate-bg-img" src="" alt/>
            </div>
            <div class="rotate-img-div">
                <img id="rotate-image" src="" alt/>
            </div>
        </div>
        <div class="slider-move">
            <div class="slider-move-track">
                拖动滑块旋转正确位置
            </div>
            <div class="slider-move-btn" id="rotate-move-btn"></div>
        </div>
        <div class="bottom">
            <div class="close-btn" id="rotate-close-btn"></div>
            <div class="refresh-btn" id="rotate-refresh-btn"></div>
        </div>
    </div>
    <script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
    <script src="index.js"></script>
    <script src="rotate.js"></script>
    </body>
    </html>
    
  • rotate.js

    $(function () {
        refreshCaptcha();
        clearAllPreventDefault($(".slider"))
    })
    
    //  旋转
    let currentCaptchaId = null;
    function refreshCaptcha() {
        $.get("http://localhost:8080/index?type=ROTATE", function (data) {
            reset();
            currentCaptchaId = data.id;
            const bgImg = $("#rotate-bg-img");
            const sliderImg = $("#rotate-image");
            bgImg.attr("src", data.captcha.backgroundImage);
            sliderImg.attr("src", data.captcha.sliderImage);
            initConfig(206, bgImg.height(), sliderImg.width(), sliderImg.height(), 206);
        })
    }
    
    $("#rotate-move-btn").mousedown(down);
    $("#rotate-move-btn").on("touchstart", down);
    
    function doDown() {
        $("#slider-move-btn").css("background-position", "-5px 31.0092%")
    }
    
    
    function doMove(currentCaptchaConfig) {
        const moveX = currentCaptchaConfig.moveX;
        $("#rotate-move-btn").css("transform", "translate(" + moveX + "px, 0px)")
        $(".rotate-img-div").css("transform", "rotate(" + (moveX / (currentCaptchaConfig.end / 360)) + "deg)")
    }
    
    function valid(captchaConfig) {
    
        let data = {
            bgImageWidth: captchaConfig.bgImageWidth,
            bgImageHeight: captchaConfig.bgImageHeight,
            sliderImageWidth: captchaConfig.sliderImageWidth,
            sliderImageHeight: captchaConfig.sliderImageHeight,
            startSlidingTime: captchaConfig.startTime,
            endSlidingTime: captchaConfig.stopTime, // 官方demo 这里有个语法错误
            trackList: captchaConfig.trackArr
        };
        
        let sendData = {
            'id' : currentCaptchaId,
            'captchaTrack': data
        }
        $.ajax({
            type:"POST",
            url:"http://localhost:8080/check",
            contentType: "application/json", 
            dataType:"json",
            data:JSON.stringify(sendData),
            success:function (res) {
                if (res) {
                    alert("验证成功!!!");
                }
                refreshCaptcha();
            }
        })
    }
    
    $("#slider-close-btn").click(() => {
    });
    
    $("#rotate-refresh-btn").click(() => {
        refreshCaptcha();
    });
    
    function reset() {
        $("#rotate-move-btn").css("background-position", "-5px 11.79625%")
        $("#rotate-move-btn").css("transform", "translate(0px, 0px)")
        $(".rotate-img-div").css("transform", "rotate(0deg)")
        currentCaptchaId = null;
    }
    
  • 最终结果

    旋转验证码

2.4 滑动还原验证码

  • html

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>滑动还原验证码</title>
        <link rel="stylesheet" type="text/css" href="common.css">
        <style>
            .bg-img-div {
                width: 100%;
                height: 100%;
                position: absolute;
                transform: translate(0px, 0px);
                background-size: 100% 159px;
                background-image: none;
                background-position: 0 0;
                z-index: 0;
    
            }
    
            .slider-img-div {
                height: 100%;
                width: 100%;
                background-size: 100% 159px;
                position: absolute;
                transform: translate(0px, 0px);
                /*border-bottom: 1px solid blue;*/
                z-index: 1;
            }
        </style>
    </head>
    
    <body>
    <div class="slider">
        <div class="content">
            <div class="slider-img-div" id="slider-img-div">
                <img id="slider-img" src="" alt/>
            </div>
            <div class="bg-img-div">
            </div>
        </div>
        <div class="slider-move">
            <div class="slider-move-track">
                拖动滑块完成拼图
            </div>
            <div class="slider-move-btn" id="slider-move-btn"></div>
        </div>
        <div class="bottom">
            <div class="close-btn" id="slider-close-btn"></div>
            <div class="refresh-btn" id="slider-refresh-btn"></div>
        </div>
    </div>
    <script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
    <script src="index.js"></script>
    <script src="concat.js"></script>
    </body>
    </html>
    
    
  • js

    var currentCaptchaId;
    $(function () {
        refreshCaptcha();
        clearAllPreventDefault($(".slider"));
    })
    
    $("#slider-move-btn").mousedown(down);
    $("#slider-move-btn").on("touchstart", down);
    
    function doDown() {
        $("#slider-move-btn").css("background-position", "-5px 31.0092%")
    }
    
    function doMove(config) {
        const moveX = config.moveX;
        $("#slider-move-btn").css("transform", "translate(" + moveX + "px, 0px)")
        $("#slider-img-div").css("background-position-x", moveX + "px");
    }
    
    $("#slider-close-btn").click(() => {
    });
    
    $("#slider-refresh-btn").click(() => {
        refreshCaptcha();
    });
    
    function valid(captchaConfig) {
    
        let data = {
            'id' : currentCaptchaId,
            'captchaTrack': {
                bgImageWidth: captchaConfig.bgImageWidth,
                bgImageHeight: captchaConfig.bgImageHeight,
                sliderImageWidth: captchaConfig.sliderImageWidth,
                sliderImageHeight: captchaConfig.sliderImageHeight,
                startSlidingTime: captchaConfig.startTime,
                endSlidingTime: captchaConfig.stopTime,
                trackList: captchaConfig.trackArr
            }
        }
       
        $.ajax({
            type:"POST",
            url:"http://localhost:8080/check",
            contentType: "application/json", 
            dataType:"json",
            data:JSON.stringify(data),
            success:function (res) {
                if (res) {
                    alert("验证成功!!!");
                }
                refreshCaptcha();
            }
        })
    }
    
    function refreshCaptcha() {
        $.get("http://localhost:8080/index?type=CONCAT", function (data) {
            reset();
            currentCaptchaId = data.id;
            const bgImg = $(".bg-img-div");
            const sliderImg = $(".slider-img-div");
    
            bgImg.css("background-image", "url(" + data.captcha.backgroundImage + ")");
            sliderImg.css("background-image", "url(" + data.captcha.backgroundImage + ")");
            sliderImg.css("background-position", "0px 0px");
            var backgroundImageHeight = data.captcha.backgroundImageHeight;
            var height = ((backgroundImageHeight - data.captcha.data) / backgroundImageHeight) * 159;
            $(".slider-img-div").css("height", height);
    
            initConfig(bgImg.width(), bgImg.height(), sliderImg.width(), sliderImg.height(), 206);
        })
    }
    
    function reset() {
        $("#slider-move-btn").css("background-position", "-5px 11.79625%")
        $("#slider-move-btn").css("transform", "translate(0px, 0px)")
        $("#slider-img-div").css("transform", "translate(0px, 0px)")
        currentCaptchaId = null;
    }
    
  • 最终结果

滑动还原验证码

2.5 文字点选验证码

  • html

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>文字点选验证码</title>
        <link rel="stylesheet" type="text/css" href="common.css">
        <style>
            .tip-img {
                width: 130px;
                position: absolute;
                right: 5px;
            }
    
            .slider-move-span {
                font-size: 18px;
                display: inline-block;
                height: 40px;
                line-height: 40px;
            }
    
            .click-span {
                position: absolute;
                left: 0;
                top: 0;
                border-radius: 50px;
                background-color: #409eff;
                width: 20px;
                height: 20px;
                text-align: center;
                line-height: 20px;
                color: #fff;
                border: 2px solid #fff;
            }
    
            .submit-btn {
                height: 40px;
                width: 120px;
                line-height: 40px;
                text-align: center;
                background-color: #409eff;
                color: #fff;
                font-size: 15px;
                box-sizing: border-box;
                border: 1px solid #409eff;
                float: right;
                border-radius: 5px;
            }
        </style>
    </head>
    
    <body>
    <div class="slider">
        <div class="slider-move">
            <span class="slider-move-span">请依次点击:</span><img src="" class="tip-img">
        </div>
        <div class="content">
            <div class="bg-img-div">
                <img id="bg-img" src="" alt/>
            </div>
            <div class="bg-click-div">
            </div>
        </div>
        <div class="bottom">
            <div class="close-btn" id="slider-close-btn"></div>
            <div class="refresh-btn" id="slider-refresh-btn"></div>
        </div>
    </div>
    <script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
    <script src="word-click.js"></script>
    </body>
    </html>
    
  • js

    let start = 0;
    let startY = 0;
    let currentCaptchaId = null;
    let movePercent = 0;
    const bgImgWidth = $(".bg-img-div").width();
    let end = 206;
    let startSlidingTime;
    let entSlidingTime;
    const trackArr = [];
    let clickCount = 0;
    $(function () {
        refreshCaptcha();
    })
    $(".content").click(function (event) {
        console.log(event);
        clickCount++;
        if (clickCount === 1) {
            startSlidingTime = new Date();
            // move 轨迹
            window.addEventListener("mousemove", move);
        }
        trackArr.push({
            x: event.offsetX,
            y: event.offsetY,
            type: "click",
            t: (new Date().getTime() - startSlidingTime.getTime())
        });
        const left = event.offsetX - 10;
        const top = event.offsetY - 10;
        $(".bg-click-div").append("<span class='click-span' style='left:" + left + "px;top: " + top + "px'>" + clickCount + "</span>")
        if (clickCount === 4) {
            // 校验
            entSlidingTime = new Date();
            window.removeEventListener("mousemove", move);
            valid();
        }
    });
    
    function move(event) {
        if (event instanceof TouchEvent) {
            event = event.touches[0];
        }
        console.log("x:", event.offsetX, "y:", event.offsetY, "time:" ,new Date().getTime() - startSlidingTime.getTime());
        trackArr.push({x: event.offsetX, y:event.offsetY, t: (new Date().getTime() - startSlidingTime.getTime()), type: "move"});
    }
    
    
    $("#slider-close-btn").click(() => {
    });
    
    $("#slider-refresh-btn").click(() => {
        refreshCaptcha();
    });
    
    function valid() {
       
        let data = {
            'id': currentCaptchaId,
            'captchaTrack': {
                bgImageWidth: $(".bg-img-div").width(),
                bgImageHeight: $(".content").height(),
                sliderImageWidth: -1,
                sliderImageHeight: -1,
                startSlidingTime: startSlidingTime,
                entSlidingTime: entSlidingTime,
                trackList: trackArr
            }
        };
        
        $.ajax({
            type:"POST",
            url:"http://localhost:8080/check",
            contentType: "application/json", 
            dataType:"json",
            data:JSON.stringify(data),
            success:function (res) {
                if (res) {
                    alert("验证成功!!!");
                }
                refreshCaptcha();
            }
        })
    }
    
    function refreshCaptcha() {
        $.get("http://localhost:8080/index?type=WORD_IMAGE_CLICK", function (data) {
            reset();
            currentCaptchaId = data.id;
            $("#bg-img").attr("src", data.captcha.backgroundImage);
            $("#slider-img").attr("src", data.captcha.sliderImage);
            $(".tip-img").attr("src", data.captcha.sliderImage);
        })
    }
    
    function reset() {
        $("#slider-move-btn").css("background-position", "-5px 11.79625%")
        $("#slider-move-btn").css("transform", "translate(0px, 0px)")
        $("#slider-img-div").css("transform", "translate(0px, 0px)")
        start = 0;
        startSlidingTime = null;
        entSlidingTime = null;
        trackArr.length = 0;
        $(".bg-click-div span").remove();
        clickCount = 0;
        movePercent = 0;
        currentCaptchaId = null;
        startY = 0;
        window.removeEventListener("mousemove", move);
    }
    
  • 最终结果

    文字点选验证码

三、源码探秘和总结

3.1 前端代码

前端代码部分主要是传递用户鼠标数据给后端,可以根据滑动和点选分为两种

  • 滑动

    滑动数据主要逻辑部分就在通用js代码中,主要就是三个函数,用户鼠标按下,移动,抬起三种操作的监听函数

  • 点选

    点选和滑动不一样,需要记录点击4个汉字的坐标,然后传递给后端

3.2 后端代码

下面会对上述验证码后端生成,校验和存储进行源码探秘

  • 滑块验证码

    滑块验证码的图片生成部分可以见:StandardSliderImageCaptchaGenerator.doGenerateCaptchaImage方法,随机从模板获取背景图片,再用滑块的图片,选择随机x,y坐标覆盖掉

    // 获取随机的 x 和 y 轴
    int randomX = ThreadLocalRandom.current().nextInt(fixedTemplate.getWidth() + 5, targetBackground.getWidth() - fixedTemplate.getWidth() - 10);
    int randomY = ThreadLocalRandom.current().nextInt(targetBackground.getHeight() - fixedTemplate.getHeight());
    
    CaptchaImageUtils.overlayImage(targetBackground, fixedTemplate, randomX, randomY);
    if (obfuscate) {
        // 加入混淆滑块
        int obfuscateX = randomObfuscateX(randomX, fixedTemplate.getWidth(), targetBackground.getWidth());
        CaptchaImageUtils.overlayImage(targetBackground, fixedTemplate, obfuscateX, randomY);
    }
    BufferedImage cutImage = CaptchaImageUtils.cutImage(cutBackground, fixedTemplate, randomX, randomY);
    CaptchaImageUtils.overlayImage(cutImage, activeTemplate, 0, 0);
    CaptchaImageUtils.overlayImage(matrixTemplate, cutImage, 0, randomY);
    return wrapSliderCaptchaInfo(randomX, randomY, targetBackground, matrixTemplate, param);
    
  • 旋转验证码

    生成部分可以见:StandardRotateImageCaptchaGenerator.doGenerateCaptchaImage 方法,选择居中部分抠出旋转随机角度

    // 算出居中的x和y
    int x = targetBackground.getWidth() / 2 - fixedTemplate.getWidth() / 2;
    int y = targetBackground.getHeight() / 2 - fixedTemplate.getHeight() / 2;
    CaptchaImageUtils.overlayImage(targetBackground, fixedTemplate, x, y);
    // 抠图部分
    BufferedImage cutImage = CaptchaImageUtils.cutImage(cutBackground, fixedTemplate, x, y);
    CaptchaImageUtils.overlayImage(cutImage, activeTemplate, 0, 0);
    // 随机旋转抠图部分
    // 随机x, 转换为角度
    int randomX = ThreadLocalRandom.current().nextInt(fixedTemplate.getWidth() + 10, targetBackground.getWidth() - 10);
    double degree = 360d - randomX / ((targetBackground.getWidth()) / 360d);
    CaptchaImageUtils.centerOverlayAndRotateImage(matrixTemplate, cutImage, degree);
    return wrapRotateCaptchaInfo(degree, randomX, targetBackground, matrixTemplate, param);
    
  • 滑块还原验证码

    生成部分可以见:StandardConcatImageCaptchaGenerator.doGenerateCaptchaImage 方法, 选择1/4-3/4高度随机值作为y坐标,这是切割部分,将图片切断成两部分,上面是可滑动的。1/8-4/5宽度随机值作为x坐标,这是x轴分隔点

    Resource resourceImage = imageCaptchaResourceManager.randomGetResource(param.getType());
    InputStream resourceInputStream = imageCaptchaResourceManager.getResourceInputStream(resourceImage);
    inputStreams.add(resourceInputStream);
    BufferedImage bgImage = wrapFile2BufferedImage(resourceInputStream);
    int spacingY = bgImage.getHeight() / 4;
    int randomY = ThreadLocalRandom.current().nextInt(spacingY, bgImage.getHeight() - spacingY);
    BufferedImage[] bgImageSplit = splitImage(randomY, true, bgImage);
    int spacingX = bgImage.getWidth() / 8;
    int randomX = ThreadLocalRandom.current().nextInt(spacingX, bgImage.getWidth() - bgImage.getWidth() / 5);
    BufferedImage[] bgImageTopSplit = splitImage(randomX, false, bgImageSplit[0]);
    
    BufferedImage sliderImage = concatImage(true,
                                            bgImageTopSplit[0].getWidth()
                                            + bgImageTopSplit[1].getWidth(), bgImageTopSplit[0].getHeight(), bgImageTopSplit[1], bgImageTopSplit[0]);
    bgImage = concatImage(false, bgImageSplit[1].getWidth(), sliderImage.getHeight() + bgImageSplit[1].getHeight(),
                          sliderImage, bgImageSplit[1]);
    return wrapConcatCaptchaInfo(randomX, randomY, bgImage, param);
    
  • 文字点选验证码

    通用生成部分可以见:AbstractClickImageCaptchaGenerator.doGenerateCaptchaImage 方法, 具体文字生成部分可以见StandardRandomWordClickImageCaptchaGenerator.genTipImage。

    生成的文字个数由变量 checkClickCount=4控制,已在代码中写死,后面是否可以做成配置?

    List<ClickImageCheckDefinition> clickImageCheckDefinitionList = new ArrayList<>(interferenceCount);
    int allImages = interferenceCount + checkClickCount;
    int avg = bgImage.getWidth() / allImages;
    List<String> imgTips = randomGetClickImgTips(allImages);
    if (allImages < imgTips.size()) {
        throw new IllegalStateException("随机生成点击图片小于请求数量, 请求生成数量=" + allImages + ",实际生成数量=" + imgTips.size());
    }
    for (int i = 0; i < allImages; i++) {
        // 随机获取点击图片
        ImgWrapper imgWrapper = getClickImg(imgTips.get(i));
        BufferedImage image = imgWrapper.getImage();
        int clickImgWidth = image.getWidth();
        int clickImgHeight = image.getHeight();
        // 随机x
        int randomX;
        if (i == 0) {
            randomX = 1;
        } else {
            randomX = avg * i;
        }
        // 随机y
        int randomY = ThreadLocalRandom.current().nextInt(10, bgImage.getHeight() - clickImgHeight);
        // 通过随机x和y 进行覆盖图片
        CaptchaImageUtils.overlayImage(bgImage, imgWrapper.getImage(), randomX, randomY);
        ClickImageCheckDefinition clickImageCheckDefinition = new ClickImageCheckDefinition();
        clickImageCheckDefinition.setTip(imgWrapper.getTip());
        clickImageCheckDefinition.setX(randomX + clickImgWidth / 2);
        clickImageCheckDefinition.setY(randomY + clickImgHeight / 2);
        clickImageCheckDefinition.setWidth(clickImgWidth);
        clickImageCheckDefinition.setHeight(clickImgHeight);
        clickImageCheckDefinitionList.add(clickImageCheckDefinition);
    }
    List<ClickImageCheckDefinition> checkClickImageCheckDefinitionList = getCheckClickImageCheckDefinitionList(clickImageCheckDefinitionList,checkClickCount);
    return wrapClickImageCaptchaInfo(param, bgImage, checkClickImageCheckDefinitionList);
    
    
  • 校验验证码

    校验部分可以根据滑动还是点选分为两种

    • 滑块

      源码主要实现部分位于SimpleImageCaptchaValidator.doValidSliderCaptcha方法,目前只校验最后一个轨迹是否到达缺口处,没有对所有轨迹进行行为校验

      public boolean doValidSliderCaptcha(ImageCaptchaTrack imageCaptchaTrack,
                                          Map<String, Object> sliderCaptchaValidData,
                                          Float tolerant,
                                          String type) {
          Float oriPercentage = getFloatParam(PERCENTAGE_KEY, sliderCaptchaValidData);
          if (oriPercentage == null) {
              // 没读取到百分比
              return false;
          }
          List<ImageCaptchaTrack.Track> trackList = imageCaptchaTrack.getTrackList();
          // 取最后一个滑动轨迹
          ImageCaptchaTrack.Track lastTrack = trackList.get(trackList.size() - 1);
          // 计算百分比
          float calcPercentage = calcPercentage(lastTrack.getX(), imageCaptchaTrack.getBgImageWidth());
          // 校验百分比
          return checkPercentage(calcPercentage, oriPercentage, tolerant);
      }
      
    • 点选

      源码实现部分位于SimpleImageCaptchaValidator.doValidClickCaptcha方法,可以看到按照顺序对点选轨迹依次进行XY坐标进行百分比校验,默认有2%的容错

      public boolean doValidClickCaptcha(ImageCaptchaTrack imageCaptchaTrack,
                                         Map<String, Object> sliderCaptchaValidData,
                                         Float tolerant,
                                         String type) {
          String validStr = getStringParam(PERCENTAGE_KEY, sliderCaptchaValidData, null);
          if (ObjectUtils.isEmpty(validStr)) {
              return false;
          }
          String[] splitArr = validStr.split(";");
          List<ImageCaptchaTrack.Track> trackList = imageCaptchaTrack.getTrackList();
          if (trackList.size() < splitArr.length) {
              return false;
          }
          // 取出点击事件的轨迹数据
          List<ImageCaptchaTrack.Track> clickTrackList = trackList
              .stream()
              .filter(t -> TrackTypeConstant.CLICK.equalsIgnoreCase(t.getType()))
              .collect(Collectors.toList());
          if (clickTrackList.size() != splitArr.length) {
              return false;
          }
          for (int i = 0; i < splitArr.length; i++) {
              ImageCaptchaTrack.Track track = clickTrackList.get(i);
              String posStr = splitArr[i];
              String[] posArr = posStr.split(",");
              float xPercentage = Float.parseFloat(posArr[0]);
              float yPercentage = Float.parseFloat(posArr[1]);
      
              float calcXPercentage = calcPercentage(track.getX(), imageCaptchaTrack.getBgImageWidth());
              float calcYPercentage = calcPercentage(track.getY(), imageCaptchaTrack.getBgImageHeight());
      
              if (!checkPercentage(calcXPercentage, xPercentage, tolerant)
                  || !checkPercentage(calcYPercentage, yPercentage, tolerant)) {
                  return false;
              }
          }
          return true;
      }
      
  • 存储部分

    验证码存储部分主要接口为 CacheStore,如果项目中引入了redis ,那么会使用redis来存储,否则会使用本地存储

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass(StringRedisTemplate.class)
    @Import({RedisAutoConfiguration.class})
    @AutoConfigureAfter({RedisAutoConfiguration.class})
    public static class RedisCacheStoreConfiguration {
    
        @Bean
        @ConditionalOnBean(StringRedisTemplate.class)
        @ConditionalOnMissingBean(CacheStore.class)
        public CacheStore redis(StringRedisTemplate redisTemplate) {
            return new RedisCacheStore(redisTemplate);
        }
    }
    
    @Configuration(proxyBeanMethods = false)
    @AutoConfigureAfter({RedisCacheStoreConfiguration.class})
    @Import({RedisCacheStoreConfiguration.class})
    public static class LocalCacheStoreConfiguration {
    
        @Bean
        @ConditionalOnMissingBean(CacheStore.class)
        public CacheStore local() {
            return new LocalCacheStore();
        }
    }
    

3.3 总结

  • 目前这个库只提供4种行为验证码,不过在后端源码验证码类型CaptchaTypeConstant 中发现了图片点选的常量,后续版本应该会加上图片点选的验证码,这种很常见,使用梯子访问谷歌时经常会碰到这种,要用户选择红绿灯、摩托车等等。
  • 最后,感谢作者将如此棒的库开源,已 starred

参考

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/100064.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java根据ip地址获取归属地

最近&#xff0c;各大平台都新增了评论区显示发言者ip归属地的功能&#xff0c;例如哔哩哔哩&#xff0c;微博&#xff0c;知乎等等。 下面&#xff0c;我就来讲讲&#xff0c;Java 中是如何获取 IP 属地的&#xff0c;主要分为以下几步 通过 HttpServletRequest 对象&#xf…

JVM监控及诊断工具-GUI

1. 工具概述 使用上一章命令行工具或组合能帮您获取目标Java应用性能相关的基础信息&#xff0c;但它们存在下列局限: 1)无法获取方法级别的分析数据&#xff0c;如方法间的调用关系、各方法的调用次数和调用时间 等&#xff08;这对定位应用性能瓶颈至关重要&#xff09;。 2…

【MybatisPlus】最全面的MybatisPlus通关教程

前言 本文为最全面的MybatisPlus通关教程相关介绍&#xff0c;下边将对MyBatisPlus概述&#xff0c;MyBatisPlus快速入门&#xff0c;CRUD扩展&#xff08;包括&#xff1a;插入操作、主键生成策略、更新操作&#xff0c;自动填充&#xff0c;乐观锁&#xff0c;查询操作&#…

我学python的那段日子(四)选择结构和循环结构

1.选择结构 总所周知&#xff0c;Java里面也有选择结构&#xff0c;分别是if单分支选择结构、if-else结构多重if结构和switch结构&#xff0c;同样的&#xff0c;Python里也有选择结构&#xff0c;分别是是if单分支选择结构、if-else结构多重if结构。 1.1 if选择结果 ​ if选…

Servlet生命周期和线程安全

✅作者简介&#xff1a;热爱国学的Java后端开发者&#xff0c;修心和技术同步精进。 &#x1f34e;个人主页&#xff1a;Java Fans的博客 &#x1f34a;个人信条&#xff1a;不迁怒&#xff0c;不贰过。小知识&#xff0c;大智慧。 &#x1f49e;当前专栏&#xff1a;JAVA开发者…

CANoe-是如何对ECU和网络进行测试的

CANoe工具是汽车仿真测试开发中使用最广泛的工具 Test Module/Test Unit 除了仿真模块和分析模块,CANoe还引入了另一个组件进行测试:Test Module(测试模块)和Test Unit(测试单元) test module调用的是capl脚本、.net脚本或xml文件,而test unit用的是vTESTstudio插件 实…

12.18

一.加强练习promise 1.fs模块 const fs require(fs) const p new Promise((resolve,reject) > {fs.readFile(./resource/context.txt, (err,data) > {if (err) reject(err)resolve(data)}) }) p.then((value) > {console.log(value.toString()) }, (reason) > …

前端炫酷特效合集

我们经常在抖音上看到一些前端很酷的特效&#xff0c;诸如&#xff1a;快叫你学编程的朋友给你写一个圣诞树&#xff0c;看着是不是很酷炫呢&#xff1f;其实只要有源码&#xff0c;你也可以拥有哦&#xff01; 跟大家分享多款前端特效源码&#xff0c;需要的朋友可以去在这里…

Java : 多态,包装类的面试题:

Java &#xff1a; 多态&#xff0c;包装类的面试题&#xff1a; 每博一文案 有人说&#xff1a;小孩子才会仰天大哭&#xff0c;成年人的世界早就调成了静音模式&#xff0c;连崩溃也很懂事。 一路走来&#xff0c;我们一边受伤&#xff0c;也一边变得更坚强&#xff0c;慢慢…

数据库原理及MySQL应用 | 日志管理

数据库系统管理维护阶段需要通过日志对数据库的性能进行监督、分析和改进。 日志是数据库系统的重要组成部分&#xff0c;记录了数据库的运行状态、数据的变更历史、错误信息及用户操作等信息。在日常管理中&#xff0c;数据库管理员可通过日志监控数据库的运行状态、优化数据库…

国内外文献镜像网站

一、常用的国内外文献镜像网站 维普网&#xff1a;http://lib.cqvip.com/ idata&#xff1a;https://www.cn-ki.net/ 独秀&#xff1a;https://www.duxiu.com/ 中国知网&#xff1a;https://www.cnki.net/ 龙源期刊网&#xff1a;http://www.qikan.com.cn/ 万方数据&#xff1…

计算机毕业设计HTML+CSS+JavaScript仿大型购物商城(1页)

常见网页设计作业题材有 个人、 美食、 公司、 学校、 旅游、 电商、 宠物、 电器、 茶叶、 家居、 酒店、 舞蹈、 动漫、 服装、 体育、 化妆品、 物流、 环保、 书籍、 婚纱、 游戏、 节日、 戒烟、 电影、 摄影、 文化、 家乡、 鲜花、 礼品、 汽车、 其他等网页设计题目, A…

毕业设计 单片机GPS定位位置记录系统 - 物联网 嵌入式 stm32

文章目录0 前言1 简介2 主要器件3 实现效果4 硬件设计概述硬件模块的连接**AB32VG1主控MCU**5 软件说明6 部分核心代码7 最后0 前言 &#x1f525; 这两年开始毕业设计和毕业答辩的要求和难度不断提升&#xff0c;传统的毕设题目缺少创新和亮点&#xff0c;往往达不到毕业答辩…

基于asp.net人力资源管理系统-计算机毕业设计

基于asp.net的人力资源管理的内容和人事管理日常管理所遇到的问题&#xff0c;整个系统可以分为下面几个子模块&#xff0c;分别为员工档案信息管理、工资信息管理、考勤信息管理、消息信息管理、账号信息管理等几个大的功能模块,开发环境:Visual studio,sqlserver数据库.资料有…

非零基础自学Golang 第9章 结构体 9.4 初始化结构体

非零基础自学Golang 文章目录非零基础自学Golang第9章 结构体9.4 初始化结构体9.4.1 键值对格式初始化9.4.2 列表格式初始化第9章 结构体 9.4 初始化结构体 上一小节我们讲解了在结构体实例化后&#xff0c;再使用“.”的方式对成员变量进行赋值。另外&#xff0c;我们还可以…

Junit5 架构、新特性及基本使用(常用注解与套件执行)

什么是 Junit5&#xff0c;在 Junit5 的官方介绍文档中这写到&#xff1a; Junit5 由JUnit Platform JUnit Jupiter JUnit Vintage3部分构成&#xff0c;借用 IBM Developer 的一张图来说明 JUnit 5 的架构&#xff1a; JUnit Platform: JUnit Jupiter: JUnit Vintage: 嵌套单…

Spring Cloud 总结 - 调用远程服务的三种方式及原理分析

一个简单的微服务架构图 本文设计的 Spring Cloud 版本以及用到的 Spring Cloud 组件 Spring Cloud Hoxton.SR5eurekafeignribbon 后面的内容都将围绕上面的图来分析. 调用远程服务的三种方式 在 Spring Cloud 服务架构中, 一个服务可能部署多个实例, 通常情况下, 这个时候…

好玩的网站安利

好玩的网站安利 1、EGOUZ https://www.egouz.com/ 授人以鱼不如授人以渔&#xff0c;先给大家分享两个可以找国外优质网站的地方。EGOUZ是一个高质量的导航站&#xff0c;内容主要是外国网站&#xff0c;并且做了很详细的分类&#xff0c;网站资源非常丰富。 EGOUZ按照国家分…

50450-80-1,Ala-Ala-Ala-对硝基苯胺

AAA-pNA, chromogenic substrate for porcine pancreatic elastase and for astacin, a crayfish zinc-endopeptidase.3a - pna&#xff0c;猪胰腺弹性酶和阿斯塔星的显色底物&#xff0c;阿斯塔星是一种小龙虾锌内肽酶。 编号: 163840中文名称: 三肽Pancreatic elastase subst…

1573_AURIX_TC275_SCU中的急停功能以及overlay

全部学习汇总&#xff1a; GreyZhang/g_TC275: happy hacking for TC275! (github.com) 急停功能是不需要软件进行干预的&#xff0c;至少&#xff0c;这个事件的动作触发是不需要软件进行干预的。急停事件的触发因素&#xff1a;外部的输入事件、SMU的警报信息。外部的输入事件…