前端验证码

一、基础验证码

gVerify.js:

!(function (window, document) {
  function GVerify(options) { //创建一个图形验证码对象,接收options对象为参数
    this.options = { //默认options参数值
      id: "", //容器Id
      canvasId: "verifyCanvas", //canvas的ID
      width: "100", //默认canvas宽度
      height: "30", //默认canvas高度
      type: "blend", //图形验证码默认类型blend:数字字母混合类型、number:纯数字、letter:纯字母
      code: ""
    }

    if (Object.prototype.toString.call(options) == "[object Object]") {//判断传入参数类型
      for (var i in options) { //根据传入的参数,修改默认参数值
        this.options[i] = options[i];
      }
    } else {
      this.options.id = options;
    }

    this.options.numArr = "0,1,2,3,4,5,6,7,8,9".split(",");
    this.options.letterArr = getAllLetter();

    this._init();
    this.refresh();
  }

  GVerify.prototype = {
    /**版本号**/
    version: '1.0.0',

    /**初始化方法**/
    _init: function () {
      var con = document.getElementById(this.options.id);
      var canvas = document.createElement("canvas");
      this.options.width = con.offsetWidth > 0 ? con.offsetWidth : "100";
      this.options.height = con.offsetHeight > 0 ? con.offsetHeight : "30";
      canvas.id = this.options.canvasId;
      canvas.innerHTML = "您的浏览器版本不支持canvas";
      canvas.width = this.options.width;
      canvas.height = this.options.height;
      canvas.style.cursor = "pointer";
      canvas.style.marginTop = '2px';
      canvas.style.width = "100px";
      canvas.style.height = "38px";

      con.appendChild(canvas);
      var parent = this;
      canvas.onclick = function () {
        parent.refresh();
      }
    },

    /**生成验证码**/
    refresh: function () {
      this.options.code = "";
      var canvas = document.getElementById(this.options.canvasId);
      if (canvas.getContext) {
        var ctx = canvas.getContext('2d');
      } else {
        alert('您的浏览器版本不支持canvas,请使用IE9以上版本的浏览器!');
        return;
      }

      ctx.textBaseline = "middle";

      ctx.fillStyle = randomColor(180, 240);
      ctx.fillRect(0, 0, this.options.width, this.options.height);

      if (this.options.type == "blend") { //判断验证码类型
        var txtArr = this.options.numArr.concat(this.options.letterArr);
      } else if (this.options.type == "number") {
        var txtArr = this.options.numArr;
      } else {
        var txtArr = this.options.letterArr;
      }
      /**生成四个字符**/
      for (var i = 1; i <= 4; i++) {
        var txt = txtArr[randomNum(0, txtArr.length)];
        this.options.code += txt;
        ctx.font = randomNum(this.options.height / 2, this.options.height) + 'px SimHei'; //随机生成字体大小
        ctx.fillStyle = randomColor(50, 160); //随机生成字体颜色
        ctx.shadowOffsetX = randomNum(-3, 3);
        ctx.shadowOffsetY = randomNum(-3, 3);
        ctx.shadowBlur = randomNum(-3, 3);
        ctx.shadowColor = "rgba(0, 0, 0, 0.3)";
        var x = this.options.width / 5 * i - 5; //根据需要向左偏移5px
        var y = this.options.height / 2;
        var deg = randomNum(-30, 30);
        /**设置旋转角度和坐标原点**/
        ctx.translate(x, y);
        ctx.rotate(deg * Math.PI / 180);
        ctx.fillText(txt, 0, 0);
        /**恢复旋转角度和坐标原点**/
        ctx.rotate(-deg * Math.PI / 180);
        ctx.translate(-x, -y);
      }
      /**绘制干扰线**/
      for (var i = 0; i < 4; i++) {
        ctx.strokeStyle = randomColor(40, 180);
        ctx.beginPath();
        ctx.moveTo(randomNum(0, this.options.width), randomNum(0, this.options.height));
        ctx.lineTo(randomNum(0, this.options.width), randomNum(0, this.options.height));
        ctx.stroke();
      }
      /**绘制干扰点**/
      for (var i = 0; i < this.options.width / 4; i++) {
        ctx.fillStyle = randomColor(0, 255);
        ctx.beginPath();
        ctx.arc(randomNum(0, this.options.width), randomNum(0, this.options.height), 1, 0, 2 * Math.PI);
        ctx.fill();
      }
    },

    /**验证验证码**/
    validate: function (code) {
      var code = code.toLowerCase();
      var v_code = this.options.code.toLowerCase();

      if (code == v_code) {
        return true;
      } else {
        this.refresh();
        return false;
      }
    }
  }
  /**生成字母数组**/
  // function getAllLetter() {
  // 	var letterStr = "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";
  // 	return letterStr.split(",");
  // }

  /**生成字母和汉字数组**/
  function getAllLetter() {
    var letterStr = "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";
    var hanziStr = chineseUnicode; //	chineseUnicode为unicode.js里面的变量
    var str = letterStr + "," + hanziStr;
    return str.split(",");
  }

  /**生成一个随机数**/
  function randomNum(min, max) {
    return Math.floor(Math.random() * (max - min) + min);
  }

  /**生成一个随机色**/
  function randomColor(min, max) {
    var r = randomNum(min, max);
    var g = randomNum(min, max);
    var b = randomNum(min, max);
    return "rgb(" + r + "," + g + "," + b + ")";
  }

  window.GVerify = GVerify;
})(window, document);

验证码添加汉字:

/*常用汉字的 unicode 编码*/
/*如果要使验证码包含汉字,请配置*/
/*
    河东太上原党三川郡南颍九阳江泗巨水鹿齐琅会邪稽汉蜀中巴陇北西地云雁沙门谷代渔右辽平海桂象邯林郸砀薛长
    \u6cb3,\u4e1c,\u592a,\u4e0a,\u539f,\u515a,\u4e09,\u5ddd,\u90e1,\u5357,\u988d,\u4e5d,\u9633,\u6c5f,\u6cd7,\u5de8,\u6c34,\u9e7f,\u9f50,\u7405,\u4f1a,\u90aa,\u7a3d,\u6c49,\u8700,\u4e2d,\u5df4,\u9647,\u5317,\u897f,\u5730,\u4e91,\u96c1,\u6c99,\u95e8,\u8c37,\u4ee3,\u6e14,\u53f3,\u8fbd,\u5e73,\u6d77,\u6842,\u8c61,\u90af,\u6797,\u90f8,\u7800,\u859b,\u957f
*/
/*unicode 编码*/
//let chineseUnicode = "\u6cb3,\u4e1c,\u592a,\u4e0a,\u539f,\u515a,\u4e09,\u5ddd,\u90e1,\u5357,\u988d,\u4e5d,\u9633,\u6c5f,\u6cd7,\u5de8,\u6c34,\u9e7f,\u9f50,\u7405,\u4f1a,\u90aa,\u7a3d,\u6c49,\u8700,\u4e2d,\u5df4,\u9647,\u5317,\u897f,\u5730,\u4e91,\u96c1,\u6c99,\u95e8,\u8c37,\u4ee3,\u6e14,\u53f3,\u8fbd,\u5e73,\u6d77,\u6842,\u8c61,\u90af,\u6797,\u90f8,\u7800,\u859b,\u957f";
let chineseUnicode = "";

登录页面使用gVerify.js: 

<!DOCTYPE html >
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
  <!--<title th:text="${#themes.code('cas.page.title')}"></title> 存在中文乱码问题,-->
  <!--所以改为在messages_zh_CN.properties文件取-->
  <title th:text="#{screen.login.header}">CAS Login</title>
  <link rel="stylesheet" th:href="@{/themes/portal/css/element-ui.css}"/>
  <link rel="stylesheet" th:href="@{/themes/portal/css/index.css}"/>
  <link rel="icon" th:href="@{/themes/portal/favicon.ico}" id="icon">
  <script th:src="@{/themes/portal/js/vue.js}"></script>
  <script th:src="@{/themes/portal/js/vue-resource.js}"></script>
  <script th:src="@{/themes/portal/js/element-ui.js}"></script>
  <script th:src="@{/themes/portal/js/jsencrypt.min.js}"></script>
  <script th:src="@{/themes/portal/js/gVerify.js}"></script>
  <script th:src="@{/themes/portal/js/unicode.js}"></script>
  <style type="text/css">
    body {
      padding: 0;
      margin: 0;
    }
  </style>
</head>

<body>
<div id="app">
  <div v-show="!loading" class="left-image">
    <img class="login-img" th:src="@{themes/portal/images/loginimg.jpg}"/>
    <img class="login-logo" th:src="@{themes/portal/images/loginlogo.png}"/>
  </div>
  <div class="right-form">
    <el-card v-show="!loading" class="card">
      <div>
        <h3 class="login-title">数字化转型平台</h3>
      </div>
      <div>
        <el-form :label-position="labelPosition" :model="ruleForm" method="post" th:object="${credential}" status-icon
                 :rules="rules" @submit.native.prevent
                 ref="ruleForm" class="demo-ruleForm">
          <div th:if="${#fields.hasErrors('*')}" class="fieldHasErr">
                          <span th:each="err : ${#fields.errors('*')}" th:utext="${err}"
                                class="fieldErr" id="fieldErr"/>
          </div>
          <div th:if="!${#fields.hasErrors('*')}" class="fieldHasErr">
            <span class="fieldErr">&nbsp;</span>
          </div>
          <label class="control-label text-right">用户名</label>
          <el-form-item prop="userDTO">
            <el-input v-model.trim="ruleForm.userDTO" name="username"
                      prefix-icon="el-icon-user"
                      placeholder="用户名" ref="userNameInput"
                      v-no-paste autocomplete="username"
            ></el-input>
          </el-form-item>
          <label class="control-label text-right">密码</label>
          <el-form-item prop="inputPass">
            <el-input type="password" v-model.trim="ruleForm.inputPass" name="inputPass"
                      prefix-icon="el-icon-key" placeholder="密码"
                      v-no-paste autocomplete="current-password"
            ></el-input>
          </el-form-item>
          <el-form-item prop="pass" v-show="false">
            <el-input type="password" v-model.trim="ruleForm.pass" name="password"
            ></el-input>
          </el-form-item>
          <div v-show="gVerify">
            <label class="control-label text-right">验证码</label>
            <el-form-item prop="captcha" id="code_input">
              <el-input v-model.trim="ruleForm.captcha" name="captcha"
                        prefix-icon="el-icon-tickets"
                        placeholder="请输入验证码"
                        value=""
                        id="captcha_input"
              ></el-input>
            </el-form-item>
            <div id="v_container">
              <div id="changeBtn">
                <!--换一张-->
              </div>
            </div>
          </div>

          <section class="form-check" th:if="${rememberMeAuthenticationEnabled}">
            <div>
              <input type="checkbox"
                     name="rememberMe"
                     id="rememberMe" value="true" tabindex="5"/>
              <label for="rememberMe" th:text="#{screen.rememberme.checkbox.title}">Remember Me</label>
            </div>
          </section>

          <el-form-item>
            <el-button type="primary" @click="submitForm('ruleForm')" class="login-submit">登&nbsp;&nbsp;录
            </el-button>
          </el-form-item>

          <input type="hidden" name="casServerLogin" v-model.trim="isCASServerLogin"/>
          <input type="hidden" name="execution" th:value="${flowExecutionKey}"/>
          <input type="hidden" name="_eventId" value="submit"/>
        </el-form>
      </div>
      <div class="login-footer" align="center">
        <span class="login-copyright">©</span> <span id="copyright">Copyright © 2024 Gener Soft. All Rights Reserved. 沪ICP备15036345号</span>
      </div>
    </el-card>
  </div>
</div>
<div class="pre-loader" id="pre-loader">
  <div class="inner one"></div>
  <div class="inner two"></div>
  <div class="inner three"></div>
</div>
</body>

<script th:inline="text" type="application/javascript">
  function getRequestParameter() {
    let url = location.search;
    let requestParameter = {};
    if (url.indexOf("?") !== -1) {
      let strs = url.substr(1).split('&');
      for (let i = 0; i < strs.length; i++) {
        requestParameter[strs[i].split("=")[0]] = unescape(strs[i].split("=")[1]);
      }
    }
    return requestParameter;
  }

  function getRequestParameterBackTo() {
    let url = location.search;
    let requestParameter = {};
    if (url.indexOf("?") !== -1) {
      let strs = url.substr(1).split('back_to%3D');
      if (strs.length > 1) {
        requestParameter = unescape(strs[strs.length - 1]);
      } else {
        let strs = url.substr(1).split('service=');
        if (strs.length > 1) {
          requestParameter = unescape(strs[strs.length - 1]);
        }
      }
    }
    return requestParameter;
  }

  // 自定义指令 v-no-paste 密码框禁止粘贴
  Vue.directive('no-paste', {
    // 当绑定元素插入到DOM中
    inserted: function (el) {
      el.addEventListener('paste', function (event) {
        event.preventDefault();
      });
    }
  });

  new Vue({
    el: '#app',
    data() {
      let checkUser = (rule, value, callback) => {
        if (!value) {
          return callback(new Error('请输入用户名'))
        } else if (value.length > 15) {
          return callback(new Error('用户名长度错误'))
        } else {
          callback()
        }
      }
      let checkPass = (rule, value, callback) => {
        if (!value) {
          callback(new Error('请输入密码'))
        } else if (value.length > 15) {
          callback(new Error('密码长度错误'))
        } else {
          callback()
        }
      };
      let checkCaptcha = (rule, value, callback) => {
        if (this.gVerify) {
          if (!value) {
            callback(new Error('请输入验证码'))
          } else {
            callback()
          }
        } else {
          callback()
        }
      };
      return {
        labelPosition: 'right',
        loading: true,
        loginConfig: null,
        ruleForm: {
          inputPass: '', // 录入的密码
          pass: '', // 提交的密码
          userDTO: '[[${credential.username}]]',
          captcha: ''
        },
        rules: {
          userDTO: [
            {validator: checkUser, trigger: 'blur'}
          ],
          inputPass: [
            {validator: checkPass, trigger: 'blur'}
          ],
          captcha: [
            {validator: checkCaptcha, trigger: 'blur'}
          ]
        },
        isCASServerLogin: false,
        publicKey: '',
        gVerify: false
      }
    },
    computed: {},
    watch: {
      loginConfig(nv) {
        this.initLoginPage()
      },
      gVerify: {
        immediate: true,
        handler(newValue) {
          if (newValue) {
            document.querySelector(".login-footer").style.marginTop = 22 + 'vh';
          } else {
            document.querySelector(".login-footer").style.marginTop = 30 + 'vh';
          }
        }
      }
    },
    created() {
      let {service} = getRequestParameter();
      if (service) {
        this.request4LoginConfig(service, this.isCASServerLogin)
      } else {
        this.isCASServerLogin = true
        this.request4LoginConfig(service, this.isCASServerLogin)
      }
      let node = document.querySelector("#fieldErr")
      if (node) {
        let fieldErr = node.innerText;
        if (fieldErr && (fieldErr.startsWith("refreshing page..."))) {
          node.className = "fieldErr1";
        }
      }
    },
    mounted() {
      // 如果错误提示信息是“用户名和密码不匹配,还可以重试n次”/“您已连续输错密码n次,账号已锁定请稍后再试”/“用户名不存在”,启用图片验证码
      let node = document.querySelector("#fieldErr")
      if (node) {
        let fieldErr = node.innerText;
        if (fieldErr && (fieldErr.startsWith("用户名和密码不匹配,还可以重试") || fieldErr.startsWith("您已连续输错密码") || fieldErr.startsWith("用户名不存在"))) {
          this.gVerify = true;
        } else if (fieldErr && (fieldErr.startsWith("登录["))) {
          // 后登录账号和已登录账号用户名不同,弹窗提示先登出已登录账号再操作登录
          this.openMsgBox(fieldErr);
        } else if (fieldErr && (fieldErr.startsWith("refreshing page..."))) {
          // 后登录账号和已登录账号用户名相同,刷新页面即可
          let search = getRequestParameterBackTo();
          location.replace(search);
        }
      }
      this.$nextTick(function () {
        this.$refs.userNameInput.focus();
      })
    },
    methods: {
      submitForm(formName) {
        this.$refs[formName].validate((valid) => {
          if (valid) {
            if (this.gVerify) {
              //验证码是否正确
              let oInput = document.querySelector("#captcha_input");
              let res = verifyCode.validate(oInput.value);
              if (!res) {
                document.querySelector("#fieldErr").innerText = "验证码不正确";
                this.ruleForm.captcha = '';
                return false;
              }
            }
            //Encrypt the input password with a public key
            let encrypt = new JSEncrypt({default_key_size: 1024});
            encrypt.setPublicKey(this.publicKey);
            let encrypted = encrypt.encrypt(this.ruleForm.inputPass);
            this.ruleForm.pass = encrypted;
            this.ruleForm.inputPass = this.ruleForm.inputPass.replace(/./g, '*');
            this.$nextTick(function () {
              this.loginSystem();
            })
          } else {
            this.$message.error('Error submit!');
            return false;
          }
        })
      },
      loginSystem() {
        this.$refs.ruleForm.$el.submit()
      },
      async logout() {
        this.$http.get('[[@{/logout}]]', {}).then(resp => {
        }).catch(e => {
        })
      },
      async request4LoginConfig(service, isCASServerLogin) {
        this.$http.post('[[@{/getLoginConfig}]]', {
          service, isCASServerLogin
        }).then(
          resp => {
            if (resp.body.publicKey) {
              this.publicKey = resp.body.publicKey;
            }
            /*if (resp.body.htmlTitle) {
              document.title = resp.body.htmlTitle;
            }
            if (resp.body.favicon) {
              document.querySelector('#icon').href = resp.body.favicon;
            }
            if (resp.body.backgroundImg) {
              let prefix = 'url('
              let suffix = ') no-repeat center center'
              let background = prefix + resp.body.backgroundImg + suffix
              document.querySelector('.login-container').style.background = background;
              document.querySelector('.login-container').style.backgroundSize = 'cover';
            }
            if (resp.body.copyright) {
              document.querySelector('#copyright').innerText = resp.body.copyright;
            }*/
            // this.loginConfig值变化可以结束预加载的动画
            if (resp.body.title) {
              this.loginConfig = {
                banner: resp.body.title
              }
            } else {
              this.loginConfig = {
                banner: 'Welcome!'
              }
            }
            this.$nextTick(function () {
              this.$refs.userNameInput.focus()
            })
          }).catch(e => {
          setTimeout(() => {
            this.loginConfig = {
              banner: 'Welcome!'
            }
            this.$refs.userNameInput.focus()
          })
        })
      },
      initLoginPage() {
        this.loading = false;
        this.stopLoading()
      },
      stopLoading() {
        const preLoader = document.querySelector('#pre-loader')
        preLoader.style.display = 'none'
      },
      openMsgBox(msg) {
        this.$confirm(msg, '确认信息', {
          distinguishCancelAndClose: true,
          confirmButtonText: '登出',
          cancelButtonText: '取消',
          beforeClose: (action, instance, done) => {
            if (action === 'confirm') {
              this.logout();
              instance.confirmButtonLoading = true;
              instance.confirmButtonText = '登出中...';
              setTimeout(() => {
                done();
                setTimeout(() => {
                  instance.confirmButtonLoading = false;
                }, 100);
              }, 1000);
            } else {
              done();
            }
          }
        }).then(() => {
          this.$message({
            type: 'info',
            message: '登出成功'
          });
          setTimeout(() => {
            let search = getRequestParameterBackTo();
            location.replace(search);
          }, 1000)
        })
      }
    }
  })
</script>
<script type="text/javascript">
  let verifyCode = new GVerify("v_container");
  let oDiv = document.querySelector("#changeBtn");
  //看不清,换一张
  oDiv.onclick = function () {
    verifyCode.refresh();
  }
</script>
</html>

二、图片滑块验证码

参考文档:SliderCaptcha: 滑块验证码 Javascript + Bootstrap,用户拖动滑块完成时完成校验,支持PC端及移动端。并可以在后台保存用户校验过程的时间,精度,滑动轨迹等信息 - Gitee.com

如果想使用本地的图片(例如把图片放在项目目录或者nginx服务器代理目录上),可以下载(内含100张图片):

https://download.csdn.net/download/qq_43542296/89101099

sliderCaptcha.css

body {
    overflow-x: hidden;
}

.block {
    position: absolute;
    left: 0;
    top: 0;
}

.sliderContainer {
    position: relative;
    text-align: center;
    line-height: 40px;
    background: #f7f9fa;
    color: #45494c;
    border-radius: 2px;
}

.sliderbg {
    position: absolute;
    left: 0;
    right: 0;
    top: 0;
    background-color: #f7f9fa;
    height: 40px;
    border-radius: 2px;
    border: 1px solid #e6e8eb;
}

.sliderContainer_active .slider {
    top: -1px;
    border: 1px solid #1991FA;
}

.sliderContainer_active .sliderMask {
    border-width: 1px 0 1px 1px;
}

.sliderContainer_success .slider {
    top: -1px;
    border: 1px solid #52CCBA;
    background-color: #52CCBA !important;
}

.sliderContainer_success .sliderMask {
    border: 1px solid #52CCBA;
    border-width: 1px 0 1px 1px;
    background-color: #D2F4EF;
}

.sliderContainer_success .sliderIcon:before {
    content: "\f00c";
}

.sliderContainer_fail .slider {
    top: -1px;
    border: 1px solid #f57a7a;
    background-color: #f57a7a !important;
}

.sliderContainer_fail .sliderMask {
    border: 1px solid #f57a7a;
    background-color: #fce1e1;
    border-width: 1px 0 1px 1px;
}

.sliderContainer_fail .sliderIcon:before {
    content: "\f00d";
}

.sliderContainer_active .sliderText, .sliderContainer_success .sliderText, .sliderContainer_fail .sliderText {
    display: none;
}

.sliderMask {
    position: absolute;
    left: 0;
    top: 0;
    height: 40px;
    border: 0 solid #1991FA;
    background: #D1E9FE;
    border-radius: 2px;
}

.slider {
    position: absolute;
    top: 0;
    left: 0;
    width: 40px;
    height: 40px;
    background: #fff;
    box-shadow: 0 0 3px rgba(0, 0, 0, 0.3);
    cursor: pointer;
    transition: background .2s linear;
    border-radius: 2px;
    display: flex;
    align-items: center;
    justify-content: center;
}

.slider:hover {
    background: #1991FA;
}

.slider:hover .sliderIcon {
    background-position: 0 -13px;
}

.sliderText {
    position: relative;
}

.sliderIcon {

}

.refreshIcon {
    position: absolute;
    right: 0;
    top: 0;
    cursor: pointer;
    margin: 6px;
    color: rgba(0, 0, 0, .25);
    font-size: 1rem;
    z-index: 5;
    transition: color .3s linear;
}

.refreshIcon:hover {
    color: #6c757d;
}

sliderCaptcha.js:

(function ($) {
  'use strict';

  var SliderCaptcha = function (element, options) {
    this.$element = $(element);
    this.options = $.extend({}, SliderCaptcha.DEFAULTS, options);
    this.$element.css({'position': 'relative', 'width': this.options.width + 'px', 'margin': '0 auto'});
    this.init();
  };

  SliderCaptcha.VERSION = '1.0';
  SliderCaptcha.Author = 'argo@163.com';
  SliderCaptcha.DEFAULTS = {
    width: 280,     // canvas宽度
    height: 155,    // canvas高度
    PI: Math.PI,
    sliderL: 42,    // 滑块边长
    sliderR: 9,     // 滑块半径
    loadingText: '正在加载中...',
    failedText: '再试一次',
    barText: '向右滑动填充拼图',
    repeatIcon: 'fa fa-repeat'
  };

  function Plugin(option) {
    return this.each(function () {
      var $this = $(this);
      var data = $this.data('lgb.SliderCaptcha');
      var options = typeof option === 'object' && option;

      if (!data && /init|reset|verify/.test(option)) return;
      if (!data) $this.data('lgb.SliderCaptcha', data = new SliderCaptcha(this, options));
      if (typeof option === 'string') data[option]();
    });
  }

  $.fn.sliderCaptcha = Plugin;
  $.fn.sliderCaptcha.Constructor = SliderCaptcha;

  var _proto = SliderCaptcha.prototype;
  _proto.init = function () {
    this.initDOM()
    this.initImg()
    this.bindEvents()
  };

  _proto.initDOM = function () {
    var createElement = function (tagName, className) {
      var elment = document.createElement(tagName);
      elment.className = className;
      return elment;
    };

    var createCanvas = function (width, height) {
      var canvas = document.createElement('canvas');
      canvas.width = width;
      canvas.height = height;
      return canvas;
    };

    var canvas = createCanvas(this.options.width - 2, this.options.height) // 画布
    var block = canvas.cloneNode(true) // 滑块
    var sliderContainer = createElement('div', 'sliderContainer');
    var refreshIcon = createElement('i', 'refreshIcon ' + this.options.repeatIcon);
    var sliderMask = createElement('div', 'sliderMask');
    var sliderbg = createElement('div', 'sliderbg');
    var slider = createElement('div', 'slider');
    var sliderIcon = createElement('i', 'fa fa-arrow-right sliderIcon');
    var text = createElement('span', 'sliderText');

    block.className = 'block'
    text.innerHTML = this.options.barText;

    var el = this.$element;
    el.append($(canvas));
    el.append($(refreshIcon));
    el.append($(block));
    slider.appendChild(sliderIcon);
    sliderMask.appendChild(slider);
    sliderContainer.appendChild(sliderbg);
    sliderContainer.appendChild(sliderMask);
    sliderContainer.appendChild(text);
    el.append($(sliderContainer));

    Object.assign(this, {
      canvas,
      block,
      sliderContainer: $(sliderContainer),
      refreshIcon,
      slider,
      sliderMask,
      sliderIcon,
      text: $(text),
      canvasCtx: canvas.getContext('2d'),
      blockCtx: block.getContext('2d')
    })
  };

  _proto.initImg = function () {
    var that = this;
    var isIE = window.navigator.userAgent.indexOf('Trident') > -1;
    var L = this.options.sliderL + this.options.sliderR * 2 + 3; // 滑块实际边长
    var drawImg = function (ctx, operation) {
      var l = that.options.sliderL;
      var r = that.options.sliderR;
      var PI = that.options.PI;
      var x = that.x;
      var y = that.y;
      ctx.beginPath()
      ctx.moveTo(x, y)
      ctx.arc(x + l / 2, y - r + 2, r, 0.72 * PI, 2.26 * PI)
      ctx.lineTo(x + l, y)
      ctx.arc(x + l + r - 2, y + l / 2, r, 1.21 * PI, 2.78 * PI)
      ctx.lineTo(x + l, y + l)
      ctx.lineTo(x, y + l)
      ctx.arc(x + r - 2, y + l / 2, r + 0.4, 2.76 * PI, 1.24 * PI, true)
      ctx.lineTo(x, y)
      ctx.lineWidth = 2
      ctx.fillStyle = 'rgba(255, 255, 255, 0.7)'
      ctx.strokeStyle = 'rgba(255, 255, 255, 0.7)'
      ctx.stroke()
      ctx[operation]()
      ctx.globalCompositeOperation = isIE ? 'xor' : 'overlay'
    }

    var getRandomNumberByRange = function (start, end) {
      return Math.round(Math.random() * (end - start) + start);
    };
    var img = new Image();
    img.crossOrigin = "Anonymous";
    img.onload = function () {
      // 随机创建滑块的位置 that.options.width - (L + 10) -> that.options.width - (L + 20)
      that.x = getRandomNumberByRange(L + 10, that.options.width - (L + 20));
      that.y = getRandomNumberByRange(10 + that.options.sliderR * 2, that.options.height - (L + 10));
      drawImg(that.canvasCtx, 'fill');
      drawImg(that.blockCtx, 'clip');

      that.canvasCtx.drawImage(img, 0, 0, that.options.width - 2, that.options.height);
      that.blockCtx.drawImage(img, 0, 0, that.options.width - 2, that.options.height);
      var y = that.y - that.options.sliderR * 2 - 1;
      var ImageData = that.blockCtx.getImageData(that.x - 3, y, L, L);
      that.block.width = L;
      that.blockCtx.putImageData(ImageData, 0, y);
      that.text.text(that.text.attr('data-text'));
    };
    img.onerror = function () {
      img.setSrc();
    }
    img.setSrc = function () {
      var src = '';
      if ($.isFunction(that.options.setSrc)) src = that.options.setSrc();
      if (!src || src === '') src = 'https://picsum.photos/' + that.options.width + '/' + that.options.height + '/?image=' + Math.round(Math.random() * 50);
      if (isIE) { // IE浏览器无法通过img.crossOrigin跨域,使用ajax获取图片blob然后转为dataURL显示
        var xhr = new XMLHttpRequest()
        xhr.onloadend = function (e) {
          var file = new FileReader(); // FileReader仅支持IE10+
          file.readAsDataURL(e.target.response);
          file.onloadend = function (e) {
            img.src = e.target.result;
          }
        }
        xhr.open('GET', src);
        xhr.responseType = 'blob';
        xhr.send();
      } else img.src = src;
    };
    img.setSrc();
    this.text.attr('data-text', this.options.barText);
    this.text.text(this.options.loadingText);
    this.img = img
  };

  _proto.clean = function () {
    this.canvasCtx.clearRect(0, 0, this.options.width, this.options.height);
    this.blockCtx.clearRect(0, 0, this.options.width, this.options.height);
    this.block.width = this.options.width;
  };

  _proto.bindEvents = function () {
    var that = this;
    this.$element.on('selectstart', function () {
      return false;
    });

    $(this.refreshIcon).on('click', function () {
      that.text.text(that.options.barText);
      that.reset();
      if ($.isFunction(that.options.onRefresh)) that.options.onRefresh.call(that.$element);
    });

    var originX, originY, trail = [],
      isMouseDown = false

    var handleDragStart = function (e) {
      originX = e.clientX || e.touches[0].clientX;
      originY = e.clientY || e.touches[0].clientY;
      isMouseDown = true;
    };

    var handleDragMove = function (e) {
      if (!isMouseDown) return false;
      var eventX = e.clientX || e.touches[0].clientX;
      var eventY = e.clientY || e.touches[0].clientY;
      var moveX = eventX - originX;
      var moveY = eventY - originY;
      if (moveX < 0 || moveX + 40 > that.options.width) return false;
      that.slider.style.left = (moveX - 1) + 'px';
      var blockLeft = (that.options.width - 40 - 20) / (that.options.width - 40) * moveX;
      that.block.style.left = blockLeft + 'px';

      that.sliderContainer.addClass('sliderContainer_active');
      that.sliderMask.style.width = (moveX + 4) + 'px';
      trail.push(moveY);
    };

    var handleDragEnd = function (e) {
      if (!isMouseDown) return false
      isMouseDown = false
      var eventX = e.clientX || e.changedTouches[0].clientX
      if (eventX == originX) return false
      that.sliderContainer.removeClass('sliderContainer_active');
      that.trail = trail
      var {
        spliced,
        verified
      } = that.verify()
      if (spliced && verified) {
        that.sliderContainer.addClass('sliderContainer_success');
        if ($.isFunction(that.options.onSuccess)) that.options.onSuccess.call(that.$element);
      } else {
        that.sliderContainer.addClass('sliderContainer_fail');
        if ($.isFunction(that.options.onFail)) that.options.onFail.call(that.$element);
        setTimeout(() => {
          that.text.text(that.options.failedText);
          that.reset();
        }, 1000)
      }
    };

    this.slider.addEventListener('mousedown', handleDragStart);
    this.slider.addEventListener('touchstart', handleDragStart);
    document.addEventListener('mousemove', handleDragMove);
    document.addEventListener('touchmove', handleDragMove);
    document.addEventListener('mouseup', handleDragEnd);
    document.addEventListener('touchend', handleDragEnd);

    document.addEventListener('mousedown', function () {
      return false;
    });
    document.addEventListener('touchstart', function () {
      return false;
    });
  };

  _proto.verify = function () {
    var sum = function (x, y) {
      return x + y;
    };
    var square = function (x) {
      return x * x;
    };
    var arr = this.trail // 拖动时y轴的移动距离
    var average = arr.reduce(sum) / arr.length;
    var deviations = arr.map(function (x) {
      return x - average;
    });
    var stddev = Math.sqrt(deviations.map(square).reduce(sum) / arr.length);
    var left = parseInt(this.block.style.left);
    return {
      spliced: Math.abs(left - this.x) < 6, // 4 -> 6更合适
      verified: stddev !== 0, // 简单验证下拖动轨迹,为零时表示Y轴上下没有波动,可能非人为操作
    }
  };

  _proto.reset = function () {
    this.sliderContainer.removeClass('sliderContainer_fail sliderContainer_success');
    this.slider.style.left = 0
    this.block.style.left = 0
    this.sliderMask.style.width = 0
    this.clean()
    this.text.attr('data-text', this.text.text());
    this.text.text(this.options.loadingText);
    this.img.setSrc();
  };
})(jQuery);

