Ruoyi ruoyi-vue3 integrates aj-captcha to implement slider and text click verification codes


0. Preface

In fact, Ruoyi's official documentation includes a part that integrates aj-captcha to implement the slider verification code, but the front-end sample code always provided is the Vue2 version, and the back-end part has not been updated. Another example is that the official document does not implement the verification code switch function after integrating aj-captcha.

Then I happened to be making something using Zoey's Vue3 version recently, so I just wanted to record it.

0.1 Description

  1. This article is written based on the official document as a template, so some text from the official document will be interspersed in the middle.

  2. The screenshots and codes involved in the article have been modified usingRuoyi Framework Package Name Modifier, so the package name , the module name prefix will be different from the original version, but only in the package name and module name. Please pay attention to screening.

  3. This article is based on back-end RuoYi-Vue 3.8.7 and front-end RuoYi-Vue3 3.8.7

  4. The official document does not implement the verification code switch function after integration. This article will implement it.

Taking the AJ-Captcha text click verification code as an example, the integration does not require manual keyboard input, which greatly optimizes the problem of poor user experience with traditional verification codes. Currently, two types of verification codes are provided to the outside world, including sliding puzzles and text clicks.

1. Backend part

1.1 Add dependencies

Add the following dependencies to in the ruoyi-framework module:pom.xml

<!-- 滑块验证码  -->
<dependency>
	<groupId>com.github.anji-plus</groupId>
	<artifactId>captcha-spring-boot-starter</artifactId>
	<version>1.2.7</version>
</dependency>

Delete the original kaptcha verification code dependency:

<!-- 验证码 -->
<dependency>
	<groupId>pro.fessional</groupId>
	<artifactId>kaptcha</artifactId>
	<exclusions>
		<exclusion>
		<artifactId>servlet-api</artifactId>
		<groupId>javax.servlet</groupId>
		</exclusion>
	</exclusions>
</dependency>

The end pom.xml Picture:

Insert image description here

1.2. Modify application.yml

Modify application.yml and add aj-captcha related configuration:
(My project uses text selection, if you need to use a slider, type Set to blockPuzzle (that’s it)

# 滑块验证码
aj:
   captcha:
      # 缓存类型
      cache-type: redis
      # blockPuzzle 滑块 clickWord 文字点选  default默认两者都实例化
      type: clickWord
      # 右下角显示字
      water-mark: B站、抖音同名搜索七维大脑
      # 校验滑动拼图允许误差偏移量(默认5像素)
      slip-offset: 5
      # aes加密坐标开启或者禁用(true|false)
      aes-status: true
      # 滑动干扰项(0/1/2)
      interference-options: 2

1.3. Added CaptchaRedisService class

Create the class under the ruoyi-framework module and com.ruoyi.framework.web.service package with the following content: CaptchaRedisService.java

(Please be careful to modify the package path to the real path of your project after copying and pasting)

package xyz.ytxy.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 Boolean.TRUE.equals(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";
    }
}

Insert image description here

1.4. Add necessary files

  1. Under the ruoyi-admin module, find the resources directory
  2. found in resources directory META-INF directory
  3. Create a new folder in the META-INF directoryservices
  4. Create a new file in the services folder (note that it is a file)com.anji.captcha.service.CaptchaCacheService
  5. Enter in the com.anji.captcha.service.CaptchaCacheService file (that is, the real path of the CaptchaRedisService class just created)xxx.xxx.framework.web.service.CaptchaRedisService
    Insert image description here

1.5. Remove unnecessary classes

  1. ruoyi-adminUnder modulecom.ruoyi.web.controller.common.CaptchaController.java
  2. ruoyi-frameworkUnder modulecom.ruoyi.framework.config.CaptchaConfig.java
  3. ruoyi-frameworkUnder modulecom.ruoyi.framework.config.KaptchaTextCreator.java

1.6. Modify login method

Repair ruoyi-admin Model com.ruoyi.web.controller.system.SysLoginController.java Type login Method:

/**
     * 登录方法
     *
     * @param loginBody 登录信息
     * @return 结果
     */
    @PostMapping("/login")
    public AjaxResult login(@RequestBody LoginBody loginBody)
    {
    
    
        AjaxResult ajax = AjaxResult.success();
        // 生成令牌
        String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode());
        ajax.put(Constants.TOKEN, token);
        return ajax;
    }

The modified step of generating tokens has fewer loginBody.getUuid() parameters than the original version.

Revised ruoyi-frameworkModifiedcom.ruoyi.framework.web.service.SysLoginService.javaClass:

