短信防刷之滑动验证码

前言:最近想写一个滑动验证码,前台的样式虽然很好看,但是并不安全,网上也都是一些demo,不是前后台分离的,然后就自己查资料,自己来完成了

滑动验证码

一、为什么要使用滑动验证码

首先,滑块验证码能够有效防止暴力破解和自动化攻击。在传统的账号密码验证方式下,黑客可以通过暴力破解手段尝试大量密码组合,从而获取用户账户的控制权。而滑块验证码的引入,使得每次验证都需要用户进行手动操作,极大地增加了黑客攻击的难度和成本。

其次,滑块验证码能够提升用户体验。相比于传统的文字或数字验证码,滑块验证码的操作更加简单直观,用户只需要通过拖动滑块即可完成验证,无需输入复杂的字符或数字。这不仅降低了用户的使用门槛,也提升了用户的操作体验。

此外,滑块验证码还具有一定的灵活性和可扩展性。淘宝等电商平台可以根据自身的安全需求和用户行为数据,动态调整滑块验证码的难度和出现频率。例如,在检测到异常登录行为或高风险操作时,平台可以要求用户进行更严格的滑块验证,以确保账户安全。

总的来说,淘宝频繁出现滑块验证码是为了保障用户账户的安全性和提升用户体验。通过引入这种交互式的验证方式,淘宝能够有效地防止自动化攻击和恶意操作,同时为用户提供更加便捷和安全的购物环境。

二、实现原理

滑动验证码的原理基于人类的视觉和手势行为。它的设计目的是模拟人类在拖动滑块上的操作,以判断用户是否为真实的人类。下面是滑动验证码的工作原理:

加载验证码:当用户访问需要进行验证的网站时,验证码会被加载并显示在页面上。

定位滑块图:通过分析页面上的验证码元素,识别出背景图、滑块图和验证区域。通常,滑块图会被嵌入到背景图中,而验证区域则是滑块图与背景图的重叠部分。

用户滑动操作:用户需要使用鼠标或触摸屏对滑块图进行拖动操作,将滑块图滑动至缺口位置,以完成验证。

验证结果判断:当用户完成滑动操作后,系统会根据滑块图的位置与缺口位置的关系来判断验证结果。如果滑块图与缺口位置匹配,系统会认定用户为真实的人类用户;否则,系统会认为用户可能是机器人或恶意攻击者。

也就是以下的流程

  1. 前端显示:用户首先看到一个页面,页面上显示了一个带有滑块和背景图案的验证码区域。通常滑块初始位置在左侧,用户需要将滑块拖动到正确的位置。

  2. 交互过程

    • 用户点击并按住鼠标左键,拖动滑块至指定位置。
    • 前端会监测鼠标移动事件,实时更新滑块的位置。
    • 用户释放鼠标左键后,前端会发送包含滑块位置信息的请求给后端进行验证。
  3. 后端验证

    • 后端接收到前端发送的请求,获取到滑块的位置信息。
    • 后端根据预先生成的验证码信息,对比用户拖动滑块后的位置是否与预期位置相符。
    • 如果位置匹配成功,则验证通过;否则验证失败,要求用户重新验证。
  4. 防御机制:滑块验证码通常会具备一些防御机制来防止恶意破解,例如检测拖动速度、检测鼠标轨迹等,以提高安全性。

三、代码实现

这里要感谢gitee作者-天爱有情开源的后台代码 captcha

3.1 后台代码

3.1.1依赖
        <dependency>
            <groupId>cloud.tianai.captcha</groupId>
            <artifactId>tianai-captcha</artifactId>
            <version>1.4.1</version>
        </dependency>

        <dependency>
            <groupId>cloud.tianai.captcha</groupId>
            <artifactId>tianai-captcha-springboot-starter</artifactId>
            <version>1.4.1</version>
        </dependency>
3.1.2 yml配置

作者这里使用的配置文件是yml的格式,小伙伴们一定要注意格式哦!!

captcha:
  cache:
    enabled: true
    cache-size: 20
  secondary:
    enabled: false
  init-default-resource: false
cors:
  control-allow-headers: "*"
  control-allow-methods: "*"
  control-allow-origin: "*"
3.1.3控制层代码
package com.brook.controller;


import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
import cloud.tianai.captcha.common.response.ApiResponse;
import cloud.tianai.captcha.spring.application.ImageCaptchaApplication;
import cloud.tianai.captcha.spring.plugins.secondary.SecondaryVerificationApplication;
import cloud.tianai.captcha.spring.vo.CaptchaResponse;
import cloud.tianai.captcha.spring.vo.ImageCaptchaVO;
import cloud.tianai.captcha.validator.common.model.dto.ImageCaptchaTrack;
import com.brook.common.result.Result;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import java.util.Collections;
@RestController
@RequestMapping("/captcha")
public class CaptchaController {


    @Autowired
    private ImageCaptchaApplication imageCaptchaApplication;

    /作者这里只写了滑动验证码,所以只匹配了滑块的类型
    @GetMapping("/gen")
    @ResponseBody
    public Result<CaptchaResponse<ImageCaptchaVO>> genCaptcha(HttpServletRequest request, @RequestParam(value = "type", required = false)String type) {
        if (StringUtils.isBlank(type)) {
            type = CaptchaTypeConstant.SLIDER;
        }
        if ("RANDOM".equals(type)) {
            type = CaptchaTypeConstant.SLIDER;
        }
        CaptchaResponse<ImageCaptchaVO> response = imageCaptchaApplication.generateCaptcha(type);
        return Result.success(response);
    }
    /校验x轴的坐标,以及其他的一些信息
    @PostMapping("/check")
    @ResponseBody
    public Result<ApiResponse<?>> checkCaptcha(@RequestBody Data data,
                                    HttpServletRequest request) {

        ImageCaptchaTrack dataData = data.getData();
        ApiResponse<?> response = imageCaptchaApplication.matching(data.getId(), dataData);
        boolean success = response.isSuccess();
        System.out.println(success);
        if (response.isSuccess()) {
            return Result.success(ApiResponse.ofSuccess(Collections.singletonMap("id", data.getId())));
        }
        return Result.success(response);
    }


    @lombok.Data
    public static class Data {
        private String  id;
        private ImageCaptchaTrack data;
    }

    /**
     * 二次验证,一般用于机器内部调用,这里为了方便测试,作者这里没有使用到
     * @param id id
     * @return boolean
     */
    @GetMapping("/check2")
    @ResponseBody
    public boolean check2Captcha(@RequestParam("id") String id) {
        // 如果开启了二次验证
        if (imageCaptchaApplication instanceof SecondaryVerificationApplication) {
            return ((SecondaryVerificationApplication) imageCaptchaApplication).secondaryVerification(id);
        }
        return false;
    }
}
3.1.4 跨域代码
package com.brook.controller;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.util.pattern.PathPatternParser;

import java.util.List;

/**
 * 该类用于设置跨域
 */
@Configuration
public class CorsPropConfiguration {


    @Bean
    public FilterRegistrationBean coreWebFilter(CorsProperties corsProperties) {
        CorsConfiguration config = new CorsConfiguration();
        // * 号表示匹配任意的
        config.setAllowedMethods(corsProperties.getControlAllowMethods());
        config.setAllowedOrigins(corsProperties.getControlAllowOrigin());
        config.setAllowedHeaders(corsProperties.getControlAllowHeaders());
        PathPatternParser patternParser = new PathPatternParser();
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(patternParser);
        // ** 代表所有
        source.registerCorsConfiguration("/**", config);
        FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
        bean.setOrder(0);
        return bean;
    }

    @Data
    @Configuration
    @ConfigurationProperties(prefix = "cors")
    public static class CorsProperties {
        private List<String> controlAllowHeaders;
        private List<String> controlAllowMethods;
        private List<String> controlAllowOrigin;
    }
}
3.1.5 负责模版和背景图,小伙伴也可以自行添加图片

像这样,把图片整合进resources下的bgimages包中,就可以啦,不过要加这行代码

注:图片名称一定要和自己整合的图片一致!!!并且图片的大小要设置为600×360

addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/m.jpg","default"));
package com.brook.controller;

