后端代码
官方使用demo文档:http://doc.captcha.tianai.cloud/#%E4%BD%BF%E7%94%A8demo
我的完整代码:https://gitee.com/Min-Duck/RuoYi-Vue.git
- 主pom.xml 加入依赖
<!-- 滑块验证码 -->
<dependency>
<groupId>cloud.tianai.captcha</groupId>
<artifactId>tianai-captcha-springboot-starter</artifactId>
<version>1.5.0</version>
</dependency>
- ruoyi-framework pom.xml 加入依赖
<!-- 滑块验证码 -->
<dependency>
<groupId>cloud.tianai.captcha</groupId>
<artifactId>tianai-captcha-springboot-starter</artifactId>
</dependency>
-
在ruoyi-admin的resources下加入验证码需要的图片
-
ruoyi-framework 加入验证码配置代码
package com.ruoyi.framework.config;
import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
import cloud.tianai.captcha.resource.ResourceStore;
import cloud.tianai.captcha.resource.common.model.dto.Resource;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
@Component
public class CaptchaResourceConfiguration {
@javax.annotation.Resource
private ResourceStore resourceStore;
@PostConstruct
public void init() {
// 2. 添加自定义背景图片
resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bg/1.png", "default"));
resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bg/2.png", "default"));
resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bg/3.png", "default"));
resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bg/4.png", "default"));
resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bg/5.png", "default"));
resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bg/6.png", "default"));
resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bg/7.png", "default"));
resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bg/8.png", "default"));
resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bg/9.png", "default"));
resourceStore.addResource(CaptchaTypeConstant.ROTATE, new Resource("classpath", "bg/10.png", "default"));
resourceStore.addResource(CaptchaTypeConstant.CONCAT, new Resource("classpath", "bg/11.png", "default"));
resourceStore.addResource(CaptchaTypeConstant.WORD_IMAGE_CLICK, new Resource("classpath", "bg/12.png", "default"));
}
}
- 在ruoyi-admin加入CaptchaController
package com.ruoyi.web.controller.system;
import cloud.tianai.captcha.application.ImageCaptchaApplication;
import cloud.tianai.captcha.application.vo.CaptchaResponse;
import cloud.tianai.captcha.application.vo.ImageCaptchaVO;
import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
import cloud.tianai.captcha.common.response.ApiResponse;
import cloud.tianai.captcha.spring.plugins.secondary.SecondaryVerificationApplication;
import cloud.tianai.captcha.validator.common.model.dto.ImageCaptchaTrack;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Collections;
import java.util.concurrent.ThreadLocalRandom;
@RestController
@RequestMapping("/captcha")
public class CaptchaController {
@Autowired
private ImageCaptchaApplication imageCaptchaApplication;
@PostMapping("/gen")
@ResponseBody
public CaptchaResponse<ImageCaptchaVO> genCaptcha(@RequestParam(value = "type", required = false) String type) {
if (StringUtils.isBlank(type)) {
type = CaptchaTypeConstant.SLIDER;
}
if ("RANDOM".equals(type)) {
int i = ThreadLocalRandom.current().nextInt(0, 4);
if (i == 0) {
type = CaptchaTypeConstant.SLIDER;
} else if (i == 1) {
type = CaptchaTypeConstant.CONCAT;
} else if (i == 2) {
type = CaptchaTypeConstant.ROTATE;
} else {
type = CaptchaTypeConstant.WORD_IMAGE_CLICK;
}
}
return imageCaptchaApplication.generateCaptcha(type);
}
@PostMapping("/check")
@ResponseBody
public ApiResponse<?> checkCaptcha(@RequestBody String data) {
// TODO 不知道可不可以使用它的实体类,因为我的是spring boot 3.3.5的时间转换有问题才这样写!!!
JSONObject jsonObject = JSON.parseObject(data);
String id = jsonObject.getString("id");
ImageCaptchaTrack imageCaptchaTrack = JSON.parseObject(jsonObject.getString("data"), ImageCaptchaTrack.class);
ApiResponse<?> response = imageCaptchaApplication.matching(id, imageCaptchaTrack);
if (response.isSuccess()) {
return ApiResponse.ofSuccess(Collections.singletonMap("id", id));
}
return response;
}
/**
* 二次验证,一般用于机器内部调用,这里为了方便测试
*
* @param id id
* @return boolean
*/
@GetMapping("/check2")
@ResponseBody
public boolean check2Captcha(@RequestParam("id") String id) {
// 如果开启了二次验证
if (imageCaptchaApplication instanceof SecondaryVerificationApplication) {
return ((SecondaryVerificationApplication) imageCaptchaApplication).secondaryVerification(id);
}
return false;
}
}
- 在ruoyi-admin的resources下的application.yml加入验证码配置信息
# 客户端验证码
captcha:
cache:
enabled: true
cache-size: 30
# 二次验证
secondary:
enabled: false
# 是否初始化默认资源
init-default-resource: true
- 在SecurityConfig加入放行接口
@Bean
protected SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception
{
return httpSecurity
// CSRF禁用,因为不使用session
.csrf(csrf -> csrf.disable())
// 禁用HTTP响应标头
.headers((headersCustomizer) -> {
headersCustomizer.cacheControl(cache -> cache.disable()).frameOptions(options -> options.sameOrigin());
})
// 认证失败处理类
.exceptionHandling(exception -> exception.authenticationEntryPoint(unauthorizedHandler))
// 基于token,所以不需要session
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// 注解标记允许匿名访问的url
.authorizeHttpRequests((requests) -> {
permitAllUrl.getUrls().forEach(url -> requests.antMatchers(url).permitAll());
// 对于登录login 注册register 验证码captchaImage 允许匿名访问
requests.antMatchers("/login", "/register", "/captchaImage").permitAll()
// 静态资源,可匿名访问
.antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll()
.antMatchers("/swagger-ui.html", "/swagger-resources/**"
, "/webjars/**", "/*/api-docs", "/druid/**"
, "/captcha/gen", "/captcha/check", "/captcha/check2"
).permitAll()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();
})
// 添加Logout filter
.logout(logout -> logout.logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler))
// 添加JWT filter
.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
// 添加CORS filter
.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class)
.addFilterBefore(corsFilter, LogoutFilter.class)
.build();
}
- 删除ruoyi-admin的common下的CaptchaController !!!!
前端代码
- 在ruoyi-ui的public下加入js和tac
tac 下载地址
load.min.js 下载地址
- 在public的index.html里面引入load.min.js
- 在login.vue加入新的验证码标签
<div id="captcha-box"></div>
- 引入自己的logo
import logo from '@/assets/logo/logo.png'
- 完整的login.vue
<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>
<!-- 注释旧验证码-->
<!-- <el-form-item prop="code" v-if="captchaEnabled">-->
<!-- <el-input-->
<!-- v-model="loginForm.code"-->
<!-- auto-complete="off"-->
<!-- placeholder="验证码"-->
<!-- style="width: 63%"-->
<!-- @keyup.enter.native="handleLogin"-->
<!-- >-->
<!-- <svg-icon slot="prefix" icon-class="validCode" class="el-input__icon input-icon" />-->
<!-- </el-input>-->
<!-- <div class="login-code">-->
<!-- <img :src="codeUrl" @click="getCode" class="login-code-img"/>-->
<!-- </div>-->
<!-- </el-form-item>-->
<!--新的滑块验证吗-->
<div id="captcha-box"></div>
<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-2024 ruoyi.vip All Rights Reserved.</span>
</div>
</div>
</template>
<script>
import Cookies from "js-cookie";
import {encrypt, decrypt} from '@/utils/jsencrypt'
import logo from '@/assets/logo/logo.png'
export default {
name: "Login",
data() {
return {
codeUrl: "",
loginForm: {
username: "admin",
password: "admin123",
rememberMe: false,
code: "",
uuid: ""
},
loginRules: {
username: [
{required: true, trigger: "blur", message: "请输入您的账号"}
],
password: [
{required: true, trigger: "blur", message: "请输入您的密码"}
],
code: [{required: true, trigger: "change", message: "请输入验证码"}]
},
loading: false,
// 验证码开关
captchaEnabled: true,
// 注册开关
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)
};
},
checkSuccess() {
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) {
// config 对象为TAC验证码的一些配置和验证的回调
const config = {
// 生成接口 (必选项,必须配置, 要符合tianai-captcha默认验证码生成接口规范)
requestCaptchaDataUrl: process.env.VUE_APP_BASE_API+"/captcha/gen",
// 验证接口 (必选项,必须配置, 要符合tianai-captcha默认验证码校验接口规范)
validCaptchaUrl: process.env.VUE_APP_BASE_API+"/captcha/check",
// 验证码绑定的div块 (必选项,必须配置)
bindEl: "#captcha-box",
// 验证成功回调函数(必选项,必须配置)
validSuccess: (res, c, tac) => {
// 销毁验证码服务
tac.destroyWindow();
this.checkSuccess()
},
// 验证失败的回调函数(可忽略,如果不自定义 validFail 方法时,会使用默认的)
validFail: (res, c, tac) => {
// 验证失败后重新拉取验证码
tac.reloadCaptcha();
},
// 刷新按钮回调事件
btnRefreshFun: (el, tac) => {
tac.reloadCaptcha();
},
// 关闭按钮回调事件
btnCloseFun: (el, tac) => {
tac.destroyWindow();
}
}
let style = {
logoUrl: logo,
}
// 参数1 为 tac文件是目录地址, 目录里包含 tac的js和css等文件
// 参数2 为 tac验证码相关配置
// 参数3 为 tac窗口一些样式配置
window.initTAC("./tac", config, style).then(tac => {
tac.init(); // 调用init则显示验证码
}).catch(e => {
console.log("初始化tac失败", e);
})
}
});
},
}
};
</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>
- 大功告成
- 登录的代码和旧验证码的代码就自己删除了,我就不赘述了