前后端滑块校验

前端

1.创建一个滑块组件slider

点击查看代码
<template>
    <div class="slider">
      <div class="mask">
        <div class="container">
          <div class="title">
            <div class="text">
              <span>请完成下列验证后继续</span>
            </div>
            <div class="button-group">
              <img
                src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAE2UlEQVRoQ+1YXWwUVRT+zsw2Qktn1ljatEjjD+4WJILpg0rE4ItGn0QMyGMTE/kJTUwFElt2ZmopQWhIMNjyRKIPiH/hScXEiJEgL/4mamcwUSG2UCvuzAJt7e4cc6ddFaTMHTq70qTztnvPPef7zt899xJm+EczHD9mCfzfEZyNQNwRaD7IFSPnLy/FbZWnv99MF8P033QRSFvuDmbuJOBL20w2zzwCpnuEwWtnLIGU6X4O8IMEHLXN5OqyRcBgVg7vzK1AwX+KGM0ANQDcwAQVwAVi+oWAU6Ti46Vp7djba6lwLXBp0/2VwQ0getUx9NaSE1h1iOcMns21ss9tANeGGRTrBDrHCnrnzNV6vt1Kl4p7RAHnBr1RgBVFUbb2Z7S9YfqmVcRNVm6Nj8I+MBYGwAiXmHFMIXzkg85UkDpQQD7BwN0M3AWfloGwGsy3FIlARYu9Q/9Q/G7aNXKHPzb2U7CmYJ2dSb5VEgLMTGkr1wX4LwUGiM4T2Kpv1A8db6HR6xm9vzs3f2S88JwPbAZjAYgKBN5iG8nexV25Rwr5wqdiv5qgh37o0E/FTkCAb+r0DjPzukkv9qnzta0yPfvfYB7Yz9ofF9xDAJ4W/ytE+3wVXyHPrwd651YssLdXDcROIGV6O4XnCZQHcavw3PWMLOn0VvrghUQ8xJw4n1AxtPiequFiETdZXpsP3g1mlUAnGbyCiMafzWhzLCI/VgJBznPhncl83xQGvvkgV14852WZueJKIOSD8DuAIYIgRrcCvLwoQ8DPtpm8Mwz8RAZIfqLbDJxxHVGwBOqzTX2jzNa06R5gQjNNdKhaZlSF76MTjqmvDJeLQCDd6W1j398tCjZRoy2KmvNFMPft4aqx0dFahf6s85kEoVpivw5Mt4v0AagSRH2OofXERiA4pCxvUPR5IoSmjozhuGSkUijV5T2MvP+Z6PP1jXpNWKuMC5yMHjkCVnYvGG0A3nPM5BoZxeWSkSKQNrOfMLBKIWzoN5IHywVOxo4kAddmcApETzqG/oGM4huVEfX2puVZTHAcQ38jTI8kgWyOgXkVSmL5d5l534Qpnc76vbvcReNjfJqAy7aZDG25Nx+BzovLxv3810TI2UZSC3OGJIHypVDKcp8A8/sEcmxTT8dEoHxF3GRln/cZfQQct83ko7EQSJWxjabM7LvBhErocYzki/EQKNNBFtzuzrjDwbyUUFY6HdqJWAiUa5RIW9mNzHgNoKH1hlYf6zgd1zA3lUeXHOB5+WHvRzDXkaJstzPaK2Hev/FxmqjXNvRNMgZkZdKm28vgDSCcbWjUU7LzllQbLYK44kKjYKOdSfbJArye3D+pI66W6jP9RrUoZKkvEgGhMW25XczcHlwpFd4yXRICPJj2MzgBKN2OqbVLIZ8UikzgP5d6ol61RtsW9YIjcr7wm7cnSJvgYYOO9Ge09UTEJSUglE+SeFlEIjAW4VllslW2MMgQBTsBVum2jeqOqOAjFfG1vCLzsCX2jXOhQQE3+ozHiPD43/diwlkF6gtRcv5qHJFT6GoFN/K0KPo8KdRTv7B6v2y3mSqtpk2gqHiqx92JdRoAeIAJX0BVjq5vrz4pc0jJ1EJsBGSMlUJmlkApvBpF52wEonirFLKzESiFV6PonI1AFG+VQvYvqQFST/EC5cgAAAAASUVORK5CYII="
                @click="reset"
              />
              <img
                src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAB3klEQVRoQ+2Yy0oEMRBFz/hABf0Gda2gO9349W50p6Br9RsUVHxS0JEwZCaV5EZp7IFedbpyT91KUpMZI//NRq6fCeCvHZwcmBxozMC/KqFV4BP4akya53Ob68Mz0OvAPrALvAA3wJMneMWYbeAQ2AQegLtcDA+AZeMsCvQGXHeAMPFHwHo013nOCQ+AjTkZshJiqyFS4s3ty1zJegBMtE1wDKxF2XkHrgROpMRbbHP5UVFCIUYPiCbxJszrQA+IZvE1AKpyWuSmq2zisip1QOGETHytAy0Q8nVU60AOIlUKcvGtDpRA7AyHlHwbbnUghpg/RcNebmPsnVy8yoEchL3vIl4NELbYeSfiXU91ev/EVJVQLDJ1QNl7ufgeDljM1IINAMUHlbIXysVaJj58627SPJOpHUhl3tpu+8U9vhRCtQYWibeSCdtoFwgFwDLx4a+npPNMlVUrgEd87pxoWtgtACXiu0HUAtSI7wJRA9AiXg5RCqAQL4UoAVCKl0F4AXqIl0B4AGzMKbAR7cO/cbH1ClwoLrZGf7Void8bnmfgVnAbt6hXsxP7ANgC7odnaV/nKaEQYGW4Xvc2ii3j3HOVALQI6vbtBNAttc7AkwPORHUbNjnQLbXOwKN34BvKiqMxJwSPZAAAAABJRU5ErkJggg=="
                @click="close"
              />
            </div>
          </div>
          <div class="img">
            <div class="backgroup-img">
              <img
                class="inner-bg-img"
                :src="backgroupImg"
              />
            </div>
            <div
              class="move-img"
              :style="{left: `${moveX}px`}"
            >
              <img
                class="inner-mv-img"
                :src="moveImg"
              />
            </div>
          </div>
          <div class="slide">
            <div
              class="slider-mask"
              :style="{width: `${blcokLeft}px`}"
            >
              <div
                class="block"
                ref="block"
                @mousedown="start"
                :style="{left: `${blcokLeft}px`}"
              >
                <span class="yidun_slider_icon"></span>
              </div>
            </div>
          </div>
          <div
            class="loading"
            v-if="loading"
          >
            <span>loading...</span>
          </div>
        </div>
      </div>
    </div>
  </template>
  
  <script>
  // =========================================
  // 父组件需要提供的方法 名称
  // =========================================
  
  /**
   * 获取滑块图片方法
   */
  const GET_IMG_FUN = "getImg";
  /**
   * 校验滑块图片方法
   */
  const VALID_IMG_FUN = "validImg";
  /**
   * 滑块窗口关闭事件监听
   */
  const CLOST_EVENT_FUN = "close";
  
  export default {
    data() {
      return {
        /**滑块背景图片 */
        backgroupImg: "",
        /**滑块图片 */
        moveImg: "",
        /**是否已经移动滑块 */
        startMove: false,
        /**滑块移动距离 */
        blcokLeft: 0,
        /**开始滑动的x轴 */
        startX: 0,
        startY: 0,
        /**划过的百分比 */
        movePercent: 0,
        /**验证码唯一ID */
        uuid: "",
        /**滑块移动的x轴 */
        moveX: 0,
        /** 加载遮罩标识 */
        loading: false,
        // 滑动轨迹滑动时间等数据
        trackData: {
            bgImageWidth: 0,
            bgImageHeight: 0,
            sliderImageWidth: 0,
            sliderImageHeight: 0,
            startSlidingTime: null,
            entSlidingTime: null,
            trackList: []
        }
      };
    },
    props: {
      // 是否开启日志, 默认true
      log: {
        type: Boolean,
        required: false,
        default: true
      }
    },
    mounted() {
      this.getImg();
    },
    methods: {
      /**
       * 打印日志
       */
      printLog(msg, ...optionalParams) {
        if (this.log) {
          if (optionalParams && optionalParams.length > 0) {
            console.info(
              `滑块验证码[${msg}]`,
              optionalParams.length === 1 ? optionalParams[0] : optionalParams
            );
          } else {
            console.info(`滑块验证码[${msg}]`);
          }
        }
      },
  
      /**
       * 获取滑块图片
       */
      getImg() {
        this.loading = true;
        this.$emit(GET_IMG_FUN, data => {
          this.printLog(GET_IMG_FUN, data);
          this.loading = false;
          if (!data) return;
          console.log("data", data);
  
          this.backgroupImg = data.backgroundImage;
          this.moveImg = data.templateImage;
          this.uuid = data.captchaKey;
          this.trackData.bgImageWidth = data.backgroundImageWidth
          this.trackData.bgImageHeight = data.backgroundImageHeight
          this.trackData.sliderImageWidth = data.templateImageHeight
          this.trackData.sliderImageHeight = data.templateImageWidth
        });
      },
      /**
       * 校验图片
       */
      validImg() {
        this.printLog(`滑块抬起`, this.trackData);
        this.$emit(VALID_IMG_FUN, this.trackData, this.uuid, data => {
          this.printLog(VALID_IMG_FUN, data);
        //   if (data === false) {
        //     this.reset();
        //   }
        });
      },
      /**
       * 重新生成图片
       */
      reset() {
        this.getImg();
        this.moveX = 0;
        this.movePercent = 0;
        this.startX = 0;
        this.startY = 0;
        this.blcokLeft = 0;
        this.trackData.startSlidingTime = null
        this.trackData.entSlidingTime = null
        this.trackData.trackList = []
        this.trackData.bgImageWidth = 0
        this.trackData.bgImageHeight = 0
        this.trackData.sliderImageWidth = 0
        this.trackData.sliderImageHeight = 0
      },
      /**
       * 按钮关闭事件
       */
      close() {
        this.printLog("关闭按钮触发");
        this.$emit(CLOST_EVENT_FUN);
      },
      /**
       * 开始滑动
       */
      start(e) {
        this.trackData.startSlidingTime = new Date()
        this.startX = e.pageX*2.12;
        this.startY = e.pageY*2.12
        this.startMove = true;
        window.addEventListener("mousemove", this.move);
        window.addEventListener("mouseup", this.up);
        
      },
      /**
       * 滑块滑动事件
       */
      move(e) {
        if (!this.startMove) return;
        const moveX = (e.pageX*2.12 - this.startX)/2.12;
        let pageX = e.pageX*2.12
        let pageY = e.pageY*2.12
        this.trackData.trackList.push({
            x: pageX - this.startX,
            y: pageY - this.startY,
            t: (new Date().getTime() - this.trackData.startSlidingTime.getTime())
        })
        const movePercent = moveX / 280;
        if (moveX <= 0) {
          this.blcokLeft = 0;
          this.moveX = 0;
          this.movePercent = 0;
        } else if (moveX >= 0 && moveX <= 235) {
          this.blcokLeft = moveX;
          this.moveX = moveX;
          this.movePercent = movePercent;
        } else if (moveX >= 235) {
          this.blcokLeft = 235;
          this.moveX = 235;
          this.movePercent = movePercent;
        }
        console.log("movethis.trackData",this.trackData)
      },
      /**
       * 滑块鼠标抬起事件
       */
      up(e) {
        console.log("upthis.trackData",this.trackData)
        this.trackData.entSlidingTime = new Date()
        window.removeEventListener("mousemove", this.move);
        window.removeEventListener("mouseup", this.up);
        if (!this.startMove) return;
        this.startMove = false;
        this.validImg();
      }
    },
    /**
     * 销毁事件
     */
    beforeDestroy() {
      window.removeEventListener("mousemove", this.move);
      window.removeEventListener("mouseup", this.up);
    }
  };
  </script>
  
  <style lang="scss" scoped>
  .slider-mask {
    position: absolute;
    left: 0;
    top: 0;
    height: 40px;
    border: 0 solid #1991fa;
    background: #d1e9fe;
    border-radius: 2px;
  }
  .yidun_slider_icon {
    position: absolute;
    top: 50%;
    margin-top: -6px;
    left: 50%;
    margin-left: -6px;
    width: 14px;
    height: 10px;
    background-image: url(https://cstaticdun.126.net//2.13.7/images/icon_light.4353d81.png);
    background-position: 0 -13px;
    background-size: 32px 544px;
  }
  .inner-mv-img,
  .inner-bg-img,
  .title {
    -moz-user-select: none;
    -webkit-user-select: none;
    -ms-user-select: none;
    -khtml-user-select: none;
    user-select: none;
  }
  .slider {
    .mask {
      display: block;
      z-index: 998;
      background: rgba(0, 0, 0, 0);
      width: 310px;
      height: 280px;
    }
    .container {
      position: absolute;
      z-index: 999;
      width: 310px;
      height: 280px;
      margin: auto;
      background: rgba(255, 255, 255, 1);
      border-radius: 6px;
      box-shadow: 0px 0px 11px 0px rgba(153, 153, 153, 1);
      box-sizing: border-box;
      padding: 17px 15px;
      .title {
        font-size: 14px;
        color: #333;
        display: flex;
        justify-content: space-between;
        .button-group {
          img {
            width: 25px;
            height: 25px;
            cursor: pointer;
          }
        }
      }
      .img {
        width: 280px;
        height: 180px;
        position: relative;
        img {
          width: 100%;
        }
        .backgroup-img {
          position: absolute;
          left: 0;
          top: 0;
          width: 100%;
        }
        .move-img {
          width: 52.20338981px;
          position: absolute;
          left: 0;
          top: 0;
        }
      }
      .slide {
        width: 100%;
        height: 40px;
        border: 1px solid #e4e7eb;
        background-color: #f7f9fa;
        box-sizing: border-box;
        position: relative;
        &::before {
          position: absolute;
          content: "按住左边按钮移动完成上方拼图";
          display: flex;
          justify-content: center;
          align-items: center;
          font-size: 12px;
          color: #999;
          width: 100%;
          height: 100%;
          text-indent: 50px;
        }
        .block {
          width: 40px;
          height: 38px;
          background-color: #fff;
          box-shadow: 0 0 3px rgba(0, 0, 0, 0.3);
          display: flex;
          justify-content: center;
          align-items: center;
          position: absolute;
          left: 0;
          top: 0;
          cursor: pointer;
          background-size: 30px;
          background-repeat: no-repeat;
          background-position: center;
        }
      }
      .block:hover {
        background-color: #1991fa;
      }
      .block:hover .yidun_slider_icon {
        background-image: url(https://cstaticdun.126.net//2.13.7/images/icon_light.4353d81.png);
        background-position: 0 0;
        background-size: 32px 544px;
      }
      .loading {
        width: 100%;
        height: 100%;
        background: rgba(0, 0, 0, 0.3);
        position: absolute;
        top: 0;
        left: 0;
        border-radius: 6px;
        display: flex;
        justify-content: center;
        align-items: center;
        color: #fff;
      }
    }
  }
  </style>

2.在需要地方引入该组件

点击查看代码
 <!-- 前后端滑块验证 -->
    <div class="islider" v-if="sliderShow">
      <Slider
        @getImg="getImg"
        @validImg="validImg"
        @close="onClose"
        :log="true"
      ></Slider>
    </div>
	
	
   <script>
   import Slider from "@/components/Mcslider/index.vue"
   export default {
	  components: {
		Slider
	  },
	  data() {
	  	return {
			sliderShow:false
		}
	  },
	  methods:{
	  	onShow() {
			if(!this.authForm.mobile){
				return this.$message.warning("请输入手机号码")
			}
			this.sliderShow = true
			},
			onClose() {
			  this.sliderShow = false;
		},

		  // 获取滑动验证码(下方有格式截图)
		  getImg(callback) {
			  genImgSwipe().then((res) => {
				  callback(res.body);
			  }, error => {
				  callback(error);
			  });
		  },

		  // 操作滑动后返回值,并传去后端验证
		  validImg(trackData, id, callback) {
			  this.sliderData = {mobile:this.authForm.mobile,...trackData,captchaKey:id}
			  this.handleGetAuthCode(); // 获取验证码
			  callback(false);
			  this.sliderShow= false;
		  },
	  }
	 }
   </script>

后端

1.导入xml

点击查看代码
<!-- maven 导入 -->
<dependency>
    <groupId>cloud.tianai.captcha</groupId>
    <artifactId>tianai-captcha</artifactId>
    <version>1.4.1</version>
</dependency>

2.使用 ImageCaptchaGenerator生成器生成验证码

点击查看代码
package example.readme;

import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
import cloud.tianai.captcha.generator.ImageCaptchaGenerator;
import cloud.tianai.captcha.generator.common.model.dto.ImageCaptchaInfo;
import cloud.tianai.captcha.generator.impl.MultiImageCaptchaGenerator;
import cloud.tianai.captcha.resource.ImageCaptchaResourceManager;
import cloud.tianai.captcha.resource.impl.DefaultImageCaptchaResourceManager;
import cloud.tianai.captcha.validator.ImageCaptchaValidator;
import cloud.tianai.captcha.validator.impl.BasicCaptchaTrackValidator;

import java.util.Map;

public class Test {
    public static void main(String[] args) throws InterruptedException {
        ImageCaptchaResourceManager imageCaptchaResourceManager = new DefaultImageCaptchaResourceManager();
        ImageTransform imageTransform = new Base64ImageTransform();
        ImageCaptchaGenerator imageCaptchaGenerator = new MultiImageCaptchaGenerator(imageCaptchaResourceManager,imageTransform).init(true);
        /*
                生成滑块验证码图片, 可选项
                SLIDER (滑块验证码)
                ROTATE (旋转验证码)
                CONCAT (滑动还原验证码)
                WORD_IMAGE_CLICK (文字点选验证码)

                更多验证码支持 详见 cloud.tianai.captcha.common.constant.CaptchaTypeConstant
         */
        ImageCaptchaInfo imageCaptchaInfo = imageCaptchaGenerator.generateCaptchaImage(CaptchaTypeConstant.SLIDER);
        System.out.println(imageCaptchaInfo);

        // 负责计算一些数据存到缓存中,用于校验使用
        // ImageCaptchaValidator负责校验用户滑动滑块是否正确和生成滑块的一些校验数据; 比如滑块到凹槽的百分比值
        ImageCaptchaValidator imageCaptchaValidator = new BasicCaptchaTrackValidator();
        // 这个map数据应该存到缓存中,校验的时候需要用到该数据
        Map<String, Object> map = imageCaptchaValidator.generateImageCaptchaValidData(imageCaptchaInfo);
    }
}

3.使用ImageCaptchaValidator校验器 验证

点击查看代码
package example.readme;

import cloud.tianai.captcha.validator.common.model.dto.ImageCaptchaTrack;
import cloud.tianai.captcha.validator.impl.BasicCaptchaTrackValidator;

import java.util.Map;

public class Test2 {
    public static void main(String[] args) {
        BasicCaptchaTrackValidator sliderCaptchaValidator = new BasicCaptchaTrackValidator();

        ImageCaptchaTrack imageCaptchaTrack = null;
        Map<String, Object> map = null;
        Float percentage = null;
        // 用户传来的行为轨迹和进行校验
        // - imageCaptchaTrack为前端传来的滑动轨迹数据
        // - map 为生成验证码时缓存的map数据
        boolean check = sliderCaptchaValidator.valid(imageCaptchaTrack, map).isSuccess();
//        // 如果只想校验用户是否滑到指定凹槽即可,也可以使用
//        // - 参数1 用户传来的百分比数据
//        // - 参数2 生成滑块是真实的百分比数据
        check = sliderCaptchaValidator.checkPercentage(0.2f, percentage);
    }
}

整体架构设计

ianai-captcha 验证码整体分为 生成器(ImageCaptchaGenerator)、校验器(ImageCaptchaValidator)、资源管理器(ImageCaptchaResourceManager) 其中生成器、校验器、资源管理器等都是基于接口模式实现 可插拔的,可以替换为自定义实现,灵活度高

具体可以看该插件的作者 天爱有情 / tianai-captcha

posted @ 2023-08-07 15:39  布偶123  阅读(361)  评论(0编辑  收藏  举报