import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
import cloud.tianai.captcha.generator.common.constant.SliderCaptchaConstant;
import cloud.tianai.captcha.generator.impl.StandardSliderImageCaptchaGenerator;
import cloud.tianai.captcha.resource.common.model.dto.Resource;
import cloud.tianai.captcha.resource.common.model.dto.ResourceMap;
import cloud.tianai.captcha.resource.impl.DefaultResourceStore;
import cloud.tianai.captcha.resource.impl.provider.ClassPathResourceProvider;
import org.springframework.stereotype.Component;

import static cloud.tianai.captcha.generator.impl.StandardSliderImageCaptchaGenerator.DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH;

/**
 * @Author: YanShuLing
 * @date 2022/7/11 14:22
 * @Description 负责模板和背景图存储的地方
 */
@Component
public class MyResourceStore extends DefaultResourceStore {

    public MyResourceStore() {

        // 滑块验证码 模板 (系统内置)
        ResourceMap template1 = new ResourceMap("default",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")));
        ResourceMap template2 = new ResourceMap("default",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")));


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


        // 2. 添加自定义背景图片
        addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/a.jpg","default"));
        addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/b.jpg","default"));
        addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/c.jpg","default"));
        addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/d.jpg","default"));
        addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/e.jpg","default"));
        addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/f.jpg","default"));
        addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/g.jpg","default"));
        addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/h.jpg","default"));
        addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/i.jpg","default"));
        addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/j.jpg","default"));
        addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/k.jpg","default"));
        addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/l.jpg","default"));
        addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/m.jpg","default"));

    }
}

3.2 vue前台代码

(作者这里使用的是vue2 从gitee上面拉取的花裤衩脚手架)小伙伴们也可以使用正常的vue2

3.2.1 首先定义一个子组件

中间运行可能需要安装一个依赖,小伙伴运行的时候,按照控制台的提示安装就好了

<template>
  <div class="slider" ref="sliderComponent">
    <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 = 'getCaptcha'
/**
 * 校验滑块图片方法
 */
const VALID_IMG_FUN = 'validImg'
/**
 * 滑块窗口关闭事件监听
 */
const CLOST_EVENT_FUN = 'close'
// import moment from 'moment'
export default {
  data() {
    return {
      /**滑块背景图片 */
      backgroupImg: '',
      /**滑块图片 */
      moveImg: '',
      /**是否已经移动滑块 */
      startMove: false,
      /**滑块移动距离 */
      blcokLeft: 0,
      /**开始滑动的x轴 */
      startX: 0,
      /**划过的百分比 */
      movePercent: 0,
      /**验证码唯一ID */
      uuid: '',
      /**滑块移动的x轴 */
      moveX: 0,
      /** 加载遮罩标识 */
      loading: false,
      Data: {
        data: {
          bgImageWidth: null,
          bgImageHeight: null,
          sliderImageWidth: null,
          sliderImageHeight: null,
          startSlidingTime: null,
          endSlidingTime: null,
          trackList: [
          ]
        },
        id: null
      }
    }
  },
  props: {
    // 是否开启日志, 默认true
    log: {
      type: Boolean,
      required: false,
      default: true
    }
  },
  mounted() {
    this.getCaptcha()
  },
  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}]`)
        }
      }
    },
    /**
     * 获取滑块图片
     */
    getCaptcha() {
      this.loading = true
      this.$emit(GET_IMG_FUN, data => {
        this.loading = false
        if (!data) return
        this.backgroupImg = data.captcha.backgroundImage
        this.moveImg = data.captcha.templateImage
        this.Data.data.bgImageWidth = 280
        this.Data.data.bgImageHeight = 180
        this.Data.data.sliderImageHeight = 180
        this.Data.data.sliderImageWidth = 52
        this.uuid = data.id
        this.Data.id = data.id
      })
    },
    /**
     * 校验图片
     */
    validImg(Data) {
      this.printLog(`滑块抬起`, this.movePercent)
      this.$emit(VALID_IMG_FUN, Data, data => {
        // this.printLog(VALID_IMG_FUN, data)
        console.log(data)
        if (data.success === false) {
          this.reset()
          return
        }
        this.close()
      })
    },
    /**
     * 重新生成图片
     */
    reset() {
      this.getCaptcha()
      this.moveX = 0
      this.movePercent = 0
      this.startX = 0
      this.blcokLeft = 0
    },
    /**
     * 按钮关闭事件
     */
    close() {
      this.printLog('关闭按钮触发')
      this.$emit(CLOST_EVENT_FUN)
    },
    /**
     * 开始滑动
     */
    start(e) {
      this.startX = e.pageX
      this.startMove = true
      window.addEventListener('mousemove', this.move)
      window.addEventListener('mouseup', this.up)
    },
    /**
     * 滑块滑动事件
     */
    move(e) {
      if (!this.startMove) return
      // this.Data.data.startSlidingTime = moment(new Date()).format('ddd MMM DD HH:mm:ss [CST] YYYY')
      this.Data.data.startSlidingTime = new Date()
      const moveX = e.pageX - this.startX
      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
      }
      // const track = {
      //   x: this.blcokLeft,
      //   y: e.pageY,
      //   t: (this.Data.data.endSlidingTime - this.Data.data.startSlidingTime)
      // }
      const track = {
        x: this.blcokLeft,
        y: 0,
        t: 0 // 初始设为0,稍后在 up 方法中更新为正确的时间间隔
      }
      if (track.x === 0) {
        return
      }
      this.Data.data.trackList.push(track)
    },
    /**
     * 滑块鼠标抬起事件
     */
    up(e) {
      window.removeEventListener('mousemove', this.move)
      window.removeEventListener('mouseup', this.up)
      if (!this.startMove) return
      // this.Data.data.endSlidingTime = moment(new Date()).format('ddd MMM DD HH:mm:ss [CST] YYYY')
      this.Data.data.endSlidingTime = new Date()
      this.Data.data.trackList.forEach(track => {
        const endTime = this.Data.data.endSlidingTime.getTime()
        const startTime = this.Data.data.startSlidingTime.getTime()
        track.t = endTime - startTime
      })
      this.startMove = false
      this.Data.data.endSlidingTime = new Date()
      this.validImg(this.Data)
    }
  },
  /**
   * 销毁事件
   */
  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>

注:一定要对照好参数,不然传到后台会报错!!!

3.2.2 父组件

(作者这里是一个注册的页面,当用户点击发送验证码的时候,滑动验证码就出现了)

小伙伴可以根据自己的需求更改代码

<template>
  <div class="login-container">
    <div style="width: 450px; height: 400px; margin: 150px auto; background-color:rgba(165,190,234,0.5); border-radius: 10px">
      <div style="width: 100%; height: 100px; font-size: 30px; line-height: 100px; text-align: center; color: #5050bb">欢迎注册</div>
      <div style="margin-top: 25px; text-align: center; height: 320px;">
        <el-form :model="UserRegisterReq">
          <el-form-item label="手机号:" label-width="100px">
            <el-input v-model="UserRegisterReq.phone" prefix-icon="el-icon-user" style="width: 80%" placeholder="请输入手机号"></el-input>
          </el-form-item>
          <el-form-item label="验证码:" label-width="100px">
            <el-input v-model="UserRegisterReq.code" prefix-icon="el-icon-user" style="width: 80%" placeholder="请输入验证码"></el-input>
          </el-form-item>
          <el-form-item label="密码:" label-width="100px">
            <el-input v-model="UserRegisterReq.password" show-password prefix-icon="el-icon-lock" style="width: 80%" placeholder="请输入密码"></el-input>
          </el-form-item>
          <el-form-item label="确认密码:" label-width="100px">
            <el-input v-model="UserRegisterReq.confirmPassword" show-password prefix-icon="el-icon-lock" style="width: 80%" placeholder="确认密码"></el-input>
          </el-form-item>
          <el-form-item>
            <el-button type="success" @click="onShow()">点击发送验证码</el-button>
            <el-button style="width: 50%; margin-top: 10px" type="primary" @click="register()">注册</el-button>
          </el-form-item>
          <div style="text-align: center">
            <span style="font-size: 15px">已有账号</span>?<a href="javascript:void(0)" style="text-decoration: none; font-size: 15px;" @click="navLogin">点击登录</a>
          </div>
        </el-form>
        <center>
          <div v-if="show">
            <Slider
              ref="sliderComponent"
              :captchaData="captchaData"
              @getCaptcha="handleGetCaptcha"
              @validImg="validImg"
              @close="onClose"
              :log="true"
            ></Slider>
          </div> </center>
    </div>
    </div>
  </div>
</template>
<script>
import { check, getCaptcha, send, sign } from '@/api/user'
import Slider from '@/views/register/slider.vue'
// import {error} from 'autoprefixer/lib/utils'
export default {
  components: {
    Slider
  },
  data() {
    return {
      UserRegisterReq: {
        confirmPassword: ''
      },
      Data: {
        ImageCaptchaTrack: {},
        id: ''
      },
      show: false,
      type: 'RANDOM',
      captchaData: null
    }
  },
  // 页面加载的时候,做一些事情,在created里面
  created() {
    // window.localStorage.setItem('isRegisterPage', 'true')
    // console.log('注册页面' + window.localStorage.getItem('isRegisterPage'))
  },
  // 定义一些页面上控件出发的事件调用的方法
  mounted() {
  },
  methods: {
    sendCode() {
      send(this.UserRegisterReq.phone).then(response => {
        alert('验证码是:' + response.data)
      })
    },
    navLogin() {
      this.$router.push('/login')
    },
    register() {
      sign(this.UserRegisterReq).then(response => {
        if (response.code === 200) {
          alert(response.msg)
        }
      })
      // 注册逻辑
    },
    onClose() {
      this.show = false
    },
    onShow() {
      this.show = true
    },
    handleGetCaptcha(callback) {
      getCaptcha(this.type).then(response => {
        // console.log(response)
        callback(response.data)
      })
    },
    validImg(data, callback) {
      // console.log(data)
      check(data).then(response => {
        // console.log(response)
        callback(response.data)
        if (response.data.success) {
          // this.onClose()
          this.sendCode()
          // location.reload()
        } else {
          alert('验证失败')
          this.handleGetCaptcha()// 验证失败时重新获取验证码
        }
      }).catch(error => {
        console.error(error)
      }).finally(() => {
        this.loading = false // 请求结束,设置loading为false
      })
    }
  }
}
</script>
<style scoped>
.login-container {
  height: 100vh;
  overflow: hidden;
  background-image: url('~@/assets/404_images/login_bg.jpg');
  background-size: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
}
ul li {
  list-style: none;
}
* {
  margin: 0;
  padding: 0;
}
.top{
  overflow: auto;
}
.top li:hover{
  cursor: pointer;
}
.top li{
  float: left;
  height: 40px;
  width: 120px;
  margin-right: 5px;
  line-height: 40px;
  text-align: center;
  background-color: #409eff;
  color: #fff;
  font-size: 15px;
  box-sizing: border-box;
  border: 1px solid #409eff;
}
.captcha-iframe {
  width: 300px;
  height: 320px;
  border: none;
}
.after {
  color: #88949d;
}
</style>

好了代码到这里也就基本结束了

3.3 过程中出现的问题

这里讲一个作者在完成滑动验证码时遇到的一个问题,有小伙伴可能会使用springcloud,要注意,滑块验证码不要跟自己的用户模块写在一起,不然这块可能在gateway网关在redis中到不到自己存在redis中的key和value ,并且可能会出现报错

org/springframework/boot/autoconfigure/data/redis/RedisAutoConfiguration.class

具体原因是:

Spring Boot 已经默认提供了 Redis 的自动配置,导致与我们自定义的 Redis 配置发生冲突,也是这个原因导致我们在网关的过滤器中取不到redis中的key和value,因为存和取不是一个redis实例

下面我们来演示一下效果

验证成功后会发送验证码,验证失败会刷新图片

                         

                       

3.4 为什么要刷新图片呢?

如果不刷新图片,恶意攻击者会通过持续尝试来破解验证码。如果一直使用同一张验证码图片,
攻击者可能会使用自动化工具进行大量尝试,增加系统的风险, 尽管刷新验证码可能会增加用户操作的复杂性,但在一定程度上也可以提高用户体验。
如果用户在第一次尝试时失败,系统刷新验证码后,用户有机会重新尝试,避免了因错误导致的长时间等待或退出

到这里就结束啦,感谢大家的一路支持,请给凌弟一个暴击三连吧!跪谢!!

                                                                

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

洁净区气流流型测试及拍摄注意事项-北京中邦兴业解读

洁净区气流流型测试在众多确认和验证项目中虽然看似微不足道&#xff0c;但其重要性却不容忽视。要做好气流流型测试&#xff0c;绝非易事&#xff0c;它要求精细的操作和深入的专业知识。毫不夸张地说&#xff0c;即便是对影视圈的大牌导演而言&#xff0c;若是不了解气流流型…

MySQL死锁与死锁检测

一、什么是MySQL死锁 MySQL中死锁是指两个或多个事务在互相等待对方释放资源&#xff0c;导致无法继续执行的情况。 MySQL系统中当两个或多个事务在并发执行时&#xff0c;就可能会遇到每项事务都持有某些资源同时又请求其他事务持有的资源&#xff0c;从而形成事务之间循环等…

npm命令卡在reify:eslint: timing reifyNode:node_modules/webpack Completed in 475ms不动

1.现象 执行npm install命令时&#xff0c;没有报错&#xff0c;卡在reify:eslint: timing reifyNode:node_modules/webpack Completed in 475ms不动 2.解决办法 &#xff08;1&#xff09;更换淘宝镜像源 原淘宝 npm 域名http://npm.taobao.org 和 http://registry.npm.ta…

springboot 人大金仓 kingbase-备份还原,命令中带密码,支持window和linux

命令带密码参考 Java代码实现国产人大金仓数据库备份还原需求-CSDN博客文章浏览阅读818次&#xff0c;点赞16次&#xff0c;收藏12次。本人在一次项目中&#xff0c;遇到了需要在系统管理中提供给用户备份还原系统数据的功能&#xff0c;由于项目特殊性&#xff0c;项目底层数…

Day:007(3) | Python爬虫:高效数据抓取的编程技术(scrapy框架使用)

Scrapy 保存数据案例-小说保存 spider import scrapyclass XiaoshuoSpiderSpider(scrapy.Spider):name xiaoshuo_spiderallowed_domains [zy200.com]url http://www.zy200.com/5/5943/start_urls [url 11667352.html]def parse(self, response):info response.xpath(&qu…

react v18 项目初始化

按照以下命令进行傻瓜式操作即可&#xff1a; 全局安装脚手架工具&#xff1a; npm install -g create-react-app创建项目my-react-app&#xff1a; create-react-app my-react-app安装 antd: yarn add antd安装 react-router-dom&#xff1a; yarn add react-router-dom启动项…

幻兽帕鲁老板公开发声:腾讯正在制作幻兽帕鲁克隆版

昨天&#xff0c;Pocketpair的老板出来指责中国游戏公司抄袭了他们的游戏Palworld&#xff0c;说这简直是太不可思议了。 Pocketpair的CEO Takuro Mizobe发布了一个叫Auroria的游戏的截图&#xff0c;然后说&#xff1a;“腾讯正在制作Palworld的克隆游戏&#xff01;在中国&a…

10个你可能没听过但很好用的建筑设计AI工具

在之前的文章中我给大家介绍了很多Midjourney、Stable Diffusion的使用方法和对应的功能&#xff1a; Midjourney vs Stable Diffusion&#xff1a;提示相同&#xff0c;出图差距竟这么大&#xff01;哪个更适配你的工作&#xff1f;https://news.vsochina.com/cn/industry/64…

Java 基于微信小程序的医院预约挂号小程序(V3)

博主介绍&#xff1a;✌程序员徐师兄、7年大厂程序员经历。全网粉丝12w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;…

基于SpringBoot的“商务安全邮箱”的设计与实现(源码+数据库+文档+PPT)

基于SpringBoot的“商务安全邮箱”的设计与实现&#xff08;源码数据库文档PPT) 开发语言&#xff1a;Java 数据库&#xff1a;MySQL 技术&#xff1a;SpringBoot 工具&#xff1a;IDEA/Ecilpse、Navicat、Maven 系统展示 系统功能结构 收件箱效果图 草稿箱效果图 已发送…

计算机网络实验实验之VLAN的配置与分析

实验目的 了解什么是带内管理&#xff1b;熟练掌握如何使用telnet方式管理交换机&#xff1b;熟练掌握如何为交换机设置web方式管理&#xff1b;熟练掌握如何进入交换机web管理方式&#xff1b;了解交换机web配置界面&#xff0c;并能进行部分操作。 (6)了解VLAN原理&#xf…

OpenHarmony南向开发案例:【智能门锁】

一. 简介 本demo是基于Openharmony 3.1 Beta本版开发&#xff0c;不仅可以接收数字管家应用下发的指令来控制门锁开启&#xff0c;而且还可以通过数字管家设置不同的开锁密码以及一次性密码&#xff0c;实现给临时用户一个临时密码&#xff0c;保证门户安全。当然除了开锁的功…

Shopee虾皮批量上传全球产品指南

当shopee虾皮需要大量上架新产品时&#xff0c;批量工具可以更好的提升效率。通过本指南&#xff0c;你将了解如何批量上传全球商品&#xff0c;本指南适用于所有站点。 一、什么是批量上传&#xff1f; 您可以通过【中国卖家中心>>全球商品>>批量上传】功能&…

部署ssm项目时遇到的一些错误

问题一&#xff1a;项目使用maven&#xff0c;打包完之后在idea启动不了 打包完之后在idea里面运行报错&#xff0c;提示找不到springmvc.xml配置文件。 再次clean后又可以运行了。 这是因为maven打包只会打包java文件&#xff0c;配置文件不进行打包&#xff0c;导致target-…

P1157 组合的输出 (dfs深搜)

题目连接&#xff1a;P1157 组合的输出 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) 思路&#xff1a; AC代码&#xff1a; #include<iostream> #include<cstring>using namespace std;const int N 30; int st[N];//用来存这个数用没用过&#xff08;1~n个…

redmibook 14 2020 安装 ubuntu

1. 参考博客 # Ubuntu20.10系统安装 -- 小米redmibook pro14 https://zhuanlan.zhihu.com/p/616543561# ubuntu18.04 wifi 问题 https://blog.csdn.net/u012748494/article/details/105421656/# 笔记本电脑安装了Ubuntu系统设置关盖/合盖不挂起/不睡眠 https://blog.csdn.net/…

智慧公厕:打造城市品质生活的必备设施

公共厕所一直是城市管理中不可忽视的一环&#xff0c;而随着智慧科技的发展&#xff0c;智慧公厕逐渐成为改善城市品质生活的利器。智慧公厕作为一种创新的公共卫生设施&#xff0c;其带来的好处不仅体现在对公共厕所的全面监测和高效智慧化管理&#xff0c;更是为市民提供了更…

蓝桥杯第十五届javab组个人总结

javab组 额今天早上打完了得对自己此次比赛做总结&#xff0c;无论是明年还参赛还是研究生蓝桥杯&#xff0c;体验感有点差&#xff0c;第一题其实一开始想手算但怕进位导致不准确还是让代码跑了&#xff0c;但跑第202420242024个数&#xff08;被20和24整除&#xff09;一直把…

算法练习第18天|111.二叉树的最小深度

111.二叉树的最小深度 111. 二叉树的最小深度 - 力扣&#xff08;LeetCode&#xff09;https://leetcode.cn/problems/minimum-depth-of-binary-tree/description/ 题目描述&#xff1a; 给定一个二叉树&#xff0c;找出其最小深度。 最小深度是从根节点到最近叶子节点的最…

bootstrap-select 搜索过滤输入中文问题,前2个字母输入转成空格

bootstrap是v3.3.7的 v1.6.3版本的bootstrap-select,注释以下2行 //that.$menu.find(li).filter(:visible:not(.divider)).eq(0).addClass(active).find(a).focus(); // $(this).focus();