1.引入MAVEN依賴
若依官方引入的是1.2.7版本。我選擇了目前最常用的1.3.0版本。
在項(xiàng)目中給的 ruoyi-framework\pom.xml 添加依賴
<!-- anji滑塊驗(yàn)證碼 -->
<dependency>
<groupId>com.anji-plus</groupId>
<artifactId>spring-boot-starter-captcha</artifactId>
<version>1.3.0</version>
</dependency>
2.修改application.yml,加入aj-captcha配置
在ruoyi-admin模塊中的application.yml中添加aj-captcha配置
spring.application.name=captcha-service
server.port=8088
# 滑動(dòng)驗(yàn)證,底圖路徑,不配置將使用默認(rèn)圖片
# 支持全路徑
# 支持項(xiàng)目路徑,以classpath:開(kāi)頭,取resource目錄下路徑,例:classpath:images/jigsaw
aj.captcha.jigsaw=classpath:images/jigsaw
# 滑動(dòng)驗(yàn)證,底圖路徑,不配置將使用默認(rèn)圖片
# 支持全路徑
# 支持項(xiàng)目路徑,以classpath:開(kāi)頭,取resource目錄下路徑,例:classpath:images/pic-click
aj.captcha.pic-click=classpath:images/pic-click
# 對(duì)于分布式部署的應(yīng)用,我們建議應(yīng)用自己實(shí)現(xiàn)CaptchaCacheService,比如用Redis或者memcache,
# 參考CaptchaCacheServiceRedisImpl.java
# 如果應(yīng)用是單點(diǎn)的,也沒(méi)有使用redis,那默認(rèn)使用內(nèi)存。
# 內(nèi)存緩存只適合單節(jié)點(diǎn)部署的應(yīng)用,否則驗(yàn)證碼生產(chǎn)與驗(yàn)證在節(jié)點(diǎn)之間信息不同步,導(dǎo)致失敗。
# !?。?注意啦,如果應(yīng)用有使用spring-boot-starter-data-redis,
# 請(qǐng)打開(kāi)CaptchaCacheServiceRedisImpl.java注釋。
# redis -----> SPI: 在resources目錄新建META-INF.services文件夾(兩層),參考當(dāng)前服務(wù)resources。
# 緩存local/redis...
aj.captcha.cache-type=local
# local緩存的閾值,達(dá)到這個(gè)值,清除緩存
#aj.captcha.cache-number=1000
# local定時(shí)清除過(guò)期緩存(單位秒),設(shè)置為0代表不執(zhí)行
#aj.captcha.timing-clear=180
#spring.redis.host=10.108.11.46
#spring.redis.port=6379
#spring.redis.password=
#spring.redis.database=2
#spring.redis.timeout=6000
# 驗(yàn)證碼類(lèi)型default兩種都實(shí)例化。
aj.captcha.type=default
# 漢字統(tǒng)一使用Unicode,保證程序通過(guò)@value讀取到是中文,可通過(guò)這個(gè)在線轉(zhuǎn)換
# https://tool.chinaz.com/tools/unicode.aspx 中文轉(zhuǎn)Unicode
# 右下角水印文字(我的水印)
aj.captcha.water-mark=我的水印
# 右下角水印字體(不配置時(shí),默認(rèn)使用文泉驛正黑)
# 由于宋體等涉及到版權(quán),我們jar中內(nèi)置了開(kāi)源字體【文泉驛正黑】
# 方式一:直接配置OS層的現(xiàn)有的字體名稱(chēng),比如:宋體
# 方式二:自定義特定字體,請(qǐng)將字體放到工程resources下fonts文件夾,支持ttf\ttc\otf字體
# aj.captcha.water-font=WenQuanZhengHei.ttf
# 點(diǎn)選文字驗(yàn)證碼的文字字體(文泉驛正黑)
# aj.captcha.font-type=WenQuanZhengHei.ttf
# 校驗(yàn)滑動(dòng)拼圖允許誤差偏移量(默認(rèn)5像素)
aj.captcha.slip-offset=5
# aes加密坐標(biāo)開(kāi)啟或者禁用(true|false)
aj.captcha.aes-status=true
# 滑動(dòng)干擾項(xiàng)(0/1/2)
aj.captcha.interference-options=2
#點(diǎn)選字體樣式 默認(rèn)Font.BOLD
aj.captcha.font-style=1
#點(diǎn)選字體字體大小
aj.captcha.font-size=25
#點(diǎn)選文字個(gè)數(shù),存在問(wèn)題,暫不支持修改
#aj.captcha.click-word-count=4
aj.captcha.history-data-clear-enable=false
# 接口請(qǐng)求次數(shù)一分鐘限制是否開(kāi)啟 true|false
aj.captcha.req-frequency-limit-enable=false
# 驗(yàn)證失敗5次,get接口鎖定
aj.captcha.req-get-lock-limit=5
# 驗(yàn)證失敗后,鎖定時(shí)間間隔,s
aj.captcha.req-get-lock-seconds=360
# get接口一分鐘內(nèi)請(qǐng)求數(shù)限制
aj.captcha.req-get-minute-limit=30
# check接口一分鐘內(nèi)請(qǐng)求數(shù)限制
aj.captcha.req-check-minute-limit=30
# verify接口一分鐘內(nèi)請(qǐng)求數(shù)限制(暫用不上,可后臺(tái)直接調(diào)用captchaService)
#aj.captcha.req-verify-minute-limit=30
或者
# 滑塊驗(yàn)證碼
aj:
captcha:
# 緩存類(lèi)型
cache-type: redis
# blockPuzzle 滑塊 clickWord 文字點(diǎn)選 default默認(rèn)兩者都實(shí)例化
type: blockPuzzle
# 右下角顯示字
water-mark: ruoyi.vip
# 校驗(yàn)滑動(dòng)拼圖允許誤差偏移量(默認(rèn)5像素)
slip-offset: 5
# aes加密坐標(biāo)開(kāi)啟或者禁用(true|false)
aes-status: true
# 滑動(dòng)干擾項(xiàng)(0/1/2)
interference-options: 2
這只是基礎(chǔ)配置,可以參考aj-captcha詳細(xì)配置選擇自己想用的參數(shù)。
在SecurityConfig中設(shè)置aj-captcha匿名訪問(wèn)權(quán)限
在 SecurityConfig.java 文件中configure方法下的httpSecurity添加如下語(yǔ)句:
.antMatchers("/captcha/get", "/captcha/check").anonymous()
修改后臺(tái)實(shí)現(xiàn)代碼
大家在修改代碼時(shí),切記不要按照文檔官方文檔直接覆蓋,建議使用文本比較工具將代碼進(jìn)行比對(duì)后只更新相關(guān)內(nèi)容,這也是開(kāi)發(fā)人員的良好習(xí)慣?。。?!
修改SysLoginService.java
抽離改動(dòng)點(diǎn)比較麻煩,大家自行去比對(duì)吧。主要改動(dòng)login方法。
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.Constants;
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.CaptchaException;
import com.ruoyi.common.exception.user.CaptchaExpireException;
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.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.ISysConfigService;
import com.ruoyi.system.service.ISysUserService;
/**
* 登錄校驗(yàn)方法
*
* @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;
/**
* 登錄驗(yàn)證
*
* @param username 用戶名
* @param password 密碼
* @param code 驗(yàn)證碼
* @param uuid 唯一標(biāo)識(shí)
* @return 結(jié)果
*/
public String login(String username, String password, String code, String uuid)
{
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();
}
/*boolean captchaOnOff = configService.selectCaptchaOnOff();
// 驗(yàn)證碼開(kāi)關(guān)
if (captchaOnOff)
{
validateCaptcha(username, code, uuid);
}*/
// 用戶驗(yàn)證
Authentication authentication = null;
try
{
// 該方法會(huì)去調(diào)用UserDetailsServiceImpl.loadUserByUsername
authentication = authenticationManager
.authenticate(new UsernamePasswordAuthenticationToken(username, password));
}
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();
recordLoginInfo(loginUser.getUserId());
// 生成token
return tokenService.createToken(loginUser);
}
/**
* 校驗(yàn)驗(yàn)證碼
*
* @param username 用戶名
* @param code 驗(yàn)證碼
* @param uuid 唯一標(biāo)識(shí)
* @return 結(jié)果
*/
/* public void validateCaptcha(String username, String code, String uuid)
{
String verifyKey = Constants.CAPTCHA_CODE_KEY + StringUtils.nvl(uuid, "");
String captcha = redisCache.getCacheObject(verifyKey);
redisCache.deleteObject(verifyKey);
if (captcha == null)
{
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire")));
throw new CaptchaExpireException();
}
if (!code.equalsIgnoreCase(captcha))
{
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")));
throw new CaptchaException();
}
}
*/
/**
* 記錄登錄信息
*
* @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);
}
}
新增 CaptchaRedisService.java
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驗(yàn)證碼緩存實(shí)現(xiàn)類(lèi)
*
* @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";
}
}
6.資源導(dǎo)入
將aj-captcha 官方提供的demo中的images包整體引入到resources包下。
文章的最上邊提供了下載地址,可自行下載。
至此,后端全部修改完畢。
7.修改前端
使用beyondcompare比較一下改動(dòng)內(nèi)容。
package.json
dependencies 增加
"crypto-js": "^4.1.1",
\src\views\login.vue
刪除原來(lái)的驗(yàn)證碼部分,增加滑塊驗(yàn)證碼相關(guān)代碼
<template>
<div class="login">
<el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form">
<h3 class="title">若依后臺(tái)管理系統(tǒng)</h3>
<el-form-item prop="username">
<el-input
v-model="loginForm.username"
type="text"
auto-complete="off"
placeholder="賬號(hào)"
>
<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'">立即注冊(cè)</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: "請(qǐng)輸入您的賬號(hào)" }
],
password: [
{ required: true, trigger: "blur", message: "請(qǐng)輸入您的密碼" }
],
},
loading: false,
// 注冊(cè)開(kāi)關(guān)
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>
其他資源
將 “集成aj-captcha實(shí)現(xiàn)滑塊驗(yàn)證碼\ruoyi-ui\src\“ 目錄下assets、components兩個(gè)目錄直接復(fù)制到項(xiàng)目中。
這就夠了。api目錄下文件用不到。
8.啟動(dòng)
1.建議先啟動(dòng)后端,再啟動(dòng)前端。
2.前后端都增加了依賴,后端需要更新maven依賴,前端更新crypto-js (npm install --save crypto-js)
最終效果如下: