若依前后端分离版学习笔记(二十)——实现滑块验证码(vue3)
参考官方文档实现
一、后端实现
1.1 新增依赖
在ruoyi-framework\pom.xml中新增依赖
<!-- 滑块验证码 -->
<dependency><groupId>com.github.anji-plus</groupId><artifactId>captcha-spring-boot-starter</artifactId><version>1.2.7</version>
</dependency>
<!-- 原有的验证码kaptcha依赖不需要可以删除 -->
1.2 新增yml配置
在application.yml中加入aj-captcha配置
# 滑块验证码
aj:captcha:# 缓存类型cache-type: redis# blockPuzzle 滑块 clickWord 文字点选 default默认两者都实例化type: blockPuzzle# 右下角显示字water-mark: ruoyi.vip# 校验滑动拼图允许误差偏移量(默认5像素)slip-offset: 5# aes加密坐标开启或者禁用(true|false)aes-status: true# 滑动干扰项(0/1/2)interference-options: 2
在ruoyi-admin\src\main\resources\META-INF\services下创建文件com.anji.captcha.service.CaptchaCacheService并写入内容
com.ruoyi.framework.web.service.CaptchaRedisService
这里注意文件路径一定要对好,否则扫不到CaptchaRedisService,当生成滑块验证码时会发生空指针报错
1.3 配置匿名访问
在SecurityConfig中设置httpSecurity配置匿名访问(可不通过身份验证直接访问)
.antMatchers("/login", "/captcha/get", "/captcha/check").permitAll()
1.4 修改相关类
1.移除不需要的类
ruoyi-admin\com\ruoyi\web\controller\common\CaptchaController.java
ruoyi-framework\com\ruoyi\framework\config\CaptchaConfig.java
ruoyi-framework\com\ruoyi\framework\config\KaptchaTextCreator.java
我这里直接注释掉
2.更改登录方法
修改ruoyi-admin\com\ruoyi\web\controller\system\SysLoginController.java
/*** 登录方法* * @param loginBody 登录信息* @return 结果*/@PostMapping("/login")public AjaxResult login(@RequestBody LoginBody loginBody){AjaxResult ajax = AjaxResult.success();// 生成令牌
// String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
// loginBody.getUuid());String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode());ajax.put(Constants.TOKEN, token);return ajax;}
修改ruoyi-framework\com\ruoyi\framework\web\service\SysLoginService.java
package com.ruoyi.framework.web.service;import javax.annotation.Resource;import com.anji.captcha.model.common.ResponseModel;
import com.anji.captcha.model.vo.CaptchaVO;
import com.anji.captcha.service.CaptchaService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import com.ruoyi.common.constant.CacheConstants;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.constant.UserConstants;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.exception.user.BlackListException;
import com.ruoyi.common.exception.user.CaptchaException;
import com.ruoyi.common.exception.user.CaptchaExpireException;
import com.ruoyi.common.exception.user.UserNotExistsException;
import com.ruoyi.common.exception.user.UserPasswordNotMatchException;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.MessageUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.ip.IpUtils;
import com.ruoyi.framework.manager.AsyncManager;
import com.ruoyi.framework.manager.factory.AsyncFactory;
import com.ruoyi.framework.security.context.AuthenticationContextHolder;
import com.ruoyi.system.service.ISysConfigService;
import com.ruoyi.system.service.ISysUserService;/*** 登录校验方法* * @author ruoyi*/
@Component
public class SysLoginService
{@Autowiredprivate TokenService tokenService;@Resourceprivate AuthenticationManager authenticationManager;@Autowiredprivate RedisCache redisCache;@Autowiredprivate ISysUserService userService;@Autowiredprivate ISysConfigService configService;@Autowired@Lazyprivate CaptchaService captchaService;/*** 登录验证* * @param username 用户名* @param password 密码* @param code 验证码* @return 结果*/public String login(String username, String password, String code){// 验证码校验validateCaptcha(username, code);// 登录前置校验loginPreCheck(username, password);// 用户验证Authentication authentication = null;try{UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);AuthenticationContextHolder.setContext(authenticationToken);// 该方法会去调用UserDetailsServiceImpl.loadUserByUsernameauthentication = authenticationManager.authenticate(authenticationToken);}catch (Exception e){if (e instanceof BadCredentialsException){AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));throw new UserPasswordNotMatchException();}else{AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));throw new ServiceException(e.getMessage());}}finally{AuthenticationContextHolder.clearContext();}AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));LoginUser loginUser = (LoginUser) authentication.getPrincipal();recordLoginInfo(loginUser.getUserId());// 生成tokenreturn tokenService.createToken(loginUser);}/*** 校验验证码** @param username 用户名* @param code 验证码* @return 结果*/public void validateCaptcha(String username, String code){CaptchaVO captchaVO = new CaptchaVO();captchaVO.setCaptchaVerification(code);ResponseModel response = captchaService.verification(captchaVO);if (!response.isSuccess()){AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")));throw new CaptchaException();}}/*** 登录前置校验* @param username 用户名* @param password 用户密码*/public void loginPreCheck(String username, String password){// 用户名或密码为空 错误if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)){AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("not.null")));throw new UserNotExistsException();}// 密码如果不在指定范围内 错误if (password.length() < UserConstants.PASSWORD_MIN_LENGTH|| password.length() > UserConstants.PASSWORD_MAX_LENGTH){AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));throw new UserPasswordNotMatchException();}// 用户名不在指定范围内 错误if (username.length() < UserConstants.USERNAME_MIN_LENGTH|| username.length() > UserConstants.USERNAME_MAX_LENGTH){AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));throw new UserPasswordNotMatchException();}// IP黑名单校验String blackStr = configService.selectConfigByKey("sys.login.blackIPList");if (IpUtils.isMatchedIp(blackStr, IpUtils.getIpAddr())){AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("login.blocked")));throw new BlackListException();}}/*** 记录登录信息** @param userId 用户ID*/public void recordLoginInfo(Long userId){SysUser sysUser = new SysUser();sysUser.setUserId(userId);sysUser.setLoginIp(IpUtils.getIpAddr());sysUser.setLoginDate(DateUtils.getNowDate());userService.updateUserProfile(sysUser);}
}
3.新增验证码缓存redis服务类
ruoyi-framework\com\ruoyi\framework\web\service\CaptchaRedisService.java
package com.ruoyi.framework.web.service;import java.util.concurrent.TimeUnit;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import com.anji.captcha.service.CaptchaCacheService;/*** 自定义redis验证码缓存实现类* * @author ruoyi*/
public class CaptchaRedisService implements CaptchaCacheService
{@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Overridepublic void set(String key, String value, long expiresInSeconds){stringRedisTemplate.opsForValue().set(key, value, expiresInSeconds, TimeUnit.SECONDS);}@Overridepublic boolean exists(String key){return stringRedisTemplate.hasKey(key);}@Overridepublic void delete(String key){stringRedisTemplate.delete(key);}@Overridepublic String get(String key){return stringRedisTemplate.opsForValue().get(key);}@Overridepublic Long increment(String key, long val){return stringRedisTemplate.opsForValue().increment(key, val);}@Overridepublic String type(){return "redis";}
}
1.5 代码启动
二、前端实现
2.1 新增依赖
在package.json中新增crypto-js依赖(启动前需要重新安装依赖)
{"name": "ruoyi","version": "3.9.0","description": "若依管理系统","author": "若依","license": "MIT","type": "module","scripts": {"dev": "vite","build:prod": "vite build","build:stage": "vite build --mode staging","preview": "vite preview"},"repository": {"type": "git","url": "https://gitee.com/y_project/RuoYi-Vue.git"},"dependencies": {"@element-plus/icons-vue": "2.3.1","@vueup/vue-quill": "1.2.0","@vueuse/core": "13.3.0","axios": "1.9.0","clipboard": "2.0.11","echarts": "5.6.0","element-plus": "2.10.7","file-saver": "2.0.5","fuse.js": "6.6.2","js-beautify": "1.14.11","js-cookie": "3.0.5","jsencrypt": "3.3.2","nprogress": "0.2.0","pinia": "3.0.2","splitpanes": "4.0.4","vue": "3.5.16","vue-cropper": "1.1.1","vue-router": "4.5.1","vuedraggable": "4.1.0","crypto-js": "4.1.1"},"devDependencies": {"@vitejs/plugin-vue": "5.2.4","sass-embedded": "1.89.1","unplugin-auto-import": "0.18.6","unplugin-vue-setup-extend-plus": "1.0.1","vite": "6.3.5","vite-plugin-compression": "0.5.1","vite-plugin-svg-icons": "2.0.1"},"overrides": {"quill": "2.0.2"}
}
2.2 导入组件,图片
将Verifition组件放入src\components下,异常图片放到src\assets\images下
2.3 修改代码
- src\api\login.js
去除login方法的uuid参数
// 登录方法
export function login(username, password, code) {const data = {username,password,code}return request({url: '/login',headers: {isToken: false,repeatSubmit: false},method: 'post',data: data})
}
2.src\store\modules\user.js
去除uuid
// 登录
login(userInfo) {const username = userInfo.username.trim()const password = userInfo.passwordconst code = userInfo.codereturn new Promise((resolve, reject) => {login(username, password, code).then(res => {setToken(res.token)this.token = res.tokenresolve()}).catch(error => {reject(error)})})
}
3.src\views\login.vue
代码中带有必要注释
<template><div class="login"><el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form"><h3 class="title">{{ title }}</h3><el-form-item prop="username"><el-inputv-model="loginForm.username"type="text"size="large"auto-complete="off"placeholder="账号"><template #prefix><svg-icon icon-class="user" class="el-input__icon input-icon" /></template></el-input></el-form-item><el-form-item prop="password"><el-inputv-model="loginForm.password"type="password"size="large"auto-complete="off"placeholder="密码"@keyup.enter="handleLogin"><template #prefix><svg-icon icon-class="password" class="el-input__icon input-icon" /></template></el-input></el-form-item>
<!-- <el-form-item prop="code" v-if="captchaEnabled">-->
<!-- <el-input-->
<!-- v-model="loginForm.code"-->
<!-- size="large"-->
<!-- auto-complete="off"-->
<!-- placeholder="验证码"-->
<!-- style="width: 63%"-->
<!-- @keyup.enter="handleLogin"-->
<!-- >-->
<!-- <template #prefix><svg-icon icon-class="validCode" class="el-input__icon input-icon" /></template>-->
<!-- </el-input>-->
<!-- <div class="login-code">-->
<!-- <img :src="codeUrl" @click="getCode" class="login-code-img"/>-->
<!-- </div>-->
<!-- </el-form-item>--><!--修改为滑块验证码,切换点击文字验证的话将captchaType的值修改为clickWord--><Verify@success="capctchaCheckSuccess":mode="'pop'":captchaType="'blockPuzzle'":imgSize="{ width: '330px', height: '155px' }"ref="verify"></Verify><el-checkbox v-model="loginForm.rememberMe" style="margin:0px 0px 25px 0px;">记住密码</el-checkbox><el-form-item style="width:100%;"><el-button:loading="loading"size="large"type="primary"style="width:100%;"@click.prevent="handleLogin"><span v-if="!loading">登 录</span><span v-else>登 录 中...</span></el-button><div style="float: right;" v-if="register"><router-link class="link-type" :to="'/register'">立即注册</router-link></div></el-form-item></el-form><!-- 底部 --><div class="el-login-footer"><span>Copyright © 2018-2025 ruoyi.vip All Rights Reserved.</span></div></div>
</template><script setup>
import { getCodeImg } from "@/api/login"
import Cookies from "js-cookie"
import { encrypt, decrypt } from "@/utils/jsencrypt"
import useUserStore from '@/store/modules/user'
// 引用组件Verify
import Verify from "@/components/Verifition/Verify";const title = import.meta.env.VITE_APP_TITLE
const userStore = useUserStore()
const route = useRoute()
const router = useRouter()
const { proxy } = getCurrentInstance()const loginForm = ref({username: "admin",password: "admin123",rememberMe: false,code: ""// 去除uuid// uuid: ""
})const loginRules = {username: [{ required: true, trigger: "blur", message: "请输入您的账号" }],password: [{ required: true, trigger: "blur", message: "请输入您的密码" }]// 去除code// code: [{ required: true, trigger: "change", message: "请输入验证码" }]
}// 去除验证码验证
// const codeUrl = ref("")
const loading = ref(false)
// 验证码开关
const captchaEnabled = ref(true)
// 注册开关
const register = ref(false)
const redirect = ref(undefined)watch(route, (newRoute) => {redirect.value = newRoute.query && newRoute.query.redirect
}, { immediate: true })function handleLogin() {proxy.$refs.loginRef.validate(valid => {if (valid) {// 展示滑块验证码proxy.$refs.verify.show();loading.value = true// 勾选了需要记住密码设置在 cookie 中设置记住用户名和密码if (loginForm.value.rememberMe) {Cookies.set("username", loginForm.value.username, { expires: 30 })Cookies.set("password", encrypt(loginForm.value.password), { expires: 30 })Cookies.set("rememberMe", loginForm.value.rememberMe, { expires: 30 })} else {// 否则移除Cookies.remove("username")Cookies.remove("password")Cookies.remove("rememberMe")}// 调用action的登录方法userStore.login(loginForm.value).then(() => {const query = route.queryconst otherQueryParams = Object.keys(query).reduce((acc, cur) => {if (cur !== "redirect") {acc[cur] = query[cur]}return acc}, {})router.push({ path: redirect.value || "/", query: otherQueryParams })}).catch(() => {loading.value = false// 重新获取验证码// if (captchaEnabled.value) {// getCode()// }})}})
}// 去除验证码验证
// function getCode() {
// getCodeImg().then(res => {
// captchaEnabled.value = res.captchaEnabled === undefined ? true : res.captchaEnabled
// if (captchaEnabled.value) {
// codeUrl.value = "data:image/gif;base64," + res.img
// loginForm.value.uuid = res.uuid
// }
// })
// }function getCookie() {const username = Cookies.get("username")const password = Cookies.get("password")const rememberMe = Cookies.get("rememberMe")loginForm.value = {username: username === undefined ? loginForm.value.username : username,password: password === undefined ? loginForm.value.password : decrypt(password),rememberMe: rememberMe === undefined ? false : Boolean(rememberMe)}
}// 去除验证码验证
// getCode()
getCookie()
</script><style lang='scss' scoped>
.login {display: flex;justify-content: center;align-items: center;height: 100%;background-image: url("../assets/images/login-background.jpg");background-size: cover;
}
.title {margin: 0px auto 30px auto;text-align: center;color: #707070;
}.login-form {border-radius: 6px;background: #ffffff;width: 400px;padding: 25px 25px 5px 25px;z-index: 1;.el-input {height: 40px;input {height: 40px;}}.input-icon {height: 39px;width: 14px;margin-left: 0px;}
}
.login-tip {font-size: 13px;text-align: center;color: #bfbfbf;
}
.login-code {width: 33%;height: 40px;float: right;img {cursor: pointer;vertical-align: middle;}
}
.el-login-footer {height: 40px;line-height: 40px;position: fixed;bottom: 0;width: 100%;text-align: center;color: #fff;font-family: Arial;font-size: 12px;letter-spacing: 1px;
}
.login-code-img {height: 40px;padding-left: 12px;
}
</style>
三、效果展示
滑块验证
点击文字验证