登录页面使用:

<!DOCTYPE html >
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
  <!--<title th:text="${#themes.code('cas.page.title')}"></title> 存在中文乱码问题,-->
  <!--所以改为在messages_zh_CN.properties文件取-->
  <title th:text="#{screen.login.header}">CAS Login</title>
  <link href="https://cdn.bootcss.com/twitter-bootstrap/4.3.1/css/bootstrap.min.css">
  <link href="https://cdn.bootcss.com/font-awesome/5.7.2/css/all.min.css">
  <link rel="stylesheet" th:href="@{/themes/portal/css/sliderCaptcha.css}"/>
  <link rel="stylesheet" th:href="@{/themes/portal/css/element-ui.css}"/>
  <link rel="stylesheet" th:href="@{/themes/portal/css/index.css}"/>
  <link rel="icon" th:href="@{/themes/portal/favicon.ico}" id="icon">
  <script th:src="@{/themes/portal/js/vue.js}"></script>
  <script th:src="@{/themes/portal/js/vue-resource.js}"></script>
  <script th:src="@{/themes/portal/js/element-ui.js}"></script>
  <script th:src="@{/themes/portal/js/jsencrypt.min.js}"></script>
  <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
  <script th:src="@{/themes/portal/js/sliderCaptcha.js}"></script>
  <style type="text/css">
    body {
      padding: 0;
      margin: 0;
    }
  </style>
</head>

<body>
<div id="app">
  <div v-show="!loading" class="left-image">
    <img class="login-img" th:src="@{themes/portal/images/loginimg.jpg}"/>
    <img class="login-logo" th:src="@{themes/portal/images/loginlogo.png}"/>
  </div>
  <div class="right-form">
    <el-card v-show="!loading" class="card">
      <div>
        <h3 class="login-title">数字化转型平台</h3>
      </div>
      <div>
        <el-form :label-position="labelPosition" :model="ruleForm" method="post" th:object="${credential}" status-icon
                 :rules="rules" @submit.native.prevent
                 ref="ruleForm" class="demo-ruleForm">
          <div th:if="${#fields.hasErrors('*')}" class="fieldHasErr">
                          <span th:each="err : ${#fields.errors('*')}" th:utext="${err}"
                                class="fieldErr" id="fieldErr"/>
          </div>
          <div th:if="!${#fields.hasErrors('*')}" class="fieldHasErr">
            <span class="fieldErr">&nbsp;</span>
          </div>
          <label class="control-label text-right">用户名</label>
          <el-form-item prop="userDTO">
            <el-input v-model.trim="ruleForm.userDTO" name="username"
                      prefix-icon="el-icon-user"
                      placeholder="用户名" ref="userNameInput"
                      v-no-paste autocomplete="username"
            ></el-input>
          </el-form-item>
          <label class="control-label text-right">密码</label>
          <el-form-item prop="inputPass">
            <el-input type="password" v-model.trim="ruleForm.inputPass" name="inputPass"
                      prefix-icon="el-icon-key" placeholder="密码"
                      v-no-paste autocomplete="current-password"
            ></el-input>
          </el-form-item>
          <el-form-item prop="pass" v-show="false">
            <el-input type="password" v-model.trim="ruleForm.pass" name="password"
            ></el-input>
          </el-form-item>

          <el-dialog
              :visible.sync="dialogVisible"
              title="请完成安全验证" :show-close="false"
              margin-top="15vh" width="320px" height="194px">
            <div id="captcha" style="min-height: 165px; max-height: 165px;"></div>
            <span slot="footer" class="dialog-footer">
              <el-tooltip class="item" effect="dark" content="关闭验证" placement="top-start">
                <el-button circle icon="el-icon-close" @click="closeCaptcha" size="medium"></el-button>
              </el-tooltip>
              <el-tooltip class="item" effect="dark" content="刷新验证" placement="top-start">
                <el-button circle icon="el-icon-refresh" @click="refreshCaptcha" size="medium"></el-button>
              </el-tooltip>
            </span>
          </el-dialog>

          <section class="form-check" th:if="${rememberMeAuthenticationEnabled}">
            <div>
              <input type="checkbox"
                     name="rememberMe"
                     id="rememberMe" value="true" tabindex="5"/>
              <label for="rememberMe" th:text="#{screen.rememberme.checkbox.title}">Remember Me</label>
            </div>
          </section>

          <el-form-item>
            <el-button type="primary" @click="submitForm('ruleForm')" class="login-submit" size="mini">登&nbsp;&nbsp;录
            </el-button>
          </el-form-item>

          <input type="hidden" name="casServerLogin" v-model.trim="isCASServerLogin"/>
          <input type="hidden" name="execution" th:value="${flowExecutionKey}"/>
          <input type="hidden" name="_eventId" value="submit"/>
        </el-form>
      </div>
      <div class="login-footer" align="center" style="margin-top:30vh">
        <span class="login-copyright">©</span> <span id="copyright">Copyright © 2024 Gener Soft. All Rights Reserved. 沪ICP备15036345号</span>
      </div>
    </el-card>
  </div>
</div>
<div class="pre-loader" id="pre-loader">
  <div class="inner one"></div>
  <div class="inner two"></div>
  <div class="inner three"></div>
</div>
</body>

