vue项目图片滑动验证码 前端+后端验证

之前项目登录时填写的是验证码,后来说要与时俱进,改成滑动图片的方式
在这里插入图片描述
这里的背景图和滑块是由后台返回的,前端传回移动距离给后端验证,这里我只写前端处理的部分的(毕竟后端的也不懂)

项目源代码:githup地址https://github.com/shengbid/vue-demo/tree/master/src/views/login 这个项目是最近在整理之前写的博客的一些案例的代码,里面还有一些其他的功能,后续也会继续完善,有需要可以下载来看下,有帮助的话记得star哦

写成一个组件,

captha.vue

<template>
  <div id="slideVerify" class="slide-verify" :style="widthlable" onselectstart="return false;">
    <canvas ref="canvas" :width="w" :height="h" />

    <canvas ref="block" class="slide-verify-block" :width="w" :height="h" />
    <div class="slide-verify-refresh-icon el-icon-refresh" @click="refresh" />
    <div class="slide-verify-info" :class="{fail: fail, show: showInfo}" @click="refresh">{{ infoText }}</div>
    <div
      class="slide-verify-slider"
      :style="widthlable"
      :class="{'container-active': containerActive, 'container-success': containerSuccess, 'container-fail': containerFail}"
    >
      <div class="slide-verify-slider-mask" :style="{width: sliderMaskWidth}">
        <!-- slider -->
        <div
          class="slide-verify-slider-mask-item"
          :style="{left: sliderLeft}"
          @mousedown="sliderDown"
          @touchstart="touchStartEvent"
          @touchmove="touchMoveEvent"
          @touchend="touchEndEvent"
        >
          <div class="slide-verify-slider-mask-item-icon el-icon-s-unfold" />
        </div>
      </div>
      <span class="slide-verify-slider-text">{{ sliderText }}</span>
    </div>
  </div>
</template>
<script>
function sum(x, y) {
  return x + y
}

function square(x) {
  return x * x
}
export default {
  name: 'SlideVerify',
  props: {
    // block length
    l: {
      type: Number,
      default: 42
    },
    // block radius
    r: {
      type: Number,
      default: 10
    },
    // canvas width
    w: { // 背景图宽
      type: [Number, String],
      default: 350
    },
    // canvas height
    h: { // 背景图高
      type: [Number, String],
      default: 200
    },
    // canvas width
    sw: { // 小图宽
      type: [Number, String],
      default: 50
    },
    // canvas height
    sh: {
      type: [Number, String],
      default: 50
    },
    // block_x: {
    //   type: Number,
    //   default: 155
    // },
    blocky: { // 小图初始的垂直距离
      type: [Number, String],
      default: 20
    },
    sliderText: {
      type: String,
      default: 'Slide filled right'
    },
    imgurl: { // 大图地址
      type: String,
      default: ''
    },
    miniimgurl: { // 小图地址
      type: String,
      default: ''
    },
    fresh: {
      type: Boolean,
      default: false
    }
  },

  data() {
    return {
      containerActive: false, // container active class
      containerSuccess: false, // container success class
      containerFail: false, // container fail class
      canvasCtx: null,
      blockCtx: null,
      block: null,
      canvasStr: null,
      // block_x: undefined, // container random position
      // blocky: undefined,
      L: this.l + this.r * 2 + 3, // block real lenght
      img: undefined,
      originX: undefined,
      originY: undefined,
      isMouseDown: false,
      trail: [],
      widthlable: '',
      sliderLeft: 0, // block right offset
      sliderMaskWidth: 0, // mask width
      dialogVisible: false,
      infoText: '验证成功',
      fail: false,
      showInfo: false
    }
  },
  watch: {
    fresh(val) {
      if (val) {
        this.init()
      }
    }
  },
  mounted() {
    // 随机生成数this.block_x =
    this.init()
  },
  methods: {
    init() {
      this.initDom()
      this.bindEvents()
      this.widthlable = 'width:' + this.w + 'px;'
    },
    initDom() {
      this.block = this.$refs.block
      this.canvasStr = this.$refs.canvas

      this.canvasCtx = this.canvasStr.getContext('2d')
      this.blockCtx = this.block.getContext('2d')
      this.initImg()
    },
    initImg(h) {
      var that = this
      const img = document.createElement('img')
      img.onload = onload
      img.onerror = () => {
        img.src = that.imgurl
      }
      img.src = that.imgurl
      img.onload = function() {
        that.canvasCtx.drawImage(img, 0, 0, that.w, that.h)
      }

      this.img = img
      const img1 = document.createElement('img')
      var blockCtx = that.blockCtx
      var blocky = h || that.blocky
      if (blocky === 0) {
        return
      }
      img1.onerror = () => {
        img1.src = that.miniimgurl
      }
      img1.src = that.miniimgurl
      img1.onload = function() {
        // blockCtx.drawImage(img1, 0, blocky, that.sw, that.sh)
        blockCtx.drawImage(img1, 0, blocky, 55, 45)
      }
      // console.log(777, h)
    },  // 刷新
    refresh() {
      this.$emit('refresh')
    },
    sliderDown(event) {
      this.originX = event.clientX
      this.originY = event.clientY
      this.isMouseDown = true
    },
    touchStartEvent(e) {
      this.originX = e.changedTouches[0].pageX
      this.originY = e.changedTouches[0].pageY
      this.isMouseDown = true
    },
    bindEvents() {
      document.addEventListener('mousemove', e => {
        if (!this.isMouseDown) return false
        const moveX = e.clientX - this.originX
        const moveY = e.clientY - this.originY
        if (moveX < 0 || moveX + 38 >= this.w) return false
        this.sliderLeft = moveX + 'px'
        const blockLeft = ((this.w - 40 - 20) / (this.w - 40)) * moveX
        this.block.style.left = blockLeft + 'px'
        this.containerActive = true // add active
        this.sliderMaskWidth = moveX + 'px'
        this.trail.push(moveY)
      })
      document.addEventListener('mouseup', e => {
        if (!this.isMouseDown) return false
        this.isMouseDown = false
        if (e.clientX === this.originX) return false
        this.containerActive = false // remove active
        this.verify()
      })
    },
    touchMoveEvent(e) {
      if (!this.isMouseDown) return false
      const moveX = e.changedTouches[0].pageX - this.originX
      const moveY = e.changedTouches[0].pageY - this.originY
      if (moveX < 0 || moveX + 38 >= this.w) return false
      this.sliderLeft = moveX + 'px'
      const blockLeft = ((this.w - 40 - 20) / (this.w - 40)) * moveX
      this.block.style.left = blockLeft + 'px'

      this.containerActive = true
      this.sliderMaskWidth = moveX + 'px'
      this.trail.push(moveY)
    },
    touchEndEvent(e) {
      if (!this.isMouseDown) return false
      this.isMouseDown = false
      if (e.changedTouches[0].pageX === this.originX) return false
      this.containerActive = false
      this.verify()
    },
    verify() {
      const arr = this.trail // drag y move distance
      const average = arr.reduce(sum) / arr.length // average
      const deviations = arr.map(x => x - average) // deviation array
      const stddev = Math.sqrt(deviations.map(square).reduce(sum) / arr.length) // standard deviation
      const left = parseInt(this.block.style.left)
      this.$emit('success', left, stddev)
    },
    reset(h) {
      this.containerActive = false
      this.containerSuccess = false
      this.containerFail = false
      this.sliderLeft = 0
      this.block.style.left = 0
      this.sliderMaskWidth = 0
      this.canvasCtx.clearRect(0, 0, this.w, this.h)
      this.blockCtx.clearRect(0, 0, this.w, this.h)
      this.fail = false
      this.showInfo = false
      this.containerFail = false
      this.containerSuccess = false
      this.initImg(h)
    },
    handleFail() {
      this.fail = true
      this.showInfo = true
      this.infoText = '验证失败'
      this.containerFail = true
      // console.log(6666)
      // setTimeout(() => {
      //   this.block.style.left = 0
      //   this.sliderMaskWidth = 0
      //   this.sliderLeft = 0
      //   this.fail = false
      //   this.showInfo = false
      //   this.containerFail = false
      // }, 800)
    },
    handleSuccess() {
      // console.log(777)
      this.showInfo = true
      this.infoText = '验证成功'
      this.containerSuccess = true
      setTimeout(() => {
        this.block.style.left = 0
        this.sliderMaskWidth = 0
        this.sliderLeft = 0
        this.fail = false
        this.showInfo = false
        this.containerSuccess = false
      }, 1000)
    }
  }
}
</script>
<style scoped>
  .slide-verify {
    position: relative;
    width: 310px;
    overflow: hidden;
  }

  .slide-verify-block {
    position: absolute;
    left: 0;
    top: 0;
  }

  .slide-verify-refresh-icon {
    position: absolute;
    right: 0;
    top: 0;
    width: 34px;
    height: 34px;
    cursor: pointer;
    content: '刷新';
    font-size: 22px;
    line-height: 34px;
    text-align: center;
    font-weight: bold;
    color: #3fdeae;
    /* background: url("../../assets/move/icon_light.png") 0 -437px; */
    background-size: 34px 471px;
  }
  .slide-verify-refresh-icon:hover {
    transform: rotate(180deg);
    transition: all 0.2s ease-in-out;
  }

  .slide-verify-slider {
    position: relative;
    text-align: center;
    width: 310px;
    height: 40px;
    line-height: 40px;
    margin-top: 15px;
    background: #f7f9fa;
    color: #45494c;
    border: 1px solid #e4e7eb;
  }

  .slide-verify-slider-mask {
    position: absolute;
    left: 0;
    top: 0;
    height: 40px;
    border: 0 solid #1991fa;
    background: #d1e9fe;
  }

  .slide-verify-info {
    position: absolute;
    top: 170px;
    left: 0;
    height: 30px;
    width: 350px;
    color: #fff;
    text-align: center;
    line-height: 30px;
    background-color: #52ccba;
    opacity: 0;
  }
  .slide-verify-info.fail {
    background-color: #f57a7a;
  }
  .slide-verify-info.show {
    animation: hide 1s ease;
  }
  @keyframes hide {
    0% {opacity: 0;}
    100% {opacity: 0.9;}
  }
  .slide-verify-slider-mask-item {
    position: absolute;
    top: 0;
    left: 0;
    width: 38px;
    height: 38px;
    background: #fff;
    box-shadow: 0 0 3px rgba(0, 0, 0, 0.3);
    cursor: pointer;
    transition: background 0.2s linear;
  }

  .slide-verify-slider-mask-item:hover {
    background: #1991fa;
  }

  .slide-verify-slider-mask-item:hover .slide-verify-slider-mask-item-icon {
    background-position: 0 -13px;
  }

  .slide-verify-slider-mask-item-icon {
    position: absolute;
    top: 9px;
    left: 7px;
    width: 14px;
    height: 12px;
    content: '法币';
    font-size: 22px;
    color: #ddd;
    /* text-align: center;
    line-height: 12px; */
    /* background: url("../../assets/move/icon_light.png") 0 -26px; */
    /* background-size: 34px 471px; */
  }
  .container-active .slide-verify-slider-mask-item {
    height: 38px;
    top: -1px;
    border: 1px solid #1991fa;
  }

  .container-active .slide-verify-slider-mask {
    height: 38px;
    border-width: 1px;
  }

  .container-success .slide-verify-slider-mask-item {
    height: 38px;
    top: -1px;
    border: 1px solid #52ccba;
    background-color: #52ccba !important;
  }

  .container-success .slide-verify-slider-mask {
    height: 38px;
    border: 1px solid #52ccba;
    background-color: #d2f4ef;
  }

  .container-success .slide-verify-slider-mask-item-icon {
    background-position: 0 0 !important;
  }

  .container-fail .slide-verify-slider-mask-item {
    height: 38px;
    top: -1px;
    border: 1px solid #f57a7a;
    background-color: #f57a7a !important;
  }

  .container-fail .slide-verify-slider-mask {
    height: 38px;
    border: 1px solid #f57a7a;
    background-color: #fce1e1;
  }

  .container-fail .slide-verify-slider-mask-item-icon {
    top: 14px;
    background-position: 0 -82px !important;
  }

  .container-active .slide-verify-slider-text,
  .container-success .slide-verify-slider-text,
  .container-fail .slide-verify-slider-text {
    display: none;
  }
