一、基础验证码
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"> </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">登 录
</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>
二、图片滑块验证码
如果想使用本地的图片(例如把图片放在项目目录或者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"> </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">登 录
</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>