package xyz.ytxy.framework.web.service;

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 xyz.ytxy.common.constant.CacheConstants;
import xyz.ytxy.common.constant.Constants;
import xyz.ytxy.common.constant.UserConstants;
import xyz.ytxy.common.core.domain.entity.SysUser;
import xyz.ytxy.common.core.domain.model.LoginUser;
import xyz.ytxy.common.core.redis.RedisCache;
import xyz.ytxy.common.exception.ServiceException;
import xyz.ytxy.common.exception.user.BlackListException;
import xyz.ytxy.common.exception.user.CaptchaException;
import xyz.ytxy.common.exception.user.CaptchaExpireException;
import xyz.ytxy.common.exception.user.UserNotExistsException;
import xyz.ytxy.common.exception.user.UserPasswordNotMatchException;
import xyz.ytxy.common.utils.DateUtils;
import xyz.ytxy.common.utils.MessageUtils;
import xyz.ytxy.common.utils.StringUtils;
import xyz.ytxy.common.utils.ip.IpUtils;
import xyz.ytxy.framework.manager.AsyncManager;
import xyz.ytxy.framework.manager.factory.AsyncFactory;
import xyz.ytxy.framework.security.context.AuthenticationContextHolder;
import xyz.ytxy.system.service.ISysConfigService;
import xyz.ytxy.system.service.ISysUserService;

/**
 * 登录校验方法
 *
 * @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;

    /**
     * 登录验证
     *
     * @param username 用户名
     * @param password 密码
     * @param code 验证码
     * @return 结果
     */
    public String login(String username, String password, String code)
    {
    
    
        // 验证码校验
        validateCaptcha(username, code);
        // 登录前置校验
        loginPreCheck(username, password);
        // 用户验证
        Authentication authentication = null;
        try
        {
    
    
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
            AuthenticationContextHolder.setContext(authenticationToken);
            // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
            authentication = authenticationManager.authenticate(authenticationToken);
        }
        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());
            }
        }
        finally
        {
    
    
            AuthenticationContextHolder.clearContext();
        }
        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);
    }

    /**
     * 校验验证码
     *
     * @param username 用户名
     * @param code 验证码
     * @return 结果
     */
    public void validateCaptcha(String username, String code)
    {
    
    
        boolean captchaEnabled = configService.selectCaptchaEnabled();
        if (captchaEnabled)
        {
    
    
            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();
            }
        }
    }

    /**
     * 登录前置校验
     * @param username 用户名
     * @param password 用户密码
     */
    public void loginPreCheck(String username, String password)
    {
    
    
        // 用户名或密码为空 错误
        if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password))
        {
    
    
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("not.null")));
            throw new UserNotExistsException();
        }
        // 密码如果不在指定范围内 错误
        if (password.length() < UserConstants.PASSWORD_MIN_LENGTH
                || password.length() > UserConstants.PASSWORD_MAX_LENGTH)
        {
    
    
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
            throw new UserPasswordNotMatchException();
        }
        // 用户名不在指定范围内 错误
        if (username.length() < UserConstants.USERNAME_MIN_LENGTH
                || username.length() > UserConstants.USERNAME_MAX_LENGTH)
        {
    
    
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
            throw new UserPasswordNotMatchException();
        }
        // IP黑名单校验
        String blackStr = configService.selectConfigByKey("sys.login.blackIPList");
        if (IpUtils.isMatchedIp(blackStr, IpUtils.getIpAddr()))
        {
    
    
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("login.blocked")));
            throw new BlackListException();
        }
    }

    /**
     * 记录登录信息
     *
     * @param userId 用户ID
     */
    public void recordLoginInfo(Long userId)
    {
    
    
        SysUser sysUser = new SysUser();
        sysUser.setUserId(userId);
        sysUser.setLoginIp(IpUtils.getIpAddr());
        sysUser.setLoginDate(DateUtils.getNowDate());
        userService.updateUserProfile(sysUser);
    }
}

  • login method has fewer parameters than the original version uuid
  • validateCaptchaThe method has fewer parameters than the original version uuid, and the method content is changed to the aj-captcha verification method
  • No other content has changed

If you directly replace the code in the official documentation here, some new functions will be missing. So just replace the code I provided here. (Note that after replacement, change the package name to your actual package name)

1.7. Added verification code switch acquisition interface

existent ruoyi-adminunder the model com.ruoyi.web.controller.commonbao new growth CaptchaEnabledController.java :

(Note to change the package name to your actual package name)

package xyz.ytxy.web.controller.common;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import xyz.ytxy.common.core.domain.AjaxResult;
import xyz.ytxy.system.service.ISysConfigService;

/**
 * 验证码操作处理
 *
 * @author B站、抖音搜索:七维大脑    点个关注呗
 */
@RestController
public class CaptchaEnabledController {
    
    

    @Autowired
    private ISysConfigService configService;

    /**
     * 获取验证码开关
     */
    @GetMapping("/captchaEnabled")
    public AjaxResult captchaEnabled() {
    
    
        AjaxResult ajax = AjaxResult.success();
        boolean captchaEnabled = configService.selectCaptchaEnabled();
        ajax.put("captchaEnabled", captchaEnabled);
        return ajax;
    }
}

1.8. Allow anonymous access

Find the class under the package under the ruoyi-framework module and modify the following content: com.ruoyi.framework.configSecurityConfig.java

Original version:

// 对于登录login 注册register 验证码captchaImage 允许匿名访问
.antMatchers("/login", "/register", "/captchaImage").permitAll()

change into:

// 对于登录login 注册register 滑块验证码/captcha/get /captcha/check 获取验证码开关 /captchaEnabled 允许匿名访问
.antMatchers("/login", "/register", "/captcha/get", "/captcha/check", "/captchaEnabled").permitAll()

2. Front-end part (Vue3)

2.1. Add new dependency crypto-js

existing package.json "dependencies" 中新增 "crypto-js": "4.1.1"

Insert image description here

Re-add install, for example, I use pnpm, execute directly: pnpm install --registry=https://registry.npmmirror.com

2.2. Added Verifition component

I put this part of the code on Alibaba Cloud Disk:https://www.alipan.com/s/4hEbavUC4Np

Download and paste into the src/components directory:

Insert image description here

2.3. Modify login.js

import request from '@/utils/request'

// 登录方法
export function login(username, password, code) {
    
    
  const data = {
    
    
    username,
    password,
    code
  }
  return request({
    
    
    url: '/login',
    headers: {
    
    
      isToken: false,
      repeatSubmit: false
    },
    method: 'post',
    data: data
  })
}

// 注册方法
export function register(data) {
    
    
  return request({
    
    
    url: '/register',
    headers: {
    
    
      isToken: false
    },
    method: 'post',
    data: data
  })
}

// 获取用户详细信息
export function getInfo() {
    
    
  return request({
    
    
    url: '/getInfo',
    method: 'get'
  })
}

// 退出方法
export function logout() {
    
    
  return request({
    
    
    url: '/logout',
    method: 'post'
  })
}

// 获取验证码开关
export function isCaptchaEnabled() {
    
    
    return request({
    
    
        url: '/captchaEnabled',
        method: 'get'
    })
}
  • Modified the login function and removed the uuid parameter
  • Removed the get verification code functiongetCodeImg
  • Added a new function to get the verification code switchisCaptchaEnabled

2.4. Modify user.js

Exclusion uuid Reference number:

// 登录
      login(userInfo) {
    
    
        const username = userInfo.username.trim()
        const password = userInfo.password
        const code = userInfo.code
        return new Promise((resolve, reject) => {
    
    
          login(username, password, code).then(res => {
    
    
            setToken(res.token)
            this.token = res.token
            resolve()
          }).catch(error => {
    
    
            reject(error)
          })
        })
      },

Insert image description here

2.5. Modify login.vue

There are a lot of modifications, it is recommended to directly replace and then modify:

<template>
  <div class="login">
    <el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form">
      <h3 class="title">若依后台管理系统</h3>
      <el-form-item prop="username">
        <el-input
            v-model="loginForm.username"
            type="text"
            size="large"
            auto-complete="off"
            placeholder="账号"
        >
          <template #prefix>
            <svg-icon icon-class="user" class="el-input__icon input-icon"/>
          </template>
        </el-input>
      </el-form-item>
      <el-form-item prop="password">
        <el-input
            v-model="loginForm.password"
            type="password"
            size="large"
            auto-complete="off"
            placeholder="密码"
            @keyup.enter="handleLogin"
        >
          <template #prefix>
            <svg-icon icon-class="password" class="el-input__icon input-icon"/>
          </template>
        </el-input>
      </el-form-item>

      <Verify
          @success="capctchaCheckSuccess"
          :mode="'pop'"
          :captchaType="'clickWord'"
          :imgSize="{ width: '330px', height: '155px' }"
          ref="verify"
          v-if="captchaEnabled"
      ></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="large"
            type="primary"
            style="width:100%;"
            @click.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-2023 ruoyi.vip All Rights Reserved.</span>
    </div>
  </div>
</template>

<script setup>
import Cookies from "js-cookie";
import {
      
      encrypt, decrypt} from "@/utils/jsencrypt";