</style>

父组件

login.vue

<template>
  <div class="login-container">
        <!-- 验证码弹框 -->
        <el-dialog width="390px" append-to-body :visible.sync="dialogVisible" :show-close="false" :close-on-click-modal="false">
      <Captcha
        ref="dialogopen"
        :l="42"
        :r="10"
        :w="catcha.w"
        :h="catcha.h"
        :blocky="catcha.blocky"
        :imgurl="catcha.imgurl"
        :miniimgurl="catcha.miniimgurl"
        :slider-text="catcha.text"
        @success="onSuccess"
        @fail="onFail"
        @refresh="onRefresh"
      />
    </el-dialog>
  </div>
</template> 
   
<script>
import Captcha from '@/components/Captcha/newcap'
import { firstLogin, forgetUpdPwd, sendEmailCode, checkLogins, getKaptcha, getKaptchaImg } from '@/api/user'

export default {
  name: 'Login',
  components: { Captcha },
  data() {
    return {
      loginForm: {
        username: '',
        password: '',
        distance: ''
      },
      loginRules: {
        username: [{ required: true, message: '账号必填', trigger: 'blur' }],
        password: [{ required: true, message: '密码必填', trigger: 'blur' }],
        captchaCode: [{ required: true, message: '验证码必填', trigger: 'blur' }]
      },
      passwordType: 'password',
      capsTooltip: false,
      loading: false,
      showDialog: false,
      redirect: undefined,
      otherQuery: {},
      dialogVisible: false, // 验证码弹框
      catcha: {
        blocky: 0,
        imgurl: '',
        miniimgurl: '',
        text: '向右滑动完成拼图',
        h: 200,
        w: 350,
        sh: 45,
        sw: 55,
        modifyImg: ''
      } // 图片验证码传值
    }
  },
  created() {
  },
  mounted() {
  },
  methods: {
    // 点击登录
    handleLogin() {
      // this.toLogin()
      this.$refs.loginForm.validate(valid => {
        if (valid) {
          this.loading = true
          checkLogins(this.loginForm.username)
            .then(response => {
              this.loading = false
              if (response.data < 3) {
                this.toLogin()
              } else {
                // 登陆错误超过三次
                this.getImageVerifyCode()
                setTimeout(() => {
                  this.dialogVisible = true
                }, 500)
              }
            })
            .catch(res => {
              this.loading = false
            })
        } else {
          console.log('error submit!!')
          return false
        }
      })
    },
    toLogin() {
      this.$store
        .dispatch('user/login', this.loginForm)
        .then(response => {
          this.loading = false
          if (response.responseCode === '000000') {
            setTimeout(() => {
              this.$router.push({ path: this.redirect || '/', query: this.otherQuery })
            }, 200)
          }
        })
        .catch(res => {
          // console.log(res)
          if (res.responseCode && this.dialogVisible) {
            this.dialogVisible = false
          }
          this.loading = false
        })
    },

    // 获取图形验证码
    getImageVerifyCode() {
      getKaptchaImg().then(res => {
        if (res && res.data) {
          // console.log(res, this.$refs.dialogopen)
          var imgobj = res.data
          this.catcha.blocky = imgobj.puzzleYAxis
          this.catcha.imgurl = 'data:image/png;base64,' + imgobj.modifyImg
          this.catcha.miniimgurl = 'data:image/png;base64,' + imgobj.puzzleImg
          this.$nextTick(() => {
            if (this.$refs.dialogopen) {
              this.$refs.dialogopen.reset(imgobj.puzzleYAxis)
            }
          })
        }
      })
    },
    onFail() {
      console.log('fail')
    },
    onSuccess(left) {
      this.loginForm.distance = left
      // console.log('succss', left)
      // 验证是否成功checkKaptchaImg是后台验证接口方法    
      checkKaptchaImg(left).then(res => {
        if (res.data) {
          this.$refs.dialogopen.handleSuccess()
          setTimeout(() => {
            this.dialogVisible = false
            this.imgurl = ''
            this.miniimgurl = ''
            this.loginForm.distance = left
            this.toLogin()
          }, 1000)
        } else {
          this.$refs.dialogopen.handleFail()
          setTimeout(() => {
            this.getImageVerifyCode()
          }, 500)
        }
      }).catch(() => {})
    },

    // 刷新
    onRefresh() {
      this.imgurl = ''
      this.miniimgurl = ''
      this.getImageVerifyCode()
    }
  }
}
</script>