<script th:inline="text" type="application/javascript">
  function getRequestParameter() {
    let url = location.search;
    let requestParameter = {};
    if (url.indexOf("?") !== -1) {
      let strs = url.substr(1).split('&');
      for (let i = 0; i < strs.length; i++) {
        requestParameter[strs[i].split("=")[0]] = unescape(strs[i].split("=")[1]);
      }
    }
    return requestParameter;
  }

  function getRequestParameterBackTo() {
    let url = location.search;
    let requestParameter = {};
    if (url.indexOf("?") !== -1) {
      let strs = url.substr(1).split('back_to%3D');
      if (strs.length > 1) {
        requestParameter = unescape(strs[strs.length - 1]);
      } else {
        let strs = url.substr(1).split('service=');
        if (strs.length > 1) {
          requestParameter = unescape(strs[strs.length - 1]);
        }
      }
    }
    return requestParameter;
  }

  // 自定义指令 v-no-paste 密码框禁止粘贴
  Vue.directive('no-paste', {
    // 当绑定元素插入到DOM中
    inserted: function (el) {
      el.addEventListener('paste', function (event) {
        event.preventDefault();
      });
    }
  });

  const vm = new Vue({
    el: '#app',
    data() {
      let checkUser = (rule, value, callback) => {
        if (!value) {
          return callback(new Error('请输入用户名'))
        } else if (value.length > 15) {
          return callback(new Error('用户名长度错误'))
        } else {
          callback()
        }
      }
      let checkPass = (rule, value, callback) => {
        if (!value) {
          callback(new Error('请输入密码'))
        } else if (value.length > 15) {
          callback(new Error('密码长度错误'))
        } else {
          callback()
        }
      };
      return {
        labelPosition: 'right',
        loading: true,
        loginConfig: null,
        ruleForm: {
          inputPass: '', // 录入的密码
          pass: '', // 提交的密码
          userDTO: '[[${credential.username}]]',
        },
        rules: {
          userDTO: [
            {validator: checkUser, trigger: 'blur'}
          ],
          inputPass: [
            {validator: checkPass, trigger: 'blur'}
          ],
        },
        isCASServerLogin: false,
        publicKey: '',
        dialogVisible: false
      }
    },
    computed: {},
    watch: {
      loginConfig(nv) {
        this.initLoginPage()
      },
    },
    created() {
      let {service} = getRequestParameter();
      if (service) {
        this.request4LoginConfig(service, this.isCASServerLogin)
      } else {
        this.isCASServerLogin = true
        this.request4LoginConfig(service, this.isCASServerLogin)
      }
      let node = document.querySelector("#fieldErr")
      if (node) {
        let fieldErr = node.innerText;
        if (fieldErr && (fieldErr.startsWith("refreshing page..."))) {
          node.className = "fieldErr1";
        }
      }
    },
    mounted() {
      let node = document.querySelector("#fieldErr")
      if (node) {
        let fieldErr = node.innerText;
        if (fieldErr && (fieldErr.startsWith("登录["))) {
          // 后登录账号和已登录账号用户名不同,弹窗提示先登出已登录账号再操作登录
          this.openMsgBox(fieldErr);
        } else if (fieldErr && (fieldErr.startsWith("refreshing page..."))) {
          // 后登录账号和已登录账号用户名相同,刷新页面即可
          let search = getRequestParameterBackTo();
          location.replace(search);
        }
      }
      this.$nextTick(function () {
        this.$refs.userNameInput.focus();
      })
    },
    methods: {
      submitForm(formName) {
        this.$refs[formName].validate((valid) => {
          if (valid) {
            this.dialogVisible = true;
            this.openCaptcha();
          } else {
            this.$message.error('Error submit!');
            return false;
          }
        })
      },
      loginSystem() {
        this.$refs.ruleForm.$el.submit()
      },
      async logout() {
        this.$http.get('[[@{/logout}]]', {}).then(resp => {
        }).catch(e => {
        })
      },
      async request4LoginConfig(service, isCASServerLogin) {
        this.$http.post('[[@{/getLoginConfig}]]', {
          service, isCASServerLogin
        }).then(
          resp => {
            if (resp.body.publicKey) {
              this.publicKey = resp.body.publicKey;
            }
            /*if (resp.body.htmlTitle) {
              document.title = resp.body.htmlTitle;
            }
            if (resp.body.favicon) {
              document.querySelector('#icon').href = resp.body.favicon;
            }
            if (resp.body.backgroundImg) {
              let prefix = 'url('
              let suffix = ') no-repeat center center'
              let background = prefix + resp.body.backgroundImg + suffix
              document.querySelector('.login-container').style.background = background;
              document.querySelector('.login-container').style.backgroundSize = 'cover';
            }
            if (resp.body.copyright) {
              document.querySelector('#copyright').innerText = resp.body.copyright;
            }*/
            // this.loginConfig值变化可以结束预加载的动画
            if (resp.body.title) {
              this.loginConfig = {
                banner: resp.body.title
              }
            } else {
              this.loginConfig = {
                banner: 'Welcome!'
              }
            }
            this.$nextTick(function () {
              this.$refs.userNameInput.focus()
            })
          }).catch(e => {
          setTimeout(() => {
            this.loginConfig = {
              banner: 'Welcome!'
            }
            this.$refs.userNameInput.focus()
          })
        })
      },
      initLoginPage() {
        this.loading = false;
        this.stopLoading()
      },
      stopLoading() {
        const preLoader = document.querySelector('#pre-loader')
        preLoader.style.display = 'none'
      },
      openMsgBox(msg) {
        this.$confirm(msg, '确认信息', {
          distinguishCancelAndClose: true,
          confirmButtonText: '登出',
          cancelButtonText: '取消',
          beforeClose: (action, instance, done) => {
            if (action === 'confirm') {
              this.logout();
              instance.confirmButtonLoading = true;
              instance.confirmButtonText = '登出中...';
              setTimeout(() => {
                done();
                setTimeout(() => {
                  instance.confirmButtonLoading = false;
                }, 100);
              }, 1000);
            } else {
              done();
            }
          }
        }).then(() => {
          this.$message({
            type: 'info',
            message: '登出成功'
          });
          setTimeout(() => {
            let search = getRequestParameterBackTo();
            location.replace(search);
          }, 1000)
        })
      },
      openCaptcha() {
        this.$nextTick(function () {
          $('#captcha').sliderCaptcha({
            width: 280,
            height: 150,
            sliderL: 25,
            sliderR: 5,
            loadingText: '正在加载中...',
            failedText: '再试一次',
            barText: '向右滑动填充拼图',
            repeatIcon: 'fa fa-redo',
            setSrc: function () {
              //return 'http://pocoafrro.bkt.clouddn.com/Pic' + Math.round(Math.random() * 136) + '.jpg';
            },
            onSuccess: function () {
              //console.log("验证通过!");
              //Encrypt the input password with a public key
              let encrypt = new JSEncrypt({default_key_size: 1024});
              encrypt.setPublicKey(vm.publicKey);
              let encrypted = encrypt.encrypt(vm.ruleForm.inputPass);
              vm.ruleForm.pass = encrypted;
              vm.ruleForm.inputPass = vm.ruleForm.inputPass.replace(/./g, '*');
              vm.$nextTick(function () {
                vm.loginSystem();
              })
            },
            onFail: function () {
              //console.log("验证失败!");
            },
            onRefresh: function () {
              //console.log("刷新了!");
            }
          });
        })
      },
      closeCaptcha() {
        this.dialogVisible = false;
      },
      refreshCaptcha() {
        $('#captcha').sliderCaptcha('reset');
      }
    }
  })
</script>
</html>

三、短信验证码

https://download.csdn.net/download/qq_43542296/89064721

  • 5
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
verify.js是一个纯前端验证码生成和验证工具。它通过使用JavaScript编写,可以帮助开发者在前端页面中添加验证码功能,以增强网站的安全性。 verify.js的生成验证码的方式多种多样,可以是数字、字母、图像等形式。开发者可以根据自己的需求选择不同的验证码类型,并可以自定义验证码的长度、字体、颜色等样式。生成的验证码可以直接在前端页面中展示给用户,通过verify.js提供的接口,可以轻松地将验证码嵌入到表单、登录页面等需要进行验证的地方。 validate.js也提供了验证验证码的功能,它通过接收用户输入的验证码信息,并与前端生成的验证码进行比对,以确定用户输入的验证码是否正确。验证过程是在前端进行的,可以有效地减轻服务器的负担,提高页面的响应速度。如果用户输入的验证码前端生成的验证码不一致,开发者可以自定义提示信息,提示用户重新输入。只有在用户输入正确的验证码后,才能通过验证码验证,进一步进行后续操作,确保了网站的安全性。 总的来说,verify.js是一个方便易用的纯前端验证码工具,它提供了生成验证码和验证验证码的功能。通过它,开发者可以简单地在前端页面中添加验证码,并提高网站的安全性。它的使用方法灵活多样,可以满足不同网站的需求。同时,verify.js还支持自定义样式和提示信息,使开发者可以根据自己的需要进行定制。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值