import useUserStore from '@/store/modules/user'
import Verify from "@/components/Verifition/Verify";
import {
      
      isCaptchaEnabled} from "@/api/login";

const userStore = useUserStore()
const route = useRoute();
const router = useRouter();
const {
      
      proxy} = getCurrentInstance();

const loginForm = ref({
      
      
  username: "admin",
  password: "admin123",
  rememberMe: false,
  code: ""
});

const loginRules = {
      
      
  username: [{
      
      required: true, trigger: "blur", message: "请输入您的账号"}],
  password: [{
      
      required: true, trigger: "blur", message: "请输入您的密码"}]
};

const loading = ref(false);
// 验证码开关
const captchaEnabled = ref(true);
// 注册开关
const register = ref(false);
const redirect = ref(undefined);

watch(route, (newRoute) => {
      
      
  redirect.value = newRoute.query && newRoute.query.redirect;
}, {
      
      immediate: true});

function userRouteLogin() {
      
      
  // 调用action的登录方法
  userStore.login(loginForm.value).then(() => {
      
      
    const query = route.query;
    const otherQueryParams = Object.keys(query).reduce((acc, cur) => {
      
      
      if (cur !== "redirect") {
      
      
        acc[cur] = query[cur];
      }
      return acc;
    }, {
      
      });
    router.push({
      
      path: redirect.value || "/", query: otherQueryParams});
  }).catch(() => {
      
      
    loading.value = false;
  });
}

function handleLogin() {
      
      
  proxy.$refs.loginRef.validate(valid => {
      
      
    if (valid && captchaEnabled.value) {
      
      
      proxy.$refs.verify.show();
    } else if (valid && !captchaEnabled.value) {
      
      
      userRouteLogin();
    }
  });
}

function getCookie() {
      
      
  const username = Cookies.get("username");
  const password = Cookies.get("password");
  const rememberMe = Cookies.get("rememberMe");
  loginForm.value = {
      
      
    username: username === undefined ? loginForm.value.username : username,
    password: password === undefined ? loginForm.value.password : decrypt(password),
    rememberMe: rememberMe === undefined ? false : Boolean(rememberMe)
  };
}

function capctchaCheckSuccess(params) {
      
      
  loginForm.value.code = params.captchaVerification;
  loading.value = true;
  // 勾选了需要记住密码设置在 cookie 中设置记住用户名和密码
  if (loginForm.value.rememberMe) {
      
      
    Cookies.set("username", loginForm.value.username, {
      
      expires: 30});
    Cookies.set("password", encrypt(loginForm.value.password), {
      
      expires: 30,});
    Cookies.set("rememberMe", loginForm.value.rememberMe, {
      
      expires: 30});
  } else {
      
      
    // 否则移除
    Cookies.remove("username");
    Cookies.remove("password");
    Cookies.remove("rememberMe");
  }
  userRouteLogin();
}

// 获取验证码开关
function getCaptchaEnabled() {
      
      
  isCaptchaEnabled().then(res => {
      
      
    captchaEnabled.value = res.captchaEnabled === undefined ? true : res.captchaEnabled;
  });
}

getCookie();
getCaptchaEnabled();
</script>

<style lang='scss' scoped>
.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: 40px;

    input {
      
      
      height: 40px;
    }
  }

  .input-icon {
      
      
    height: 39px;
    width: 14px;
    margin-left: 0px;
  }
}

.login-tip {
      
      
  font-size: 13px;
  text-align: center;
  color: #bfbfbf;
}

.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;
}
</style>

2.6. Switch text click or slider verification code

There are two types, one is text click and the other is slider verification, so how to switch?

2.6.1 Backend modification

Repairfcat-adminMojoshita application.yml Medium aj — type :

  • Fill inblockPuzzle for slider
  • Fill in clickWord Click for text

Insert image description here

2.6.2 Front-end modification

Revision login.vue :

<Verify
	@success="capctchaCheckSuccess"
	:mode="'pop'"
	:captchaType="'clickWord'"
	:imgSize="{ width: '330px', height: '155px' }"
	ref="verify"
	v-if="captchaEnabled"
></Verify>

Modify the above codecaptchaType

  • Fill inblockPuzzle for slider
  • Fill in clickWord Click for text

Insert image description here

2.7. Results display:

  • The default basemap display is used for interface exceptions and other situations:

    Insert image description here

  • Screenshot of the slider verification code displaying normally:
    Insert image description here

  • Screenshot of the normal display of the verification code when clicking on the text:
    Insert image description here

Guess you like

Origin blog.csdn.net/weixin_52799373/article/details/135010384