当前位置: 首页 > 工具软件 > go-captcha > 使用案例 >

若依学习笔记11——集成aj-captcha实现滑块验证码

谢烨烨
2023-12-01

        由于最近在集成这玩意儿的时候实在是报错报麻了,所以写一下笔记记录一下,如果有人跟我一样,集成这个东西的时候各种报错,可以往下翻一翻找一下有没有你遇到的情况。

首先准备东西:

插件集成 | RuoYi

这里边的集成aj-captcha实现滑块验证码部分里第五步有一个百度网盘链接,先下载以后有用。

aj-captcha源码下载:AJ-Captcha: 行为验证码(滑动拼图、点选文字),前后端(java)交互,包含vue/h5/Android/IOS/flutter/uni-app/react/php/go/微信小程序的源码和实现

步骤参考:05-若依使用AJ-captcha验证_刀不封的博客-CSDN博客

但是我觉得这文章还缺点东西,所以我写这篇再给他封装一层。

一、后端整合步骤

1.添加依赖

找到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

2.修改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的文档:

​​​​​​​AJ-Captcha: 行为验证码(滑动拼图、点选文字),前后端(java)交互,包含vue/h5/Android/IOS/flutter/uni-app/react/php/go/微信小程序的源码和实现

3.添加实现类指向设置

找到ruoyi-admin\src\main\resources\META-INF\services下创建com.anji.captcha.service.CaptchaCacheService文件(不需要后缀名)同时设置文件内容为:

com.ruoyi.framework.web.service.CaptchaRedisService

这里的地址要跟你下一步写CaptchaRedisService的路径一致,不然扫不到包。

4.设置aj-captcha匿名访问权限

在 ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java 文件中configure方法下的httpSecurity添加如下语句:

.antMatchers("/login", "/captcha/get", "/captcha/check").permitAll()

这步保证这仨方法能够不登陆就访问,不然你登录的时候要求你有权限才能登录,就尬住了。

“我要登录。”
"对不起先生,您没有权限,不能登录。"
“我怎么才能有权限?”
"您需要先登录才有权限。"
“我要登录。”
"对不起先生,您没有权限,不能登录。"

我们要的是20岁拥有30年工作经验的人才.java

5.修改代码

        若依官方文档让修改 ruoyi-admin\com\ruoyi\web\controller\system\SysLoginController.java,但是其实改不改都行,区别就是改之前四个参数,改之后三个参数,多的那个参数uuid在换成滑动验证码之后其实用不到,所以传不传都行。

        但是得保证修改之后,SysLoginController里的调用和SysLoginService里的定义是一致的,不管三个还是四个参数,得对应起来,不然就会报错。

        我参考那个文章里有一句提醒:

大家在修改代码时,切记不要按照文档官方文档直接覆盖,建议使用文本比较工具将代码进行比对后只更新相关内容,这也是开发人员的良好习惯!!!!

        这个其实很重要,但是翻了翻一些文档对比工具,好多都要钱,而且如果是在公司电脑上开发,有可能还不让你下载,所以我用的这个http://www.jsons.cn/txtdiff/

        免费的,虽然界面可能low了点,但是挺好使。

        并且记得边修改边思考,因为自身水平有限,所以我经常性进行递归式学习,看到这句代码,不知道干嘛的,点进去看一看,百度搜一搜,然后看的过程中又发现了不懂的,又往里进一层,继续点进去看继续搜。然后一层一层越进越深。但是学的东西也越来越多。

        盗梦空间了属于是。

        主要是这是个学习的过程,不是只为了使用而去使用。学习嘛,不寒碜。

(1)修改SysLoginService.java

修改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

        修了就好,可喜可贺。

(2)新增 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";
    }
}

(3)删除不要的类        

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的时候重名了显示已存在。

6、导入资源

将aj-captcha 官方提供的demo中的images包整体引入到resources包下。
文章的最上边提供了下载地址,可自行下载。
至此,后端全部修改完毕。

二、前端修改

        其实前端改的比较简单,不容易出什么错,就不分步骤了,简单一说:

        下载官方提供的前端修改代码,就我一开始说要下载的那个
        链接: https://pan.baidu.com/s/13JVC9jm-Dp9PfHdDDylLCQ 提取码: y9jt
        在ruoyi-ui目录下的文件不要全部替换到项目中,建议比较一下改动内容。

  • ruoyi-ui\package.json
    dependencies 增加
     "crypto-js": "^4.1.1",
  • ruoyi-ui\src\views\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>
          <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>
    
    

  • 其他资源
    将 “集成aj-captcha实现滑块验证码\ruoyi-ui\src\“ 目录下assets、components两个目录直接复制到项目中。

        前端更新crypto-js (npm install --save crypto-js)

        api文件夹里的东西可以不改,看一眼就知道,api里边没新增东西,只删了一些原来的验证码用的,现在用不到了,不删也能运行。

        然后运行应该就能正常用了

 类似资料: