Table of contents
0. Preface
In fact, Ruoyi's official documentation includes a part that integrates aj-captcha to implement the slider verification code, but the front-end sample code always provided is the Vue2 version, and the back-end part has not been updated. Another example is that the official document does not implement the verification code switch function after integrating aj-captcha.
Then I happened to be making something using Zoey's Vue3 version recently, so I just wanted to record it.
0.1 Description
-
This article is written based on the official document as a template, so some text from the official document will be interspersed in the middle.
-
The screenshots and codes involved in the article have been modified usingRuoyi Framework Package Name Modifier, so the package name , the module name prefix will be different from the original version, but only in the package name and module name. Please pay attention to screening.
-
This article is based on back-end
RuoYi-Vue 3.8.7
and front-endRuoYi-Vue3 3.8.7
-
The official document does not implement the verification code switch function after integration. This article will implement it.
Taking the AJ-Captcha text click verification code as an example, the integration does not require manual keyboard input, which greatly optimizes the problem of poor user experience with traditional verification codes. Currently, two types of verification codes are provided to the outside world, including sliding puzzles and text clicks.
1. Backend part
1.1 Add dependencies
Add the following dependencies to in the ruoyi-framework
module:pom.xml
<!-- 滑块验证码 -->
<dependency>
<groupId>com.github.anji-plus</groupId>
<artifactId>captcha-spring-boot-starter</artifactId>
<version>1.2.7</version>
</dependency>
Delete the original kaptcha
verification code dependency:
<!-- 验证码 -->
<dependency>
<groupId>pro.fessional</groupId>
<artifactId>kaptcha</artifactId>
<exclusions>
<exclusion>
<artifactId>servlet-api</artifactId>
<groupId>javax.servlet</groupId>
</exclusion>
</exclusions>
</dependency>
The end pom.xml
Picture:
1.2. Modify application.yml
Modify application.yml and add aj-captcha related configuration:
(My project uses text selection, if you need to use a slider, type
Set to blockPuzzle
(that’s it)
# 滑块验证码
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. Added CaptchaRedisService class
Create the class under the ruoyi-framework
module and com.ruoyi.framework.web.service
package with the following content: CaptchaRedisService.java
(Please be careful to modify the package path to the real path of your project after copying and pasting)
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. Add necessary files
- Under the
ruoyi-admin
module, find theresources
directory - found in
resources
directoryMETA-INF
directory - Create a new folder in the
META-INF
directoryservices
- Create a new file in the
services
folder (note that it is a file)com.anji.captcha.service.CaptchaCacheService
- Enter in the
com.anji.captcha.service.CaptchaCacheService
file (that is, the real path of the CaptchaRedisService class just created)xxx.xxx.framework.web.service.CaptchaRedisService
1.5. Remove unnecessary classes
ruoyi-admin
Under modulecom.ruoyi.web.controller.common.CaptchaController.java
ruoyi-framework
Under modulecom.ruoyi.framework.config.CaptchaConfig.java
ruoyi-framework
Under modulecom.ruoyi.framework.config.KaptchaTextCreator.java
1.6. Modify login method
Repair ruoyi-admin
Model com.ruoyi.web.controller.system.SysLoginController.java
Type login
Method:
/**
* 登录方法
*
* @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;
}
The modified step of generating tokens has fewer loginBody.getUuid()
parameters than the original version.
Revised ruoyi-framework
Modifiedcom.ruoyi.framework.web.service.SysLoginService.java
Class:
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
method has fewer parameters than the original versionuuid
validateCaptcha
The method has fewer parameters than the original versionuuid
, and the method content is changed to the aj-captcha verification method- No other content has changed
If you directly replace the code in the official documentation here, some new functions will be missing. So just replace the code I provided here. (Note that after replacement, change the package name to your actual package name)
1.7. Added verification code switch acquisition interface
existent ruoyi-admin
under the model com.ruoyi.web.controller.common
bao new growth CaptchaEnabledController.java
:
(Note to change the package name to your actual package name)
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. Allow anonymous access
Find the class under the package under the ruoyi-framework
module and modify the following content: com.ruoyi.framework.config
SecurityConfig.java
Original version:
// 对于登录login 注册register 验证码captchaImage 允许匿名访问
.antMatchers("/login", "/register", "/captchaImage").permitAll()
change into:
// 对于登录login 注册register 滑块验证码/captcha/get /captcha/check 获取验证码开关 /captchaEnabled 允许匿名访问
.antMatchers("/login", "/register", "/captcha/get", "/captcha/check", "/captchaEnabled").permitAll()
2. Front-end part (Vue3)
2.1. Add new dependency crypto-js
existing package.json
"dependencies"
中新增 "crypto-js": "4.1.1"
:
Re-add install
, for example, I use pnpm
, execute directly: pnpm install --registry=https://registry.npmmirror.com
2.2. Added Verifition component
I put this part of the code on Alibaba Cloud Disk:https://www.alipan.com/s/4hEbavUC4Np
Download and paste into the src/components
directory:
2.3. Modify 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'
})
}
- Modified the
login
function and removed theuuid
parameter - Removed the get verification code function
getCodeImg
- Added a new function to get the verification code switch
isCaptchaEnabled
2.4. Modify user.js
Exclusion uuid
Reference number:
// 登录
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. Modify login.vue
There are a lot of modifications, it is recommended to directly replace and then modify:
<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. Switch text click or slider verification code
There are two types, one is text click and the other is slider verification, so how to switch?
2.6.1 Backend modification
Repairfcat-admin
Mojoshita application.yml
Medium aj — type
:
- Fill in
blockPuzzle
for slider - Fill in
clickWord
Click for text
2.6.2 Front-end modification
Revision login.vue
:
<Verify
@success="capctchaCheckSuccess"
:mode="'pop'"
:captchaType="'clickWord'"
:imgSize="{ width: '330px', height: '155px' }"
ref="verify"
v-if="captchaEnabled"
></Verify>
Modify the above codecaptchaType
- Fill in
blockPuzzle
for slider - Fill in
clickWord
Click for text
2.7. Results display:
-
The default basemap display is used for interface exceptions and other situations:
-
Screenshot of the slider verification code displaying normally:
-
Screenshot of the normal display of the verification code when clicking on the text: