ruoyi-vue集成tianai-captcha验证码

后端代码

官方使用demo文档:http://doc.captcha.tianai.cloud/#使用demo

我的完整代码:https://gitee.com/Min-Duck/RuoYi-Vue.git

  1. 主pom.xml 加入依赖
xml 复制代码
  <!-- 滑块验证码  -->
  <dependency>
      <groupId>cloud.tianai.captcha</groupId>
      <artifactId>tianai-captcha-springboot-starter</artifactId>
      <version>1.5.0</version>
  </dependency>
  1. ruoyi-framework pom.xml 加入依赖
xml 复制代码
 <!-- 滑块验证码  -->
   <dependency>
       <groupId>cloud.tianai.captcha</groupId>
       <artifactId>tianai-captcha-springboot-starter</artifactId>
   </dependency>
  1. 在ruoyi-admin的resources下加入验证码需要的图片

  2. ruoyi-framework 加入验证码配置代码

java 复制代码
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"));
    }
}
  1. 在ruoyi-admin加入CaptchaController
java 复制代码
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;
    }
}
  1. 在ruoyi-admin的resources下的application.yml加入验证码配置信息
yaml 复制代码
# 客户端验证码
captcha:
  cache:
    enabled: true
    cache-size: 30
  # 二次验证
  secondary:
    enabled: false
  # 是否初始化默认资源
  init-default-resource: true
  1. 在SecurityConfig加入放行接口
java 复制代码
@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();
    }
  1. 删除ruoyi-admin的common下的CaptchaController !!!!

前端代码

  1. 在ruoyi-ui的public下加入js和tac
    tac 下载地址
    load.min.js 下载地址
  2. 在public的index.html里面引入load.min.js
  3. 在login.vue加入新的验证码标签
html 复制代码
<div id="captcha-box"></div>
  1. 引入自己的logo
javascript 复制代码
import logo from '@/assets/logo/logo.png'
  1. 完整的login.vue
javascript 复制代码
<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>
  1. 大功告成
  2. 登录的代码和旧验证码的代码就自己删除了,我就不赘述了
相关推荐
路在脚下@10 分钟前
spring boot的配置文件属性注入到类的静态属性
java·spring boot·sql
森屿Serien13 分钟前
Spring Boot常用注解
java·spring boot·后端
轻口味39 分钟前
命名空间与模块化概述
开发语言·前端·javascript
前端小小王1 小时前
React Hooks
前端·javascript·react.js
苹果醋31 小时前
React源码02 - 基础知识 React API 一览
java·运维·spring boot·mysql·nginx
迷途小码农零零发1 小时前
react中使用ResizeObserver来观察元素的size变化
前端·javascript·react.js
Hello.Reader2 小时前
深入解析 Apache APISIX
java·apache
娃哈哈哈哈呀2 小时前
vue中的css深度选择器v-deep 配合!important
前端·css·vue.js
菠萝蚊鸭2 小时前
Dhatim FastExcel 读写 Excel 文件
java·excel·fastexcel