ruoyi-vue3 集成aj-captcha实现滑块、文字点选验证码功能
作者:七维大脑
0. 前言
其实若依的官方文档中有集成aj-captcha实现滑块验证码的部分,但是一直给的前端示例代码中都是Vue2的版本,而且后端部分也一直未保持更新。再比如官方文档在集成aj-captcha后并未实现验证码开关的功能。
然后我最近正好在用若依的Vue3版本做东西,正好记录一下。
0.1 说明
以官方文档为模板写的这篇文章,所以中间会穿插官方文档中的一些文字。
文章中所涉及的截图、代码,由于我已经使用 若依框架包名修改器 修改过了,所以包名、模块名前缀会和原版有出入,但仅限于包名和模块名。请注意甄别。
本文基于后端RuoYi-Vue 3.8.7
和 前端 RuoYi-Vue3 3.8.7
官方文档在集成后并没有实现验证码开关功能,本文会进行实现。
集成以AJ-Captcha文字点选验证码为例,不需要键盘手动输入,极大优化了传统验证码用户体验不佳的问题。目前对外提供两种类型的验证码,其中包含滑动拼图、文字点选。
1. 后端部分
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
验证码依赖:
<!-- 验证码 --> <dependency> <groupId>pro.fessional</groupId> <artifactId>kaptcha</artifactId> <exclusions> <exclusion> <artifactId>servlet-api</artifactId> <groupId>javax.servlet</groupId> </exclusion> </exclusions> </dependency>
最终 pom.xml
截图:
1.2. 修改 application.yml
修改application.yml,加入aj-captcha相关配置:
(我的项目使用的是文字点选,如需要使用滑块,type
设置为 blockPuzzle
即可)
# 滑块验证码 aj: captcha: # 缓存类型 cache-type: redis # blockPuzzle 滑块 clickWord 文字点选 default默认两者都实例化 type: clickWord # 右下角显示字 water-mark: B站、抖音同名搜索七维大脑 # 校验滑动拼图允许误差偏移量(默认5像素) slip-offset: 5 # aes加密坐标开启或者禁用(true|false) aes-status: true # 滑动干扰项(0/1/2) interference-options: 2
1.3. 新增 CaptchaRedisService 类
在 ruoyi-framework
模块下,com.ruoyi.framework.web.service
包下创建CaptchaRedisService.java
类,内容如下:
(请复制粘贴后注意修改包路径为自己项目真实路径)
package xyz.ytxy.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 { @Autowired private StringRedisTemplate stringRedisTemplate; @Override public void set(String key, String value, long expiresInSeconds) { stringRedisTemplate.opsForValue().set(key, value, expiresInSeconds, TimeUnit.SECONDS); } @Override public boolean exists(String key) { return Boolean.TRUE.equals(stringRedisTemplate.hasKey(key)); } @Override public void delete(String key) { stringRedisTemplate.delete(key); } @Override public String get(String key) { return stringRedisTemplate.opsForValue().get(key); } @Override public Long increment(String key, long val) { return stringRedisTemplate.opsForValue().increment(key, val); } @Override public String type() { return "redis"; } }
1.4. 添加必须文件
在ruoyi-admin
模块下,找到 resources
目录
在 resources
目录找到 META-INF
目录在 META-INF
目录中新建 services
文件夹
在 services
文件夹中新建 com.anji.captcha.service.CaptchaCacheService
文件(注意是文件)
在 com.anji.captcha.service.CaptchaCacheService
文件中输入 xxx.xxx.framework.web.service.CaptchaRedisService
(也就是刚刚创建的CaptchaRedisService类的真实路径)
1.5. 移除不需要的类
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
1.6. 修改登录方法
修改 ruoyi-admin
模块下 com.ruoyi.web.controller.system.SysLoginController.java
类中的 login
方法:
/** * 登录方法 * * @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()); ajax.put(Constants.TOKEN, token); return ajax; }
修改后生成令牌这一步比原版少了 loginBody.getUuid()
参数。
修改 ruoyi-framework
模块下的com.ruoyi.framework.web.service.SysLoginService.java
类:
package xyz.ytxy.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 xyz.ytxy.common.constant.CacheConstants; import xyz.ytxy.common.constant.Constants; import xyz.ytxy.common.constant.UserConstants; import xyz.ytxy.common.core.domain.entity.SysUser; import xyz.ytxy.common.core.domain.model.LoginUser; import xyz.ytxy.common.core.redis.RedisCache; import xyz.ytxy.common.exception.ServiceException; import xyz.ytxy.common.exception.user.BlackListException; import xyz.ytxy.common.exception.user.CaptchaException; import xyz.ytxy.common.exception.user.CaptchaExpireException; import xyz.ytxy.common.exception.user.UserNotExistsException; import xyz.ytxy.common.exception.user.UserPasswordNotMatchException; import xyz.ytxy.common.utils.DateUtils; import xyz.ytxy.common.utils.MessageUtils; import xyz.ytxy.common.utils.StringUtils; import xyz.ytxy.common.utils.ip.IpUtils; import xyz.ytxy.framework.manager.AsyncManager; import xyz.ytxy.framework.manager.factory.AsyncFactory; import xyz.ytxy.framework.security.context.AuthenticationContextHolder; import xyz.ytxy.system.service.ISysConfigService; import xyz.ytxy.system.service.ISysUserService; /** * 登录校验方法 * * @author ruoyi */ @Component public class SysLoginService { @Autowired private TokenService tokenService; @Resource private AuthenticationManager authenticationManager; @Autowired private RedisCache redisCache; @Autowired private ISysUserService userService; @Autowired private ISysConfigService configService; @Autowired @Lazy private 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.loadUserByUsername authentication = 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()); // 生成token return tokenService.createToken(loginUser); } /** * 校验验证码 * * @param username 用户名 * @param code 验证码 * @return 结果 */ public void validateCaptcha(String username, String code) { boolean captchaEnabled = configService.selectCaptchaEnabled(); if (captchaEnabled) { 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); } }
login
方法比原版少了uuid
的参数validateCaptcha
方法比原版少了uuid
的参数,方法内容更改为aj-captcha的验证方式- 其他内容未更改
这地方如果直接替换官方文档中的代码会造成部分新功能缺失。所以这里直接替换我提供的代码即可。(注意替换后将包名改为你实际的包名)
1.7. 新增验证码开关获取接口
在 ruoyi-admin
模块下的 com.ruoyi.web.controller.common
包新增 CaptchaEnabledController.java
:
(注意将包名改为你实际的包名)
package xyz.ytxy.web.controller.common; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import xyz.ytxy.common.core.domain.AjaxResult; import xyz.ytxy.system.service.ISysConfigService; /** * 验证码操作处理 * * @author B站、抖音搜索:七维大脑 点个关注呗 */ @RestController public class CaptchaEnabledController { @Autowired private ISysConfigService configService; /** * 获取验证码开关 */ @GetMapping("/captchaEnabled") public AjaxResult captchaEnabled() { AjaxResult ajax = AjaxResult.success(); boolean captchaEnabled = configService.selectCaptchaEnabled(); ajax.put("captchaEnabled", captchaEnabled); return ajax; } }
1.8. 允许匿名访问
在ruoyi-framework
模块下的 com.ruoyi.framework.config
包下找到 SecurityConfig.java
类,修改以下内容:
原版:
// 对于登录login 注册register 验证码captchaImage 允许匿名访问 .antMatchers("/login", "/register", "/captchaImage").permitAll()
修改为:
// 对于登录login 注册register 滑块验证码/captcha/get /captcha/check 获取验证码开关 /captchaEnabled 允许匿名访问 .antMatchers("/login", "/register", "/captcha/get", "/captcha/check", "/captchaEnabled").permitAll()
2. 前端部分(Vue3)
2.1. 新增依赖 crypto-js
在 package.json
的 "dependencies"
中新增 "crypto-js": "4.1.1"
:
新增后重新 install
,比如我用的pnpm
,直接执行:pnpm install --registry=https://registry.npmmirror.com
2.2. 新增 Verifition 组件
此部分代码我放到了阿里云盘:https://www.alipan.com/s/4hEbavUC4Np
下载后粘贴到 src/components
目录下:
2.3. 修改login.js
import request from '@/utils/request' // 登录方法 export function login(username, password, code) { const data = { username, password, code } return request({ url: '/login', headers: { isToken: false, repeatSubmit: false }, method: 'post', data: data }) } // 注册方法 export function register(data) { return request({ url: '/register', headers: { isToken: false }, method: 'post', data: data }) } // 获取用户详细信息 export function getInfo() { return request({ url: '/getInfo', method: 'get' }) } // 退出方法 export function logout() { return request({ url: '/logout', method: 'post' }) } // 获取验证码开关 export function isCaptchaEnabled() { return request({ url: '/captchaEnabled', method: 'get' }) }
- 修改了
login
函数,去掉了uuid
参数 - 删除了获取验证码函数
getCodeImg
- 新增了获取验证码开关函数
isCaptchaEnabled
2.4. 修改 user.js
删除 uuid
参数 :
// 登录 login(userInfo) { const username = userInfo.username.trim() const password = userInfo.password const code = userInfo.code return new Promise((resolve, reject) => { login(username, password, code).then(res => { setToken(res.token) this.token = res.token resolve() }).catch(error => { reject(error) }) }) },
2.5. 修改login.vue
修改内容较多,建议直接替换再修改:
<template> <div class="login"> <el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form"> <h3 class="title">若依后台管理系统</h3> <el-form-item prop="username"> <el-input v-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-input v-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> <Verify @success="capctchaCheckSuccess" :mode="'pop'" :captchaType="'clickWord'" :imgSize="{ width: '330px', height: '155px' }" ref="verify" v-if="captchaEnabled" ></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-2023 ruoyi.vip All Rights Reserved.</span> </div> </div> </template> <script setup> import Cookies from "js-cookie"; import {encrypt, decrypt} from "@/utils/jsencrypt"; import useUserStore from '@/store/modules/user' import Verify from "@/components/Verifition/Verify"; import {isCaptchaEnabled} from "@/api/login"; const userStore = useUserStore() const route = useRoute(); const router = useRouter(); const {proxy} = getCurrentInstance(); const loginForm = ref({ username: "admin", password: "admin123", rememberMe: false, code: "" }); const loginRules = { username: [{required: true, trigger: "blur", message: "请输入您的账号"}], password: [{required: true, trigger: "blur", message: "请输入您的密码"}] }; 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 userRouteLogin() { // 调用action的登录方法 userStore.login(loginForm.value).then(() => { const query = route.query; const 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; }); } function handleLogin() { proxy.$refs.loginRef.validate(valid => { if (valid && captchaEnabled.value) { proxy.$refs.verify.show(); } else if (valid && !captchaEnabled.value) { userRouteLogin(); } }); } 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) }; } function capctchaCheckSuccess(params) { loginForm.value.code = params.captchaVerification; 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"); } userRouteLogin(); } // 获取验证码开关 function getCaptchaEnabled() { isCaptchaEnabled().then(res => { captchaEnabled.value = res.captchaEnabled === undefined ? true : res.captchaEnabled; }); } getCookie(); getCaptchaEnabled(); </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; .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; } .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; } </style>
2.6. 切换文字点选或滑块验证码
有两种类型,一种是文字点选,一种是滑块验证,那如何切换呢?
2.6.1 后端修改
修改fcat-admin
模块下 application.yml
中的 aj — type
:
- 填写
blockPuzzle
为滑块 - 填写
clickWord
为文字点选
2.6.2 前端修改
修改 login.vue
:
<Verify @success="capctchaCheckSuccess" :mode="'pop'" :captchaType="'clickWord'" :imgSize="{ width: '330px', height: '155px' }" ref="verify" v-if="captchaEnabled" ></Verify>
修改上述代码中的 captchaType
填写blockPuzzle
为滑块填写 clickWord
为文字点选
2.7. 成果展示:
默认底图展示,用于接口异常等情况:
滑块验证码正常显示截图:
文字点选验证码正常显示截图:
到此这篇关于 ruoyi-vue3 集成aj-captcha实现滑块、文字点选验证码的文章就介绍到这了,更多相关ruoyi-vue3 滑块验证码内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!