由于最近在集成这玩意儿的时候实在是报错报麻了,所以写一下笔记记录一下,如果有人跟我一样,集成这个东西的时候各种报错,可以往下翻一翻找一下有没有你遇到的情况。
首先准备东西:
这里边的集成aj-captcha实现滑块验证码部分里第五步有一个百度网盘链接,先下载以后有用。
aj-captcha源码下载:AJ-Captcha: 行为验证码(滑动拼图、点选文字),前后端(java)交互,包含vue/h5/Android/IOS/flutter/uni-app/react/php/go/微信小程序的源码和实现
步骤参考:05-若依使用AJ-captcha验证_刀不封的博客-CSDN博客
但是我觉得这文章还缺点东西,所以我写这篇再给他封装一层。
找到ruoyi-framework\pom.xml文件,在<dependencies></dependencies>里边添加上依赖
<!-- anji滑块验证码 -->
<dependency>
<groupId>com.anji-plus</groupId>
<artifactId>spring-boot-starter-captcha</artifactId>
<version>1.3.0</version>
</dependency>
原来那个验证码的依赖kaptcha可以删掉不要了
添加好依赖之后记得更新一下maven
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
这上边都是最基本的设置,想要其他配置效果可以参考AJ-Captcha的文档:
找到ruoyi-admin\src\main\resources\META-INF\services下创建com.anji.captcha.service.CaptchaCacheService文件(不需要后缀名)同时设置文件内容为:
com.ruoyi.framework.web.service.CaptchaRedisService
这里的地址要跟你下一步写CaptchaRedisService的路径一致,不然扫不到包。
在 ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java 文件中configure方法下的httpSecurity添加如下语句:
.antMatchers("/login", "/captcha/get", "/captcha/check").permitAll()
这步保证这仨方法能够不登陆就访问,不然你登录的时候要求你有权限才能登录,就尬住了。
“我要登录。”
"对不起先生,您没有权限,不能登录。"
“我怎么才能有权限?”
"您需要先登录才有权限。"
“我要登录。”
"对不起先生,您没有权限,不能登录。"
我们要的是20岁拥有30年工作经验的人才.java
若依官方文档让修改 ruoyi-admin\com\ruoyi\web\controller\system\SysLoginController.java,但是其实改不改都行,区别就是改之前四个参数,改之后三个参数,多的那个参数uuid在换成滑动验证码之后其实用不到,所以传不传都行。
但是得保证修改之后,SysLoginController里的调用和SysLoginService里的定义是一致的,不管三个还是四个参数,得对应起来,不然就会报错。
我参考那个文章里有一句提醒:
大家在修改代码时,切记不要按照文档官方文档直接覆盖,建议使用文本比较工具将代码进行比对后只更新相关内容,这也是开发人员的良好习惯!!!!
这个其实很重要,但是翻了翻一些文档对比工具,好多都要钱,而且如果是在公司电脑上开发,有可能还不让你下载,所以我用的这个http://www.jsons.cn/txtdiff/
免费的,虽然界面可能low了点,但是挺好使。
并且记得边修改边思考,因为自身水平有限,所以我经常性进行递归式学习,看到这句代码,不知道干嘛的,点进去看一看,百度搜一搜,然后看的过程中又发现了不懂的,又往里进一层,继续点进去看继续搜。然后一层一层越进越深。但是学的东西也越来越多。
盗梦空间了属于是。
主要是这是个学习的过程,不是只为了使用而去使用。学习嘛,不寒碜。
修改com/ruoyi/framework/web/service/SysLoginService.java的内容,这个文件建议不要看北京时间2022年10月9日21:00之前的官方文档,那个版本官方文档里缺两句东西,用了会报错。
package com.ruoyi.framework.web.service;
import javax.annotation.Resource;
import com.ruoyi.framework.security.context.AuthenticationContextHolder;
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.anji.captcha.model.common.ResponseModel;
import com.anji.captcha.model.vo.CaptchaVO;
import com.anji.captcha.service.CaptchaService;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.exception.user.CaptchaException;
import com.ruoyi.common.exception.user.UserPasswordNotMatchException;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.MessageUtils;
import com.ruoyi.common.utils.ServletUtils;
import com.ruoyi.common.utils.ip.IpUtils;
import com.ruoyi.framework.manager.AsyncManager;
import com.ruoyi.framework.manager.factory.AsyncFactory;
import com.ruoyi.system.service.ISysUserService;
/**
* 登录校验方法
*
* @author ruoyi
*/
@Component
public class SysLoginService
{
@Autowired
private TokenService tokenService;
@Resource
private AuthenticationManager authenticationManager;
@Autowired
private ISysUserService userService;
@Autowired
@Lazy
private CaptchaService captchaService;
/**
* 登录验证
*
* @param username 用户名
* @param password 密码
* @param code 验证码
* @return 结果
*/
public String login(String username, String password, 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();
}
// 用户验证
Authentication authentication = null;
try
{
// 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
UsernamePasswordAuthenticationToken temp = new UsernamePasswordAuthenticationToken(username, password);
AuthenticationContextHolder.setContext(temp);
authentication = authenticationManager.authenticate(temp);
}
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());
}
}
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
// 生成token
return tokenService.createToken(loginUser);
}
/**
* 记录登录信息
*
* @param userId 用户ID
*/
public void recordLoginInfo(Long userId)
{
SysUser sysUser = new SysUser();
sysUser.setUserId(userId);
sysUser.setLoginIp(IpUtils.getIpAddr(ServletUtils.getRequest()));
sysUser.setLoginDate(DateUtils.getNowDate());
userService.updateUserProfile(sysUser);
}
}
就是说不要无脑复制,最好对比一下看一看思考一下跟以前哪里不一样。
这里跟官方文档有区别的地方是这里:
// 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
UsernamePasswordAuthenticationToken temp = new UsernamePasswordAuthenticationToken(username, password);
AuthenticationContextHolder.setContext(temp);
authentication = authenticationManager.authenticate(temp);
官方旧版:
// 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
authentication = authenticationManager
.authenticate(new UsernamePasswordAuthenticationToken(username, password));
就差一句setContext。可能是某个接口单独起了线程,所以要加一句setContext进行Holder的绑定,不然取不出来东西。
有关这个SecurityContext的相关知识详见:https://www.jianshu.com/p/a951859333a4,供递归式学习使用。
关于空指针报错的排查:SecurityContextHolder.getContext().getAuthentication()报空指针异常,已解决。_qq_48697226的博客-CSDN博客
这个issue在我写这篇文章的11天前就被提出(正好国庆放假前一天),然后在写这篇文章的18小时之前官方文档修复了,而昨天一整天我都被这个莫名其妙的空指针报错折磨着。
issue地址:
更换滑块验证码的BUG · Issue #I5TUBG · 若依/RuoYi - Gitee.com
修了就好,可喜可贺。
CaptchaRedisService.java
在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
{
@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 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";
}
}
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
不删直接运行会报错,如果运行的时候报错org.springframework.beans.factory.BeanDefinitionStoreException
八成是CaptchaController没删,导的那个依赖里边也有一个同名文件,所以创建Bean的时候重名了显示已存在。
将aj-captcha 官方提供的demo中的images包整体引入到resources包下。
文章的最上边提供了下载地址,可自行下载。
至此,后端全部修改完毕。
其实前端改的比较简单,不容易出什么错,就不分步骤了,简单一说:
下载官方提供的前端修改代码,就我一开始说要下载的那个
链接: https://pan.baidu.com/s/13JVC9jm-Dp9PfHdDDylLCQ 提取码: y9jt
在ruoyi-ui目录下的文件不要全部替换到项目中,建议比较一下改动内容。
"crypto-js": "^4.1.1",
<template>
<div class="login">
<el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form">
<h3 class="title">若依后台管理系统</h3>
<el-form-item prop="username">
<el-input
v-model="loginForm.username"
type="text"
auto-complete="off"
placeholder="账号"
>
<svg-icon slot="prefix" icon-class="user" class="el-input__icon input-icon" />
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="loginForm.password"
type="password"
auto-complete="off"
placeholder="密码"
@keyup.enter.native="handleLogin"
>
<svg-icon slot="prefix" icon-class="password" class="el-input__icon input-icon" />
</el-input>
</el-form-item>
<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="medium"
type="primary"
style="width:100%;"
@click.native.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-2022 ruoyi.vip All Rights Reserved.</span>
</div>
</div>
</template>
<script>
import Cookies from "js-cookie";
import { encrypt, decrypt } from '@/utils/jsencrypt'
import Verify from "@/components/Verifition/Verify";
export default {
components: { Verify },
name: "Login",
data() {
return {
loginForm: {
username: "admin",
password: "admin123",
rememberMe: false,
code: "",
},
loginRules: {
username: [
{ required: true, trigger: "blur", message: "请输入您的账号" }
],
password: [
{ required: true, trigger: "blur", message: "请输入您的密码" }
],
},
loading: false,
// 注册开关
register: false,
redirect: undefined
};
},
watch: {
$route: {
handler: function(route) {
this.redirect = route.query && route.query.redirect;
},
immediate: true
}
},
created() {
this.getCookie();
},
methods: {
getCookie() {
const username = Cookies.get("username");
const password = Cookies.get("password");
const rememberMe = Cookies.get('rememberMe')
this.loginForm = {
username: username === undefined ? this.loginForm.username : username,
password: password === undefined ? this.loginForm.password : decrypt(password),
rememberMe: rememberMe === undefined ? false : Boolean(rememberMe)
};
},
capctchaCheckSuccess(params) {
this.loginForm.code = params.captchaVerification;
this.loading = true;
if (this.loginForm.rememberMe) {
Cookies.set("username", this.loginForm.username, { expires: 30 });
Cookies.set("password", encrypt(this.loginForm.password), { expires: 30, });
Cookies.set("rememberMe", this.loginForm.rememberMe, { expires: 30 });
} else {
Cookies.remove("username");
Cookies.remove("password");
Cookies.remove("rememberMe");
}
this.$store.dispatch("Login", this.loginForm).then(() => {
this.$router.push({ path: this.redirect || "/" }).catch(() => {});
}).catch(() => {
this.loading = false;
});
},
handleLogin() {
this.$refs.loginForm.validate((valid) => {
if (valid) {
this.$refs.verify.show();
}
});
}
}
};
</script>
<style rel="stylesheet/scss" lang="scss">
.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: 38px;
input {
height: 38px;
}
}
.input-icon {
height: 39px;
width: 14px;
margin-left: 2px;
}
}
.login-tip {
font-size: 13px;
text-align: center;
color: #bfbfbf;
}
.login-code {
width: 33%;
height: 38px;
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: 38px;
}
</style>
前端更新crypto-js (npm install --save crypto-js)
api文件夹里的东西可以不改,看一眼就知道,api里边没新增东西,只删了一些原来的验证码用的,现在用不到了,不删也能运行。
然后运行应该就能正常用了