zoukankan      html  css  js  c++  java
  • 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="{ 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 = '' + 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; 310px; overflow: hidden; } .slide-verify-block { position: absolute; left: 0; top: 0; } .slide-verify-refresh-icon { position: absolute; right: 0; top: 0; 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; 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; 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; 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; 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- 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.如果滑动不成功,自动刷新图片,重置拼图,滑动成功,且账号密码正确就直接跳转到首页

  • 相关阅读:
    [2017BUAA软工助教]结对项目小结
    DPDK flow_filtering 源码阅读
    DPDK flow_classify 源码阅读
    阅读源代码,查出某个宏定义在哪个头文件内的方法
    pktgen-dpdk 实战
    pktgen-dpdk 运行 run.py 报错 Config file 'default' not found 解决方法
    DPDK RX / TX Callbacks 源码阅读
    DPDK skeleton basicfwd 源码阅读
    DPDK helloworld 源码阅读
    DPDK实例程序:testpmd
  • 原文地址:https://www.cnblogs.com/steamed-twisted-roll/p/12841483.html
Copyright © 2011-2022 走看看