总结一下思路:

1.小拼图的初始位置y,小拼图的图片,背景图片是从后台获取

2.点击登录按钮,先调取后台接口验证这个账号登录错误是否超过3次,超过三次展示拼图弹出框,调取后台接口获取位置,图片地址

3.图片滑动之后把滑动的距离left传过来,调取后台接口验证是否滑动成功,成功调取登录接口,此时需要把left距离参数也传过去,为了安全,验证距离这一步也可以放在登录接口里一起验证,具体看你们的业务场景

4.如果滑动不成功,自动刷新图片,重置拼图,滑动成功,且账号密码正确就直接跳转到首页

  • 2
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
### 回答1: 企业展示小程序v5.0.0的前端后端都是指该小程序的开发部分。前端是指用户在使用小程序时所看到的界面和交互部分,后端则是指运行在服务器端的程序,负责处理用户请求、数据库操作等。 企业展示小程序v5.0.0的前端主要负责展示企业的相关信息,包括企业的介绍、产品或服务的展示等。前端的开发需要使用前端技术框架,如Vue.js、React等,通过HTML、CSS、JavaScript等语言实现页面的布局、样式和交互效果。同时,在前端开发过程中,还需要通过接口与后端进行数据交互,并将服务器返回的数据实时展示在小程序界面上。 而企业展示小程序v5.0.0的后端主要负责处理前端的请求,进行数据存储和数据库操作等。后端开发通常使用Node.js作为主要的后端开发语言,结合Express、Koa等框架进行开发,实现服务器端的业务逻辑。在后端开发过程中,需要与前端进行数据交互,通过接口来接收前端传递的数据,并经过处理后将结果返回给前端展示。 综上所述,企业展示小程序v5.0.0的前端后端是相互配合的,前端负责展示界面和交互效果,后端负责处理请求和数据操作。只有前端后端的有效组合和协调配合,才能实现一个完善的企业展示小程序。 ### 回答2: 企业展示小程序v5.0.0的前端后端是指该小程序的开发架构和技术实现。 前端部分主要涉及小程序的界面设计和用户交互,以及与后端的数据交互。在v5.0.0版本中,前端可能采用了一些常见的前端框架,如Vue.js或React.js,用来实现小程序的页面布局和组件的开发。这些框架可以提高前端的开发效率和优化用户体验。同时,前端还会利用小程序提供的API,实现用户的登录、数据的请求和展示等功能。 后端部分则主要负责处理前端发送的请求,并返回相应的数据。后端可能使用了一种服务器框架,如Node.js或者Java等,用来处理业务逻辑和数据存取。后端还可能会与数据库进行交互,如MySQL或MongoDB等,用来存储和管理企业展示小程序的相关数据。同时,为了提高小程序的性能和并发处理能力,后端还可能采用一些缓存技术和负载均衡等手段。 整个企业展示小程序的前端后端是协同工作的:前端提供了用户友好的界面和良好的用户体验,通过调用后端的接口来获取数据;后端则处理前端的请求,从数据库中获取相应的数据,并将数据返回给前端进行展示。通过前端后端的配合工作,使企业展示小程序能够实现同步更新、快速响应和良好的用户体验。 综上所述,企业展示小程序v5.0.0的前端后端是相互配合的技术实现部分,用于实现小程序的界面设计、用户交互和数据处理。 ### 回答3: 企业展示小程序v5.0.0的前端后端是指该小程序在更新到版本5.0.0之后所进行的前端后端的开发工作。 前端开发是指为小程序设计和实现用户界面的过程。在企业展示小程序v5.0.0中,前端开发人员负责根据设计需求和产品经理的要求,使用前端开发技术(如HTML、CSS、JavaScript等)来开发小程序的界面,包括页面布局、样式设计、交互逻辑等。他们还需要与设计团队和产品经理密切合作,确保界面设计的一致性和用户体验的良好。 后端开发是指为小程序提供数据和服务支持的过程。在企业展示小程序v5.0.0中,后端开发人员负责开发和维护小程序的服务器端逻辑。他们使用后端开发技术(如Node.js、Java等),与前端开发人员合作,实现小程序的业务逻辑,处理用户请求,提供数据接口等。后端开发还需要关注小程序的性能和安全性,确保系统的稳定和数据的安全。 为了达到企业展示小程序v5.0.0的设计目标和用户需求,前后端开发人员需要密切合作,进行沟通和协作。前端开发人员根据产品需求和设计稿进行界面开发,将用户的交互操作和页面展示效果实现;后端开发人员根据前端开发人员的需求和接口设计,提供数据支持和服务端逻辑处理。两者的协作使得企业展示小程序v5.0.0能够在用户界面和功能上得到完善和优化。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值