前言:最近想写一个滑动验证码,前台的样式虽然很好看,但是并不安全,网上也都是一些demo,不是前后台分离的,然后就自己查资料,自己来完成了
滑动验证码
一、为什么要使用滑动验证码
首先,滑块验证码能够有效防止暴力破解和自动化攻击。在传统的账号密码验证方式下,黑客可以通过暴力破解手段尝试大量密码组合,从而获取用户账户的控制权。而滑块验证码的引入,使得每次验证都需要用户进行手动操作,极大地增加了黑客攻击的难度和成本。
其次,滑块验证码能够提升用户体验。相比于传统的文字或数字验证码,滑块验证码的操作更加简单直观,用户只需要通过拖动滑块即可完成验证,无需输入复杂的字符或数字。这不仅降低了用户的使用门槛,也提升了用户的操作体验。
此外,滑块验证码还具有一定的灵活性和可扩展性。淘宝等电商平台可以根据自身的安全需求和用户行为数据,动态调整滑块验证码的难度和出现频率。例如,在检测到异常登录行为或高风险操作时,平台可以要求用户进行更严格的滑块验证,以确保账户安全。
总的来说,淘宝频繁出现滑块验证码是为了保障用户账户的安全性和提升用户体验。通过引入这种交互式的验证方式,淘宝能够有效地防止自动化攻击和恶意操作,同时为用户提供更加便捷和安全的购物环境。
二、实现原理
滑动验证码的原理基于人类的视觉和手势行为。它的设计目的是模拟人类在拖动滑块上的操作,以判断用户是否为真实的人类。下面是滑动验证码的工作原理:
加载验证码:当用户访问需要进行验证的网站时,验证码会被加载并显示在页面上。
定位滑块图:通过分析页面上的验证码元素,识别出背景图、滑块图和验证区域。通常,滑块图会被嵌入到背景图中,而验证区域则是滑块图与背景图的重叠部分。
用户滑动操作:用户需要使用鼠标或触摸屏对滑块图进行拖动操作,将滑块图滑动至缺口位置,以完成验证。
验证结果判断:当用户完成滑动操作后,系统会根据滑块图的位置与缺口位置的关系来判断验证结果。如果滑块图与缺口位置匹配,系统会认定用户为真实的人类用户;否则,系统会认为用户可能是机器人或恶意攻击者。
也就是以下的流程
-
前端显示:用户首先看到一个页面,页面上显示了一个带有滑块和背景图案的验证码区域。通常滑块初始位置在左侧,用户需要将滑块拖动到正确的位置。
-
交互过程:
- 用户点击并按住鼠标左键,拖动滑块至指定位置。
- 前端会监测鼠标移动事件,实时更新滑块的位置。
- 用户释放鼠标左键后,前端会发送包含滑块位置信息的请求给后端进行验证。
-
后端验证:
- 后端接收到前端发送的请求,获取到滑块的位置信息。
- 后端根据预先生成的验证码信息,对比用户拖动滑块后的位置是否与预期位置相符。
- 如果位置匹配成功,则验证通过;否则验证失败,要求用户重新验证。
-
防御机制:滑块验证码通常会具备一些防御机制来防止恶意破解,例如检测拖动速度、检测鼠标轨迹等,以提高安全性。
三、代码实现
这里要感谢gitee作者-天爱有情开源的后台代码 captcha
3.1 后台代码
3.1.1依赖
<dependency>
<groupId>cloud.tianai.captcha</groupId>
<artifactId>tianai-captcha</artifactId>
<version>1.4.1</version>
</dependency>
<dependency>
<groupId>cloud.tianai.captcha</groupId>
<artifactId>tianai-captcha-springboot-starter</artifactId>
<version>1.4.1</version>
</dependency>
3.1.2 yml配置
作者这里使用的配置文件是yml的格式,小伙伴们一定要注意格式哦!!
captcha:
cache:
enabled: true
cache-size: 20
secondary:
enabled: false
init-default-resource: false
cors:
control-allow-headers: "*"
control-allow-methods: "*"
control-allow-origin: "*"
3.1.3控制层代码
package com.brook.controller;
import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
import cloud.tianai.captcha.common.response.ApiResponse;
import cloud.tianai.captcha.spring.application.ImageCaptchaApplication;
import cloud.tianai.captcha.spring.plugins.secondary.SecondaryVerificationApplication;
import cloud.tianai.captcha.spring.vo.CaptchaResponse;
import cloud.tianai.captcha.spring.vo.ImageCaptchaVO;
import cloud.tianai.captcha.validator.common.model.dto.ImageCaptchaTrack;
import com.brook.common.result.Result;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.util.Collections;
@RestController
@RequestMapping("/captcha")
public class CaptchaController {
@Autowired
private ImageCaptchaApplication imageCaptchaApplication;
/作者这里只写了滑动验证码,所以只匹配了滑块的类型
@GetMapping("/gen")
@ResponseBody
public Result<CaptchaResponse<ImageCaptchaVO>> genCaptcha(HttpServletRequest request, @RequestParam(value = "type", required = false)String type) {
if (StringUtils.isBlank(type)) {
type = CaptchaTypeConstant.SLIDER;
}
if ("RANDOM".equals(type)) {
type = CaptchaTypeConstant.SLIDER;
}
CaptchaResponse<ImageCaptchaVO> response = imageCaptchaApplication.generateCaptcha(type);
return Result.success(response);
}
/校验x轴的坐标,以及其他的一些信息
@PostMapping("/check")
@ResponseBody
public Result<ApiResponse<?>> checkCaptcha(@RequestBody Data data,
HttpServletRequest request) {
ImageCaptchaTrack dataData = data.getData();
ApiResponse<?> response = imageCaptchaApplication.matching(data.getId(), dataData);
boolean success = response.isSuccess();
System.out.println(success);
if (response.isSuccess()) {
return Result.success(ApiResponse.ofSuccess(Collections.singletonMap("id", data.getId())));
}
return Result.success(response);
}
@lombok.Data
public static class Data {
private String id;
private ImageCaptchaTrack data;
}
/**
* 二次验证,一般用于机器内部调用,这里为了方便测试,作者这里没有使用到
* @param id id
* @return boolean
*/
@GetMapping("/check2")
@ResponseBody
public boolean check2Captcha(@RequestParam("id") String id) {
// 如果开启了二次验证
if (imageCaptchaApplication instanceof SecondaryVerificationApplication) {
return ((SecondaryVerificationApplication) imageCaptchaApplication).secondaryVerification(id);
}
return false;
}
}
3.1.4 跨域代码
package com.brook.controller;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.util.pattern.PathPatternParser;
import java.util.List;
/**
* 该类用于设置跨域
*/
@Configuration
public class CorsPropConfiguration {
@Bean
public FilterRegistrationBean coreWebFilter(CorsProperties corsProperties) {
CorsConfiguration config = new CorsConfiguration();
// * 号表示匹配任意的
config.setAllowedMethods(corsProperties.getControlAllowMethods());
config.setAllowedOrigins(corsProperties.getControlAllowOrigin());
config.setAllowedHeaders(corsProperties.getControlAllowHeaders());
PathPatternParser patternParser = new PathPatternParser();
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(patternParser);
// ** 代表所有
source.registerCorsConfiguration("/**", config);
FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
bean.setOrder(0);
return bean;
}
@Data
@Configuration
@ConfigurationProperties(prefix = "cors")
public static class CorsProperties {
private List<String> controlAllowHeaders;
private List<String> controlAllowMethods;
private List<String> controlAllowOrigin;
}
}
3.1.5 负责模版和背景图,小伙伴也可以自行添加图片
像这样,把图片整合进resources下的bgimages包中,就可以啦,不过要加这行代码
注:图片名称一定要和自己整合的图片一致!!!并且图片的大小要设置为600×360
addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/m.jpg","default"));
package com.brook.controller;
import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
import cloud.tianai.captcha.generator.common.constant.SliderCaptchaConstant;
import cloud.tianai.captcha.generator.impl.StandardSliderImageCaptchaGenerator;
import cloud.tianai.captcha.resource.common.model.dto.Resource;
import cloud.tianai.captcha.resource.common.model.dto.ResourceMap;
import cloud.tianai.captcha.resource.impl.DefaultResourceStore;
import cloud.tianai.captcha.resource.impl.provider.ClassPathResourceProvider;
import org.springframework.stereotype.Component;
import static cloud.tianai.captcha.generator.impl.StandardSliderImageCaptchaGenerator.DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH;
/**
* @Author: YanShuLing
* @date 2022/7/11 14:22
* @Description 负责模板和背景图存储的地方
*/
@Component
public class MyResourceStore extends DefaultResourceStore {
public MyResourceStore() {
// 滑块验证码 模板 (系统内置)
ResourceMap template1 = new ResourceMap("default",4);
template1.put(SliderCaptchaConstant.TEMPLATE_ACTIVE_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME, DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/1/active.png")));
template1.put(SliderCaptchaConstant.TEMPLATE_FIXED_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME, DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/1/fixed.png")));
ResourceMap template2 = new ResourceMap("default",4);
template2.put(SliderCaptchaConstant.TEMPLATE_ACTIVE_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME, DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/2/active.png")));
template2.put(SliderCaptchaConstant.TEMPLATE_FIXED_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME, DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/2/fixed.png")));
// 1. 添加一些模板
addTemplate(CaptchaTypeConstant.SLIDER, template1);
addTemplate(CaptchaTypeConstant.SLIDER, template2);
// 2. 添加自定义背景图片
addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/a.jpg","default"));
addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/b.jpg","default"));
addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/c.jpg","default"));
addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/d.jpg","default"));
addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/e.jpg","default"));
addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/f.jpg","default"));
addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/g.jpg","default"));
addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/h.jpg","default"));
addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/i.jpg","default"));
addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/j.jpg","default"));
addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/k.jpg","default"));
addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/l.jpg","default"));
addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/m.jpg","default"));
}
}
3.2 vue前台代码
(作者这里使用的是vue2 从gitee上面拉取的花裤衩脚手架)小伙伴们也可以使用正常的vue2
3.2.1 首先定义一个子组件
中间运行可能需要安装一个依赖,小伙伴运行的时候,按照控制台的提示安装就好了
<template>
<div class="slider" ref="sliderComponent">
<div class="mask">
<div class="container">
<div class="title">
<div class="text">
<span>请完成下列验证后继续</span>
</div>
<div class="button-group">
<img
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAE2UlEQVRoQ+1YXWwUVRT+zsw2Qktn1ljatEjjD+4WJILpg0rE4ItGn0QMyGMTE/kJTUwFElt2ZmopQWhIMNjyRKIPiH/hScXEiJEgL/4mamcwUSG2UCvuzAJt7e4cc6ddFaTMHTq70qTztnvPPef7zt899xJm+EczHD9mCfzfEZyNQNwRaD7IFSPnLy/FbZWnv99MF8P033QRSFvuDmbuJOBL20w2zzwCpnuEwWtnLIGU6X4O8IMEHLXN5OqyRcBgVg7vzK1AwX+KGM0ANQDcwAQVwAVi+oWAU6Ti46Vp7djba6lwLXBp0/2VwQ0getUx9NaSE1h1iOcMns21ss9tANeGGRTrBDrHCnrnzNV6vt1Kl4p7RAHnBr1RgBVFUbb2Z7S9YfqmVcRNVm6Nj8I+MBYGwAiXmHFMIXzkg85UkDpQQD7BwN0M3AWfloGwGsy3FIlARYu9Q/9Q/G7aNXKHPzb2U7CmYJ2dSb5VEgLMTGkr1wX4LwUGiM4T2Kpv1A8db6HR6xm9vzs3f2S88JwPbAZjAYgKBN5iG8nexV25Rwr5wqdiv5qgh37o0E/FTkCAb+r0DjPzukkv9qnzta0yPfvfYB7Yz9ofF9xDAJ4W/ytE+3wVXyHPrwd651YssLdXDcROIGV6O4XnCZQHcavw3PWMLOn0VvrghUQ8xJw4n1AxtPiequFiETdZXpsP3g1mlUAnGbyCiMafzWhzLCI/VgJBznPhncl83xQGvvkgV14852WZueJKIOSD8DuAIYIgRrcCvLwoQ8DPtpm8Mwz8RAZIfqLbDJxxHVGwBOqzTX2jzNa06R5gQjNNdKhaZlSF76MTjqmvDJeLQCDd6W1j398tCjZRoy2KmvNFMPft4aqx0dFahf6s85kEoVpivw5Mt4v0AagSRH2OofXERiA4pCxvUPR5IoSmjozhuGSkUijV5T2MvP+Z6PP1jXpNWKuMC5yMHjkCVnYvGG0A3nPM5BoZxeWSkSKQNrOfMLBKIWzoN5IHywVOxo4kAddmcApETzqG/oGM4huVEfX2puVZTHAcQ38jTI8kgWyOgXkVSmL5d5l534Qpnc76vbvcReNjfJqAy7aZDG25Nx+BzovLxv3810TI2UZSC3OGJIHypVDKcp8A8/sEcmxTT8dEoHxF3GRln/cZfQQct83ko7EQSJWxjabM7LvBhErocYzki/EQKNNBFtzuzrjDwbyUUFY6HdqJWAiUa5RIW9mNzHgNoKH1hlYf6zgd1zA3lUeXHOB5+WHvRzDXkaJstzPaK2Hev/FxmqjXNvRNMgZkZdKm28vgDSCcbWjUU7LzllQbLYK44kKjYKOdSfbJArye3D+pI66W6jP9RrUoZKkvEgGhMW25XczcHlwpFd4yXRICPJj2MzgBKN2OqbVLIZ8UikzgP5d6ol61RtsW9YIjcr7wm7cnSJvgYYOO9Ge09UTEJSUglE+SeFlEIjAW4VllslW2MMgQBTsBVum2jeqOqOAjFfG1vCLzsCX2jXOhQQE3+ozHiPD43/diwlkF6gtRcv5qHJFT6GoFN/K0KPo8KdRTv7B6v2y3mSqtpk2gqHiqx92JdRoAeIAJX0BVjq5vrz4pc0jJ1EJsBGSMlUJmlkApvBpF52wEonirFLKzESiFV6PonI1AFG+VQvYvqQFST/EC5cgAAAAASUVORK5CYII="
@click="reset"
/>
<img
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAB3klEQVRoQ+2Yy0oEMRBFz/hABf0Gda2gO9349W50p6Br9RsUVHxS0JEwZCaV5EZp7IFedbpyT91KUpMZI//NRq6fCeCvHZwcmBxozMC/KqFV4BP4akya53Ob68Mz0OvAPrALvAA3wJMneMWYbeAQ2AQegLtcDA+AZeMsCvQGXHeAMPFHwHo013nOCQ+AjTkZshJiqyFS4s3ty1zJegBMtE1wDKxF2XkHrgROpMRbbHP5UVFCIUYPiCbxJszrQA+IZvE1AKpyWuSmq2zisip1QOGETHytAy0Q8nVU60AOIlUKcvGtDpRA7AyHlHwbbnUghpg/RcNebmPsnVy8yoEchL3vIl4NELbYeSfiXU91ev/EVJVQLDJ1QNl7ufgeDljM1IINAMUHlbIXysVaJj58627SPJOpHUhl3tpu+8U9vhRCtQYWibeSCdtoFwgFwDLx4a+npPNMlVUrgEd87pxoWtgtACXiu0HUAtSI7wJRA9AiXg5RCqAQL4UoAVCKl0F4AXqIl0B4AGzMKbAR7cO/cbH1ClwoLrZGf7Void8bnmfgVnAbt6hXsxP7ANgC7odnaV/nKaEQYGW4Xvc2ii3j3HOVALQI6vbtBNAttc7AkwPORHUbNjnQLbXOwKN34BvKiqMxJwSPZAAAAABJRU5ErkJggg=="
@click="close"
/>
</div>
</div>
<div class="img">
<div class="backgroup-img">
<img
class="inner-bg-img"
:src="backgroupImg"
/>
</div>
<div
class="move-img"
:style="{left: `${moveX}px`}"
>
<img
class="inner-mv-img"
:src="moveImg"
/>
</div>
</div>
<div class="slide">
<div
class="slider-mask"
:style="{width: `${blcokLeft}px`}"
>
<div
class="block"
ref="block"
@mousedown="start"
:style="{left: `${blcokLeft}px`}"
>
<span class="yidun_slider_icon"></span>
</div>
</div>
</div>
<div
class="loading"
v-if="loading"
>
<span>loading...</span>
</div>
</div>
</div>
</div>
</template>
<script>
// =========================================
// 父组件需要提供的方法 名称
// =========================================
/**
* 获取滑块图片方法
*/
const GET_IMG_FUN = 'getCaptcha'
/**
* 校验滑块图片方法
*/
const VALID_IMG_FUN = 'validImg'
/**
* 滑块窗口关闭事件监听
*/
const CLOST_EVENT_FUN = 'close'
// import moment from 'moment'
export default {
data() {
return {
/**滑块背景图片 */
backgroupImg: '',
/**滑块图片 */
moveImg: '',
/**是否已经移动滑块 */
startMove: false,
/**滑块移动距离 */
blcokLeft: 0,
/**开始滑动的x轴 */
startX: 0,
/**划过的百分比 */
movePercent: 0,
/**验证码唯一ID */
uuid: '',
/**滑块移动的x轴 */
moveX: 0,
/** 加载遮罩标识 */
loading: false,
Data: {
data: {
bgImageWidth: null,
bgImageHeight: null,
sliderImageWidth: null,
sliderImageHeight: null,
startSlidingTime: null,
endSlidingTime: null,
trackList: [
]
},
id: null
}
}
},
props: {
// 是否开启日志, 默认true
log: {
type: Boolean,
required: false,
default: true
}
},
mounted() {
this.getCaptcha()
},
methods: {
/**
* 打印日志
*/
printLog(msg, ...optionalParams) {
if (this.log) {
if (optionalParams && optionalParams.length > 0) {
console.info(
`滑块验证码[${msg}]`,
optionalParams.length === 1 ? optionalParams[0] : optionalParams
)
} else {
console.info(`滑块验证码[${msg}]`)
}
}
},
/**
* 获取滑块图片
*/
getCaptcha() {
this.loading = true
this.$emit(GET_IMG_FUN, data => {
this.loading = false
if (!data) return
this.backgroupImg = data.captcha.backgroundImage
this.moveImg = data.captcha.templateImage
this.Data.data.bgImageWidth = 280
this.Data.data.bgImageHeight = 180
this.Data.data.sliderImageHeight = 180
this.Data.data.sliderImageWidth = 52
this.uuid = data.id
this.Data.id = data.id
})
},
/**
* 校验图片
*/
validImg(Data) {
this.printLog(`滑块抬起`, this.movePercent)
this.$emit(VALID_IMG_FUN, Data, data => {
// this.printLog(VALID_IMG_FUN, data)
console.log(data)
if (data.success === false) {
this.reset()
return
}
this.close()
})
},
/**
* 重新生成图片
*/
reset() {
this.getCaptcha()
this.moveX = 0
this.movePercent = 0
this.startX = 0
this.blcokLeft = 0
},
/**
* 按钮关闭事件
*/
close() {
this.printLog('关闭按钮触发')
this.$emit(CLOST_EVENT_FUN)
},
/**
* 开始滑动
*/
start(e) {
this.startX = e.pageX
this.startMove = true
window.addEventListener('mousemove', this.move)
window.addEventListener('mouseup', this.up)
},
/**
* 滑块滑动事件
*/
move(e) {
if (!this.startMove) return
// this.Data.data.startSlidingTime = moment(new Date()).format('ddd MMM DD HH:mm:ss [CST] YYYY')
this.Data.data.startSlidingTime = new Date()
const moveX = e.pageX - this.startX
const movePercent = moveX / 280
if (moveX <= 0) {
this.blcokLeft = 0
this.moveX = 0
this.movePercent = 0
} else if (moveX >= 0 && moveX <= 235) {
this.blcokLeft = moveX
this.moveX = moveX
this.movePercent = movePercent
} else if (moveX >= 235) {
this.blcokLeft = 235
this.moveX = 235
this.movePercent = movePercent
}
// const track = {
// x: this.blcokLeft,
// y: e.pageY,
// t: (this.Data.data.endSlidingTime - this.Data.data.startSlidingTime)
// }
const track = {
x: this.blcokLeft,
y: 0,
t: 0 // 初始设为0,稍后在 up 方法中更新为正确的时间间隔
}
if (track.x === 0) {
return
}
this.Data.data.trackList.push(track)
},
/**
* 滑块鼠标抬起事件
*/
up(e) {
window.removeEventListener('mousemove', this.move)
window.removeEventListener('mouseup', this.up)
if (!this.startMove) return
// this.Data.data.endSlidingTime = moment(new Date()).format('ddd MMM DD HH:mm:ss [CST] YYYY')
this.Data.data.endSlidingTime = new Date()
this.Data.data.trackList.forEach(track => {
const endTime = this.Data.data.endSlidingTime.getTime()
const startTime = this.Data.data.startSlidingTime.getTime()
track.t = endTime - startTime
})
this.startMove = false
this.Data.data.endSlidingTime = new Date()
this.validImg(this.Data)
}
},
/**
* 销毁事件
*/
beforeDestroy() {
window.removeEventListener('mousemove', this.move)
window.removeEventListener('mouseup', this.up)
}
}
</script>
<style lang="scss" scoped>
.slider-mask {
position: absolute;
left: 0;
top: 0;
height: 40px;
border: 0 solid #1991fa;
background: #d1e9fe;
border-radius: 2px;
}
.yidun_slider_icon {
position: absolute;
top: 50%;
margin-top: -6px;
left: 50%;
margin-left: -6px;
width: 14px;
height: 10px;
background-image: url(https://cstaticdun.126.net//2.13.7/images/icon_light.4353d81.png);
background-position: 0 -13px;
background-size: 32px 544px;
}
.inner-mv-img,
.inner-bg-img,
.title {
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
-khtml-user-select: none;
user-select: none;
}
.slider {
.mask {
display: block;
z-index: 998;
background: rgba(0, 0, 0, 0);
width: 310px;
height: 280px;
}
.container {
position: absolute;
z-index: 999;
width: 310px;
height: 280px;
margin: auto;
background: rgba(255, 255, 255, 1);
border-radius: 6px;
box-shadow: 0px 0px 11px 0px rgba(153, 153, 153, 1);
box-sizing: border-box;
padding: 17px 15px;
.title {
font-size: 14px;
color: #333;
display: flex;
justify-content: space-between;
.button-group {
img {
width: 25px;
height: 25px;
cursor: pointer;
}
}
}
.img {
width: 280px;
height: 180px;
position: relative;
img {
width: 100%;
}
.backgroup-img {
position: absolute;
left: 0;
top: 0;
width: 100%;
}
.move-img {
width: 52.20338981px;
position: absolute;
left: 0;
top: 0;
}
}
.slide {
width: 100%;
height: 40px;
border: 1px solid #e4e7eb;
background-color: #f7f9fa;
box-sizing: border-box;
position: relative;
&::before {
position: absolute;
content: "按住左边按钮移动完成上方拼图";
display: flex;
justify-content: center;
align-items: center;
font-size: 12px;
color: #999;
width: 100%;
height: 100%;
text-indent: 50px;
}
.block {
width: 40px;
height: 38px;
background-color: #fff;
box-shadow: 0 0 3px rgba(0, 0, 0, 0.3);
display: flex;
justify-content: center;
align-items: center;
position: absolute;
left: 0;
top: 0;
cursor: pointer;
background-size: 30px;
background-repeat: no-repeat;
background-position: center;
}
}
.block:hover {
background-color: #1991fa;
}
.block:hover .yidun_slider_icon {
background-image: url(https://cstaticdun.126.net//2.13.7/images/icon_light.4353d81.png);
background-position: 0 0;
background-size: 32px 544px;
}
.loading {
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.3);
position: absolute;
top: 0;
left: 0;
border-radius: 6px;
display: flex;
justify-content: center;
align-items: center;
color: #fff;
}
}
}
</style>
注:一定要对照好参数,不然传到后台会报错!!!
3.2.2 父组件
(作者这里是一个注册的页面,当用户点击发送验证码的时候,滑动验证码就出现了)
小伙伴可以根据自己的需求更改代码
<template>
<div class="login-container">
<div style="width: 450px; height: 400px; margin: 150px auto; background-color:rgba(165,190,234,0.5); border-radius: 10px">
<div style="width: 100%; height: 100px; font-size: 30px; line-height: 100px; text-align: center; color: #5050bb">欢迎注册</div>
<div style="margin-top: 25px; text-align: center; height: 320px;">
<el-form :model="UserRegisterReq">
<el-form-item label="手机号:" label-width="100px">
<el-input v-model="UserRegisterReq.phone" prefix-icon="el-icon-user" style="width: 80%" placeholder="请输入手机号"></el-input>
</el-form-item>
<el-form-item label="验证码:" label-width="100px">
<el-input v-model="UserRegisterReq.code" prefix-icon="el-icon-user" style="width: 80%" placeholder="请输入验证码"></el-input>
</el-form-item>
<el-form-item label="密码:" label-width="100px">
<el-input v-model="UserRegisterReq.password" show-password prefix-icon="el-icon-lock" style="width: 80%" placeholder="请输入密码"></el-input>
</el-form-item>
<el-form-item label="确认密码:" label-width="100px">
<el-input v-model="UserRegisterReq.confirmPassword" show-password prefix-icon="el-icon-lock" style="width: 80%" placeholder="确认密码"></el-input>
</el-form-item>
<el-form-item>
<el-button type="success" @click="onShow()">点击发送验证码</el-button>
<el-button style="width: 50%; margin-top: 10px" type="primary" @click="register()">注册</el-button>
</el-form-item>
<div style="text-align: center">
<span style="font-size: 15px">已有账号</span>?<a href="javascript:void(0)" style="text-decoration: none; font-size: 15px;" @click="navLogin">点击登录</a>
</div>
</el-form>
<center>
<div v-if="show">
<Slider
ref="sliderComponent"
:captchaData="captchaData"
@getCaptcha="handleGetCaptcha"
@validImg="validImg"
@close="onClose"
:log="true"
></Slider>
</div> </center>
</div>
</div>
</div>
</template>
<script>
import { check, getCaptcha, send, sign } from '@/api/user'
import Slider from '@/views/register/slider.vue'
// import {error} from 'autoprefixer/lib/utils'
export default {
components: {
Slider
},
data() {
return {
UserRegisterReq: {
confirmPassword: ''
},
Data: {
ImageCaptchaTrack: {},
id: ''
},
show: false,
type: 'RANDOM',
captchaData: null
}
},
// 页面加载的时候,做一些事情,在created里面
created() {
// window.localStorage.setItem('isRegisterPage', 'true')
// console.log('注册页面' + window.localStorage.getItem('isRegisterPage'))
},
// 定义一些页面上控件出发的事件调用的方法
mounted() {
},
methods: {
sendCode() {
send(this.UserRegisterReq.phone).then(response => {
alert('验证码是:' + response.data)
})
},
navLogin() {
this.$router.push('/login')
},
register() {
sign(this.UserRegisterReq).then(response => {
if (response.code === 200) {
alert(response.msg)
}
})
// 注册逻辑
},
onClose() {
this.show = false
},
onShow() {
this.show = true
},
handleGetCaptcha(callback) {
getCaptcha(this.type).then(response => {
// console.log(response)
callback(response.data)
})
},
validImg(data, callback) {
// console.log(data)
check(data).then(response => {
// console.log(response)
callback(response.data)
if (response.data.success) {
// this.onClose()
this.sendCode()
// location.reload()
} else {
alert('验证失败')
this.handleGetCaptcha()// 验证失败时重新获取验证码
}
}).catch(error => {
console.error(error)
}).finally(() => {
this.loading = false // 请求结束,设置loading为false
})
}
}
}
</script>
<style scoped>
.login-container {
height: 100vh;
overflow: hidden;
background-image: url('~@/assets/404_images/login_bg.jpg');
background-size: 100%;
display: flex;
align-items: center;
justify-content: center;
}
ul li {
list-style: none;
}
* {
margin: 0;
padding: 0;
}
.top{
overflow: auto;
}
.top li:hover{
cursor: pointer;
}
.top li{
float: left;
height: 40px;
width: 120px;
margin-right: 5px;
line-height: 40px;
text-align: center;
background-color: #409eff;
color: #fff;
font-size: 15px;
box-sizing: border-box;
border: 1px solid #409eff;
}
.captcha-iframe {
width: 300px;
height: 320px;
border: none;
}
.after {
color: #88949d;
}
</style>
好了代码到这里也就基本结束了
3.3 过程中出现的问题
这里讲一个作者在完成滑动验证码时遇到的一个问题,有小伙伴可能会使用springcloud,要注意,滑块验证码不要跟自己的用户模块写在一起,不然这块可能在gateway网关在redis中到不到自己存在redis中的key和value ,并且可能会出现报错
org/springframework/boot/autoconfigure/data/redis/RedisAutoConfiguration.class
具体原因是:
Spring Boot 已经默认提供了 Redis 的自动配置,导致与我们自定义的 Redis 配置发生冲突,也是这个原因导致我们在网关的过滤器中取不到redis中的key和value,因为存和取不是一个redis实例
下面我们来演示一下效果
验证成功后会发送验证码,验证失败会刷新图片
3.4 为什么要刷新图片呢?
如果不刷新图片,恶意攻击者会通过持续尝试来破解验证码。如果一直使用同一张验证码图片,
攻击者可能会使用自动化工具进行大量尝试,增加系统的风险, 尽管刷新验证码可能会增加用户操作的复杂性,但在一定程度上也可以提高用户体验。
如果用户在第一次尝试时失败,系统刷新验证码后,用户有机会重新尝试,避免了因错误导致的长时间等待或退出
到这里就结束啦,感谢大家的一路支持,请给凌弟一个暴击三连吧!跪谢!!