Browse Source

图形验证码组件

leo 1 year ago
parent
commit
a6fe0c382d

BIN
assets/image/default.jpg


File diff suppressed because it is too large
+ 493 - 0
components/verifition/Verify.vue


+ 292 - 0
components/verifition/Verify/VerifyPoints.vue

@@ -0,0 +1,292 @@
+<template>
+  <div style="position: relative">
+    <div class="verify-img-out">
+      <div
+        class="verify-img-panel"
+        :style="{
+          width: setSize.imgWidth,
+          height: setSize.imgHeight,
+          'background-size': setSize.imgWidth + ' ' + setSize.imgHeight,
+          'margin-bottom': vSpace + 'px',
+        }"
+      >
+        <div
+          v-show="showRefresh"
+          class="verify-refresh"
+          style="z-index: 3"
+          @click="refresh"
+        >
+          <i class="iconfont icon-refresh" />
+        </div>
+        <img
+          ref="canvas"
+          :src="
+            pointBackImgBase
+              ? 'data:image/png;base64,' + pointBackImgBase
+              : defaultImg
+          "
+          alt=""
+          style="width: 100%; height: 100%; display: block"
+          @click="bindingClick ? canvasClick($event) : undefined"
+        />
+
+        <div
+          v-for="(tempPoint, index) in tempPoints"
+          :key="index"
+          class="point-area"
+          :style="{
+            'background-color': '#1abd6c',
+            color: '#fff',
+            'z-index': 9999,
+            width: '20px',
+            height: '20px',
+            'text-align': 'center',
+            'line-height': '20px',
+            'border-radius': '50%',
+            position: 'absolute',
+            top: parseInt(tempPoint.y - 10) + 'px',
+            left: parseInt(tempPoint.x - 10) + 'px',
+          }"
+        >
+          {{ index + 1 }}
+        </div>
+      </div>
+    </div>
+    <!-- 'height': this.barSize.height, -->
+    <div
+      class="verify-bar-area"
+      :style="{
+        width: setSize.imgWidth,
+        color: this.barAreaColor,
+        'border-color': this.barAreaBorderColor,
+        'line-height': this.barSize.height,
+      }"
+    >
+      <span class="verify-msg">{{ text }}</span>
+    </div>
+  </div>
+</template>
+<script type="text/babel">
+/**
+ * VerifyPoints
+ * @description 点选
+ * */
+import {
+  resetSize,
+  _code_chars,
+  _code_color1,
+  _code_color2,
+} from './../utils/util';
+import { aesEncrypt } from './../utils/ase';
+
+export default {
+  name: 'VerifyPoints',
+  props: {
+    // 弹出式pop,固定fixed
+    mode: {
+      type: String,
+      default: 'fixed',
+    },
+    captchaType: {
+      type: String,
+    },
+    // 间隔
+    vSpace: {
+      type: Number,
+      default: 5,
+    },
+    imgSize: {
+      type: Object,
+      default() {
+        return {
+          width: '310px',
+          height: '155px',
+        };
+      },
+    },
+    barSize: {
+      type: Object,
+      default() {
+        return {
+          width: '310px',
+          height: '40px',
+        };
+      },
+    },
+    defaultImg: {
+      type: String,
+      default: '',
+    },
+  },
+  data() {
+    return {
+      secretKey: '', // 后端返回的ase加密秘钥
+      checkNum: 3, // 默认需要点击的字数
+      fontPos: [], // 选中的坐标信息
+      checkPosArr: [], // 用户点击的坐标
+      num: 1, // 点击的记数
+      pointBackImgBase: '', // 后端获取到的背景图片
+      poinTextList: [], // 后端返回的点击字体顺序
+      backToken: '', // 后端返回的token值
+      setSize: {
+        imgHeight: 0,
+        imgWidth: 0,
+        barHeight: 0,
+        barWidth: 0,
+      },
+      tempPoints: [],
+      text: '',
+      barAreaColor: undefined,
+      barAreaBorderColor: undefined,
+      showRefresh: true,
+      bindingClick: true,
+    };
+  },
+  computed: {
+    resetSize() {
+      return resetSize;
+    },
+  },
+  watch: {
+    // type变化则全面刷新
+    type: {
+      immediate: true,
+      handler() {
+        this.init();
+      },
+    },
+  },
+  mounted() {
+    // 禁止拖拽
+    this.$el.onselectstart = function () {
+      return false;
+    };
+  },
+  methods: {
+    init() {
+      // 加载页面
+      this.fontPos.splice(0, this.fontPos.length);
+      this.checkPosArr.splice(0, this.checkPosArr.length);
+      this.num = 1;
+      this.getPictrue();
+      this.$nextTick(() => {
+        this.setSize = this.resetSize(this); // 重新设置宽度高度
+        this.$parent.$emit('ready', this);
+      });
+    },
+    canvasClick(e) {
+      this.checkPosArr.push(this.getMousePos(this.$refs.canvas, e));
+      if (this.num == this.checkNum) {
+        this.num = this.createPoint(this.getMousePos(this.$refs.canvas, e));
+        // 按比例转换坐标值
+        this.checkPosArr = this.pointTransfrom(this.checkPosArr, this.setSize);
+        // 等创建坐标执行完
+        setTimeout(() => {
+          // var flag = this.comparePos(this.fontPos, this.checkPosArr);
+          // 发送后端请求
+          var captchaVerification = this.secretKey
+            ? aesEncrypt(
+                this.backToken + '---' + JSON.stringify(this.checkPosArr),
+                this.secretKey,
+              )
+            : this.backToken + '---' + JSON.stringify(this.checkPosArr);
+          const data = {
+            captchaType: this.captchaType,
+            pointJson: this.secretKey
+              ? aesEncrypt(JSON.stringify(this.checkPosArr), this.secretKey)
+              : JSON.stringify(this.checkPosArr),
+            token: this.backToken,
+          };
+          this.$axios.$post('activity/captcha/check', data).then((res) => {
+            if (res.repCode == '0000') {
+              this.barAreaColor = '#4cae4c';
+              this.barAreaBorderColor = '#5cb85c';
+              this.text = '验证成功';
+              this.bindingClick = false;
+              if (this.mode == 'pop') {
+                setTimeout(() => {
+                  this.$parent.clickShow = false;
+                  this.refresh();
+                }, 1500);
+              }
+              this.$parent.$emit('success', { captchaVerification });
+            } else {
+              this.$parent.$emit('error', this);
+              this.barAreaColor = '#d9534f';
+              this.barAreaBorderColor = '#d9534f';
+              this.text = '验证失败';
+              setTimeout(() => {
+                this.refresh();
+              }, 700);
+            }
+          });
+        }, 400);
+      }
+      if (this.num < this.checkNum) {
+        this.num = this.createPoint(this.getMousePos(this.$refs.canvas, e));
+      }
+    },
+
+    // 获取坐标
+    getMousePos: function (obj, e) {
+      var x = e.offsetX;
+      var y = e.offsetY;
+      return { x, y };
+    },
+    // 创建坐标点
+    createPoint: function (pos) {
+      this.tempPoints.push(Object.assign({}, pos));
+      return ++this.num;
+    },
+    refresh: function () {
+      this.tempPoints.splice(0, this.tempPoints.length);
+      this.barAreaColor = '#000';
+      this.barAreaBorderColor = '#ddd';
+      this.bindingClick = true;
+      this.fontPos.splice(0, this.fontPos.length);
+      this.checkPosArr.splice(0, this.checkPosArr.length);
+      this.num = 1;
+      this.getPictrue();
+      this.text = '验证失败';
+      this.showRefresh = true;
+    },
+
+    // 请求背景图片和验证图片
+    getPictrue() {
+      const data = {
+        captchaType: this.captchaType,
+        clientUid: localStorage.getItem('point'),
+        ts: Date.now(), // 现在的时间戳
+      };
+      this.$axios.$post('/captcha/get', data).then((res) => {
+        console.log(res);
+        if (res.repCode == '0000') {
+          console.log(123456);
+          this.pointBackImgBase = res.repData.originalImageBase64;
+          this.backToken = res.repData.token;
+          this.secretKey = res.repData.secretKey;
+          this.poinTextList = res.repData.wordList;
+          this.text = '请依次点击【' + this.poinTextList.join(',') + '】';
+        } else {
+          this.text = res.repMsg;
+        }
+
+        // 判断接口请求次数是否失效
+        if (res.repCode == '6201') {
+          this.pointBackImgBase = null;
+        }
+      });
+    },
+    // 坐标转换函数
+    pointTransfrom(pointArr, imgSize) {
+      var newPointArr = pointArr.map((p) => {
+        const x = Math.round((310 * p.x) / parseInt(imgSize.imgWidth));
+        const y = Math.round((155 * p.y) / parseInt(imgSize.imgHeight));
+        return { x, y };
+      });
+      // console.log(newPointArr,"newPointArr");
+      return newPointArr;
+    },
+  },
+};
+</script>

+ 438 - 0
components/verifition/Verify/VerifySlide.vue

@@ -0,0 +1,438 @@
+<template>
+  <div style="position: relative">
+    <div
+      v-if="type === '2'"
+      class="verify-img-out"
+      :style="{ height: parseInt(setSize.imgHeight) + vSpace + 'px' }"
+    >
+      <div
+        class="verify-img-panel"
+        :style="{ width: setSize.imgWidth, height: setSize.imgHeight }"
+      >
+        <img
+          :src="
+            backImgBase ? 'data:image/png;base64,' + backImgBase : defaultImg
+          "
+          alt=""
+          style="width: 100%; height: 100%; display: block"
+        />
+        <div v-show="showRefresh" class="verify-refresh" @click="refresh">
+          <i class="iconfont icon-refresh" />
+        </div>
+        <transition name="tips">
+          <span
+            v-if="tipWords"
+            class="verify-tips"
+            :class="passFlag ? 'suc-bg' : 'err-bg'"
+            >{{ tipWords }}</span
+          >
+        </transition>
+      </div>
+    </div>
+    <!-- 公共部分 -->
+    <div
+      class="verify-bar-area"
+      :style="{
+        width: setSize.imgWidth,
+        height: barSize.height,
+        'line-height': barSize.height,
+      }"
+    >
+      <span class="verify-msg" v-text="text" />
+      <div
+        class="verify-left-bar"
+        :style="{
+          width: leftBarWidth !== undefined ? leftBarWidth : barSize.height,
+          height: barSize.height,
+          'border-color': leftBarBorderColor,
+          transaction: transitionWidth,
+        }"
+      >
+        <span class="verify-msg" v-text="finishText" />
+        <div
+          class="verify-move-block"
+          :style="{
+            width: barSize.height,
+            height: barSize.height,
+            'background-color': moveBlockBackgroundColor,
+            left: moveBlockLeft,
+            transition: transitionLeft,
+          }"
+          @touchstart="start"
+          @mousedown="start"
+        >
+          <i
+            :class="['verify-icon iconfont', iconClass]"
+            :style="{ color: iconColor }"
+          />
+          <div
+            v-if="type === '2'"
+            class="verify-sub-block"
+            :style="{
+              width: Math.floor((parseInt(setSize.imgWidth) * 47) / 310) + 'px',
+              height: setSize.imgHeight,
+              top: '-' + (parseInt(setSize.imgHeight) + vSpace) + 'px',
+              'background-size': setSize.imgWidth + ' ' + setSize.imgHeight,
+            }"
+          >
+            <img
+              :src="'data:image/png;base64,' + blockBackImgBase"
+              alt=""
+              style="width: 100%; height: 100%; display: block"
+            />
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+<script type="text/babel">
+/**
+ * VerifySlide
+ * @description 滑块
+ * */
+import { aesEncrypt } from './../utils/ase';
+import { resetSize } from './../utils/util';
+
+//  "captchaType":"blockPuzzle",
+export default {
+  name: 'VerifySlide',
+  props: {
+    captchaType: {
+      type: String,
+    },
+    type: {
+      type: String,
+      default: '1',
+    },
+    // 弹出式pop,固定fixed
+    mode: {
+      type: String,
+      default: 'fixed',
+    },
+    vSpace: {
+      type: Number,
+      default: 5,
+    },
+    explain: {
+      type: String,
+      default: '向右滑动完成验证',
+    },
+    imgSize: {
+      type: Object,
+      default() {
+        return {
+          width: '310px',
+          height: '155px',
+        };
+      },
+    },
+    blockSize: {
+      type: Object,
+      default() {
+        return {
+          width: '50px',
+          height: '50px',
+        };
+      },
+    },
+    barSize: {
+      type: Object,
+      default() {
+        return {
+          width: '310px',
+          height: '40px',
+        };
+      },
+    },
+    defaultImg: {
+      type: String,
+      default: '',
+    },
+  },
+  data() {
+    return {
+      secretKey: '', // 后端返回的加密秘钥 字段
+      passFlag: '', // 是否通过的标识
+      backImgBase: '', // 验证码背景图片
+      blockBackImgBase: '', // 验证滑块的背景图片
+      backToken: '', // 后端返回的唯一token值
+      startMoveTime: '', // 移动开始的时间
+      endMovetime: '', // 移动结束的时间
+      tipsBackColor: '', // 提示词的背景颜色
+      tipWords: '',
+      text: '',
+      finishText: '',
+      setSize: {
+        imgHeight: 0,
+        imgWidth: 0,
+        barHeight: 0,
+        barWidth: 0,
+      },
+      top: 0,
+      left: 0,
+      moveBlockLeft: undefined,
+      leftBarWidth: undefined,
+      // 移动中样式
+      moveBlockBackgroundColor: undefined,
+      leftBarBorderColor: '#ddd',
+      iconColor: undefined,
+      iconClass: 'icon-right',
+      status: false, // 鼠标状态
+      isEnd: false, // 是够验证完成
+      showRefresh: true,
+      transitionLeft: '',
+      transitionWidth: '',
+    };
+  },
+  computed: {
+    barArea() {
+      return this.$el.querySelector('.verify-bar-area');
+    },
+    resetSize() {
+      return resetSize;
+    },
+  },
+  watch: {
+    // type变化则全面刷新
+    type: {
+      immediate: true,
+      handler() {
+        this.init();
+      },
+    },
+  },
+  mounted() {
+    // 禁止拖拽
+    this.$el.onselectstart = function () {
+      return false;
+    };
+    console.log(this.defaultImg);
+  },
+  methods: {
+    init() {
+      this.text = this.explain;
+      this.getPictrue();
+      this.$nextTick(() => {
+        const setSize = this.resetSize(this); // 重新设置宽度高度
+        for (const key in setSize) {
+          this.$set(this.setSize, key, setSize[key]);
+        }
+        this.$parent.$emit('ready', this);
+      });
+
+      var _this = this;
+
+      window.removeEventListener('touchmove', function (e) {
+        _this.move(e);
+      });
+      window.removeEventListener('mousemove', function (e) {
+        _this.move(e);
+      });
+
+      // 鼠标松开
+      window.removeEventListener('touchend', function () {
+        _this.end();
+      });
+      window.removeEventListener('mouseup', function () {
+        _this.end();
+      });
+
+      window.addEventListener('touchmove', function (e) {
+        _this.move(e);
+      });
+      window.addEventListener('mousemove', function (e) {
+        _this.move(e);
+      });
+
+      // 鼠标松开
+      window.addEventListener('touchend', function () {
+        _this.end();
+      });
+      window.addEventListener('mouseup', function () {
+        _this.end();
+      });
+    },
+
+    // 鼠标按下
+    start: function (e) {
+      e = e || window.event;
+      if (!e.touches) {
+        // 兼容PC端
+        var x = e.clientX;
+      } else {
+        // 兼容移动端
+        var x = e.touches[0].pageX;
+      }
+      this.startLeft = Math.floor(
+        x - this.barArea.getBoundingClientRect().left,
+      );
+      this.startMoveTime = +new Date(); // 开始滑动的时间
+      if (this.isEnd == false) {
+        this.text = '';
+        this.moveBlockBackgroundColor = '#337ab7';
+        this.leftBarBorderColor = '#337AB7';
+        this.iconColor = '#fff';
+        e.stopPropagation();
+        this.status = true;
+      }
+    },
+    // 鼠标移动
+    move: function (e) {
+      e = e || window.event;
+      if (this.status && this.isEnd == false) {
+        if (!e.touches) {
+          // 兼容PC端
+          var x = e.clientX;
+        } else {
+          // 兼容移动端
+          var x = e.touches[0].pageX;
+        }
+        var bar_area_left = this.barArea.getBoundingClientRect().left;
+        var move_block_left = x - bar_area_left; // 小方块相对于父元素的left值
+        if (
+          move_block_left >=
+          this.barArea.offsetWidth -
+            parseInt(parseInt(this.blockSize.width) / 2) -
+            2
+        ) {
+          move_block_left =
+            this.barArea.offsetWidth -
+            parseInt(parseInt(this.blockSize.width) / 2) -
+            2;
+        }
+        if (move_block_left <= 0) {
+          move_block_left = parseInt(parseInt(this.blockSize.width) / 2);
+        }
+        // 拖动后小方块的left值
+        this.moveBlockLeft = move_block_left - this.startLeft + 'px';
+        this.leftBarWidth = move_block_left - this.startLeft + 'px';
+      }
+    },
+
+    // 鼠标松开
+    end: function () {
+      this.endMovetime = +new Date();
+      var _this = this;
+      // 判断是否重合
+      if (this.status && this.isEnd == false) {
+        var moveLeftDistance = parseInt(
+          (this.moveBlockLeft || '').replace('px', ''),
+        );
+        moveLeftDistance =
+          (moveLeftDistance * 310) / parseInt(this.setSize.imgWidth);
+        const data = {
+          captchaType: this.captchaType,
+          pointJson: this.secretKey
+            ? aesEncrypt(
+                JSON.stringify({ x: moveLeftDistance, y: 5.0 }),
+                this.secretKey,
+              )
+            : JSON.stringify({ x: moveLeftDistance, y: 5.0 }),
+          token: this.backToken,
+        };
+        this.$axios.$post('activity/captcha/check', data).then((res) => {
+          if (res.repCode == '0000') {
+            this.moveBlockBackgroundColor = '#5cb85c';
+            this.leftBarBorderColor = '#5cb85c';
+            this.iconColor = '#fff';
+            this.iconClass = 'icon-check';
+            this.showRefresh = false;
+            this.isEnd = true;
+            if (this.mode == 'pop') {
+              setTimeout(() => {
+                this.$parent.clickShow = false;
+                this.refresh();
+              }, 1500);
+            }
+            this.passFlag = true;
+            this.tipWords = `${(
+              (this.endMovetime - this.startMoveTime) /
+              1000
+            ).toFixed(2)}s验证成功`;
+            var captchaVerification = this.secretKey
+              ? aesEncrypt(
+                  this.backToken +
+                    '---' +
+                    JSON.stringify({ x: moveLeftDistance, y: 5.0 }),
+                  this.secretKey,
+                )
+              : this.backToken +
+                '---' +
+                JSON.stringify({ x: moveLeftDistance, y: 5.0 });
+            setTimeout(() => {
+              this.tipWords = '';
+              this.$parent.closeBox();
+              this.$parent.$emit('success', { captchaVerification });
+            }, 1000);
+          } else {
+            this.moveBlockBackgroundColor = '#d9534f';
+            this.leftBarBorderColor = '#d9534f';
+            this.iconColor = '#fff';
+            this.iconClass = 'icon-close';
+            this.passFlag = false;
+            setTimeout(function () {
+              _this.refresh();
+            }, 1000);
+            this.$parent.$emit('error', this);
+            this.tipWords = '验证失败';
+            setTimeout(() => {
+              this.tipWords = '';
+            }, 1000);
+          }
+        });
+        this.status = false;
+      }
+    },
+
+    refresh: function () {
+      this.showRefresh = true;
+      this.finishText = '';
+
+      this.transitionLeft = 'left .3s';
+      this.moveBlockLeft = 0;
+
+      this.leftBarWidth = undefined;
+      this.transitionWidth = 'width .3s';
+
+      this.leftBarBorderColor = '#ddd';
+      this.moveBlockBackgroundColor = '#fff';
+      this.iconColor = '#000';
+      this.iconClass = 'icon-right';
+      this.isEnd = false;
+
+      this.getPictrue();
+      setTimeout(() => {
+        this.transitionWidth = '';
+        this.transitionLeft = '';
+        this.text = this.explain;
+      }, 300);
+    },
+
+    // 请求背景图片和验证图片
+    getPictrue() {
+      const data = {
+        captchaType: this.captchaType,
+        clientUid: localStorage.getItem('slider'),
+        ts: Date.now(), // 现在的时间戳
+      };
+      this.$axios.$post('activity/captcha/get', data).then((res) => {
+        if (res.repCode == '0000') {
+          this.backImgBase = res.repData.originalImageBase64;
+          this.blockBackImgBase = res.repData.jigsawImageBase64;
+          this.backToken = res.repData.token;
+          this.secretKey = res.repData.secretKey;
+        } else {
+          this.tipWords = res.repMsg;
+        }
+
+        // 判断接口请求次数是否失效
+        if (res.repCode == '6201') {
+          this.backImgBase = null;
+          this.blockBackImgBase = null;
+        }
+      });
+    },
+  },
+};
+</script>

+ 11 - 0
components/verifition/utils/ase.js

@@ -0,0 +1,11 @@
+import CryptoJS from 'crypto-js'
+/**
+ * @word 要加密的内容
+ * @keyWord String  服务器随机返回的关键字
+ *  */
+export function aesEncrypt(word, keyWord = 'XwKsGlMcdPMEhR1B') {
+  var key = CryptoJS.enc.Utf8.parse(keyWord)
+  var srcs = CryptoJS.enc.Utf8.parse(word)
+  var encrypted = CryptoJS.AES.encrypt(srcs, key, { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7 })
+  return encrypted.toString()
+}

+ 36 - 0
components/verifition/utils/util.js

@@ -0,0 +1,36 @@
+export function resetSize(vm) {
+  var img_width, img_height, bar_width, bar_height	// 图片的宽度、高度,移动条的宽度、高度
+
+  var parentWidth = vm.$el.parentNode.offsetWidth || window.offsetWidth
+  var parentHeight = vm.$el.parentNode.offsetHeight || window.offsetHeight
+
+  if (vm.imgSize.width.indexOf('%') != -1) {
+    img_width = parseInt(this.imgSize.width) / 100 * parentWidth + 'px'
+  } else {
+    img_width = this.imgSize.width
+  }
+
+  if (vm.imgSize.height.indexOf('%') != -1) {
+    img_height = parseInt(this.imgSize.height) / 100 * parentHeight + 'px'
+  } else {
+    img_height = this.imgSize.height
+  }
+
+  if (vm.barSize.width.indexOf('%') != -1) {
+    bar_width = parseInt(this.barSize.width) / 100 * parentWidth + 'px'
+  } else {
+    bar_width = this.barSize.width
+  }
+
+  if (vm.barSize.height.indexOf('%') != -1) {
+    bar_height = parseInt(this.barSize.height) / 100 * parentHeight + 'px'
+  } else {
+    bar_height = this.barSize.height
+  }
+
+  return { imgWidth: img_width, imgHeight: img_height, barWidth: bar_width, barHeight: bar_height }
+}
+
+export const _code_chars = [1, 2, 3, 4, 5, 6, 7, 8, 9, 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']
+export const _code_color1 = ['#fffff0', '#f0ffff', '#f0fff0', '#fff0f0']
+export const _code_color2 = ['#FF0033', '#006699', '#993366', '#FF9900', '#66CC66', '#FF33CC']