基于 Vue 和 Spring Boot 实现滑块验证码的机器验证

基于 Vue 和 Spring Boot 实现滑块验证码的机器验证

本文详细介绍了如何使用 AJ-Captcha 库实现一个安全的移动端登录系统,包含滑块验证码(行为验证)功能。用户需要输入手机号,完成滑块验证,并由后端校验手机号是否存在后才能触发短信验证码发送。本实现参考了 Gitee 的手机号登录流程。

实现效果如下:

需求概述

登录流程如下:

  1. 用户输入手机号,前端进行格式校验。
  2. 点击"发送验证码"后触发滑块验证码。
  3. 滑块验证通过后,后端校验手机号是否存在。
  4. 手机号存在则发送短信验证码,用户输入验证码后完成登录。

以下是参考的 Gitee 登录流程时序图和界面示意图:

技术选型

  • 采用 AJ-Captcha 行为验证码库,支持滑动拼图和文字点选两种验证方式。AJ-Captcha 提供前后端交互的完整解决方案,支持 Java 后端以及多种前端框架(Vue、React、Flutter 等)。

  • 功能特点

    • 嵌入式集成,接入简单,安全高效。

    • 抛弃传统字符验证码,优化用户体验。

    • 支持多种前端框架和后端缓存方式(Redis、内存等)。

    • 提供滑动拼图和文字点选两种验证码类型。

    • 在线文档AJ-Captcha 官方文档

前端实现

参考文档:vue | AJ-Captcha

1. 引入组件

view/vue3/src/components/verifition · anji-plus/AJ-Captcha - 码云 - 开源中国

apl 复制代码
# 1.复制view/vue/src/components/verifition文件夹,到自己工程对应目录下,在登录页面插入如下代码。
# 2.安装请求和加密依赖
npm install axios  crypto-js   -S

2. 修改后端请求URL

修改aixos.js中的axios.defaults.baseURL为后端服务的url

3. 新增机器验证页面

vue 复制代码
<!-- src/views/TestVerification.vue -->
<template>
  <Verify
      @success="success"
      :mode="'pop'"
      :captchaType="'blockPuzzle'"
      :imgSize="{ width: '330px', height: '155px' }"
      ref="verify"
  />
  <button @click="useVerify">调用验证组件</button>
</template>

<script>
import Verify from '../components/verifition/Verify.vue'

export default {
  name: 'TestVerification',
  components: { Verify },
  methods: {
    success(params) {
      console.log('验证成功,返回参数:', params)
    },
    useVerify() {
      this.$refs.verify.show()
    }
  }
}
</script>

4.首页调用验证组件

修改App.vue文件

vue 复制代码
<template>
  <verify/>
</template>

<script>
import verify from './views/TestVerification.vue'

export default {
  name: 'App',
  components: {
    verify
  }
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

后端实现

流程梳理

  1. 用户输入手机号 -> 前端格式校验 -> 点击"发送验证码"

  2. 调用后端机器校验初始化接口 -> 返回滑块校验参数 -> 跳转滑块页面

  3. 前端滑块完成 -> 调用后端滑块校验接口 + 传滑块验证数据 + 手机号

  4. 后端验证滑块数据:

    • 成功 -> 校验手机号是否存在
      • 存在 -> 返回允许发送验证码
      • 不存在 -> 返回错误提示手机号不存在
    • 失败 -> 返回滑块验证失败
  5. 前端收到允许发送验证码后 -> 调用发送验证码接口(携带手机号)

  6. 用户输入验证码 -> 调用登录接口(手机号+验证码)

具体实现

1. 引入依赖

xml 复制代码
<dependency>
   <groupId>com.anji-plus</groupId>
   <artifactId>captcha-spring-boot-starter</artifactId>
   <version>1.4.0</version>
</dependency>

复制 images文件夹到resources目录下,项目地址:images · anji-plus/AJ-Captcha - 码云 - 开源中国

2. 增加yml配置

yaml 复制代码
aj:
  captcha:
    jigsaw: classpath:images/jigsaw
    #滑动验证,底图路径,不配置将使用默认图片
    ##支持全路径
    # 支持项目路径,以classpath:开头,取resource目录下路径,例:classpath:images/pic-click
    pic-click: classpath:images/pic-click
    # 对于分布式部署的应用,我们建议应用自己实现CaptchaCacheService,比如用Redis或者memcache,
    # 参考CaptchaCacheServiceRedisImpl.java
    # 如果应用是单点的,也没有使用redis,那默认使用内存。
    # 内存缓存只适合单节点部署的应用,否则验证码生产与验证在节点之间信息不同步,导致失败。
    # !!! 注意啦,如果应用有使用spring-boot-starter-data-redis,
    # 请打开CaptchaCacheServiceRedisImpl.java注释。
    # redis ----->  SPI: 在resources目录新建META-INF.services文件夹(两层),参考当前服务resources。
    # 缓存local/redis...
    cache-type: local
    # local缓存的阈值,达到这个值,清除缓存
    cache-number: 1000
    # local定时清除过期缓存(单位秒),设置为0代表不执行
    timing-clear: 180
    # 验证码类型default两种都实例化。
    type: default
    # 汉字统一使用Unicode,保证程序通过@value读取到是中文,可通过这个在线转换;yml格式不需要转换
    # https://tool.chinaz.com/tools/unicode.aspx 中文转Unicode
    # 右下角水印文字(我的水印)
    water-mark: water-mark
    # 右下角水印字体(不配置时,默认使用文泉驿正黑)
    # 由于宋体等涉及到版权,我们jar中内置了开源字体【文泉驿正黑】
    # 方式一:直接配置OS层的现有的字体名称,比如:宋体
    # 方式二:自定义特定字体,请将字体放到工程resources下fonts文件夹,支持ttf\ttc\otf字体
    # aj.captcha.water-font=WenQuanZhengHei.ttf
    # water-font: SourceHanSansCN-Normal.otf
    # 点选文字验证码的文字字体(文泉驿正黑)
    # aj.captcha.font-type=WenQuanZhengHei.ttf
    # font-type: SourceHanSansCN-Normal.otf
    # 校验滑动拼图允许误差偏移量(默认5像素)
    slip-offset: 5
    # aes加密坐标开启或者禁用(true|false)
    aes-status: true
    # 滑动干扰项(0/1/2)
    interference-options: 1
    history-data-clear-enable: true
    # 接口请求次数一分钟限制是否开启 true|false
    req-frequency-limit-enable: true
    # 验证失败5次,get接口锁定
    req-get-lock-limit: 5
    # 验证失败后,锁定时间间隔,s
    req-get-lock-seconds: 60
    # get接口一分钟内请求数限制
    req-get-minute-limit: 30
    # check接口一分钟内请求数限制
    req-check-minute-limit: 60
    # verify接口一分钟内请求数限制
    req-verify-minute-limit: 60

3. 代码实现

新增CaptchaController.java类

java 复制代码
package com.zhou.demo.controller;

import com.anji.captcha.model.common.ResponseModel;
import com.anji.captcha.model.vo.CaptchaVO;
import com.anji.captcha.service.CaptchaService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

/**
 * 验证码操作处理
 *
 * @author cxstar
 */
@RestController
public class CaptchaController {

    @Resource
    private CaptchaService captchaService;

    /**
     * 获取验证码接口
     *
     * @param captchaVO 验证码参数
     *                  "captchaType": "blockPuzzle",
     *                  "clientUid": "唯一标识"
     */
    @PostMapping("/captcha/get")
    public ResponseModel get(@RequestBody CaptchaVO captchaVO) {
        return captchaService.get(captchaVO);
    }

    /**
     * 校验滑动验证
     *
     * @param captchaVO 验证码参数
     *                  "captchaType": "blockPuzzle",
     *                  "pointJson": "QxIVdlJoWUi04iM+65hTow==",  //aes加密坐标信息
     *                  "token": "71dd26999e314f9abb0c635336976635"  //get请求返回的token
     */
    @PostMapping("/captcha/check")
    public ResponseModel check(@RequestBody CaptchaVO captchaVO) {
        return captchaService.check(captchaVO);
    }

}

4.跨域配置(可选)

如果报错如下:

shell 复制代码
Access to XMLHttpRequest at 'http://localhost:9001/captcha/get' from origin 'http://localhost:8080' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.

在后端增加过滤配置:

java 复制代码
package com.zhou.demo;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**") // 匹配所有接口
                .allowedOriginPatterns("*") // 允许所有来源(Spring Boot 2.4+ 推荐用 allowedOriginPatterns)
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .allowedHeaders("*")
                .allowCredentials(true) // 允许携带 Cookie
                .maxAge(3600); // 预检请求缓存时间,单位秒
    }
}

实现效果

如上所述已经实现了基础的验证功能

二次验证的实现

为什么需要二次验证?

若不进行后端二次验证,攻击者可直接构造验证码发送请求,绕过滑块验证,导致短信接口被恶意刷取。二次验证通过后端校验滑块结果,确保请求合法性。

前端实现

以下内容基于上述基础实现

1.APP.vue

主要去除了原先的测试组件

vue 复制代码
<template>
  <router-view/>
</template>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
}

nav {
  padding: 30px;
}

nav a {
  font-weight: bold;
  color: #2c3e50;
}

nav a.router-link-exact-active {
  color: #42b983;
}
</style>

2.新增登录和验证页面

LoginPage.vue 登录页面

vue 复制代码
<template>
  <div class="login-container">
    <h2>登录</h2>
    <div class="input-group">
      <input type="tel" v-model="phoneNumber" placeholder="请输入手机号" @input="validatePhone" />
      <span v-if="phoneError" class="error-message">{{ phoneError }}</span>
    </div>
    <div class="captcha-group">
      <input type="text" v-model="captcha" placeholder="请输入验证码" />
      <button @click="goToSlider" class="captcha-btn" :disabled="!!phoneError">获取验证码</button>
    </div>
    <button @click="login" class="login-btn">登录</button>
  </div>
</template>

<script setup>
import { ref } from 'vue';
import { useRouter } from 'vue-router';

const phoneNumber = ref('');
const captcha = ref('');
const phoneError = ref('');
const router = useRouter();

const validatePhone = () => {
  const phoneRegex = /^\d{11}$/;
  if (!phoneNumber.value) {
    phoneError.value = '手机号不能为空';
  } else if (!phoneRegex.test(phoneNumber.value)) {
    phoneError.value = '请输入有效的11位手机号';
  } else {
    phoneError.value = '';
  }
};

const goToSlider = () => {
  if (!phoneError.value) {
    console.log('跳转到滑块验证页面:', phoneNumber.value)
    router.push({ path: '/slider', query: { phone: phoneNumber.value } });
  } else {
    console.log('手机号格式错误,未跳转:', phoneError.value);
  }
};

const login = () => {
  if (!phoneNumber.value || !captcha.value || phoneError.value) {
    alert('请填写正确的手机号和验证码!');
    return;
  }
  console.log('登录中...', {
    phoneNumber: phoneNumber.value,
    captcha: captcha.value
  });
};
</script>

<style scoped>
/* 保持原有样式不变 */
.login-container {
  width: 300px;
  margin: 100px auto;
  padding: 20px;
  border: 1px solid #ccc;
  border-radius: 5px;
  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
  text-align: center;
}

h2 {
  color: #333;
  margin-bottom: 20px;
}

.input-group {
  margin-bottom: 15px;
  position: relative;
}

input {
  width: 100%;
  padding: 10px;
  border: 1px solid #ccc;
  border-radius: 5px;
  font-size: 14px;
}

.captcha-group {
  display: flex;
  align-items: center;
  margin-bottom: 15px;
}

.captcha-group input {
  flex: 1;
  margin-right: 10px;
}

.captcha-btn {
  padding: 10px 20px;
  background-color: #ff8c00;
  color: white;
  border: none;
  border-radius: 5px;
  cursor: pointer;
  font-size: 14px;
}

.captcha-btn:hover {
  background-color: #e07b00;
}

.captcha-btn:disabled {
  background-color: #ccc;
  cursor: not-allowed;
}

.login-btn {
  width: 100%;
  padding: 10px;
  background-color: #ff8c00;
  color: white;
  border: none;
  border-radius: 5px;
  font-size: 16px;
  cursor: pointer;
}

.login-btn:hover {
  background-color: #e07b00;
}

.error-message {
  color: #ff0000;
  font-size: 12px;
  position: absolute;
  top: 100%;
  left: 0;
  margin-top: 5px;
}
</style>

SliderVerification.vue 验证页面

vue 复制代码
<template>
  <div>
    <div class="phone-input">
      <input
          v-model="phone"
          type="text"
          placeholder="请输入手机号码"
          @input="validatePhone"
      />
      <span v-if="phoneError" class="error">{{ phoneError }}</span>
    </div>
    <Verify
        @success="success"
        :mode="'pop'"
        :captchaType="'blockPuzzle'"
        :imgSize="{ width: '330px', height: '155px' }"
        ref="verify"
    />
  </div>
</template>

<script>
import Verify from "./../components/verifition/Verify";
import request from "@/components/verifition/utils/axios";

export default {
  name: "App",
  components: {
    Verify,
  },
  data() {
    return {
      phone: "",
      phoneError: "",
    };
  },
  mounted() {
    // 显示验证弹窗
    this.$refs.verify.show();
  },
  methods: {
    validatePhone() {
      // 基本的手机号码验证(示例:10-12位数字)
      const phoneRegex = /^\d{10,12}$/;
      this.phoneError = phoneRegex.test(this.phone)
          ? ""
          : "请输入有效的手机号码(10-12位数字)";
    },
    success(params) {
      // 构建 SmsRequest 数据
      const smsRequest = {
        // phone: this.phone, // 来自用户输入,这里只做二次验证的演示,因此不传入phone参数
        captchaVerification: params.captchaVerification, // 来自 Verify 组件
      };

      debugger
      // 发送请求到后端
      request({
        url: "/sendLoginCode",
        method: "post",
        data: smsRequest,
      })
          .then((response) => {
            console.log("短信请求成功:", response.data);
            // 跳转到登录页面
            this.$router.push("/login");
          })
          .catch((error) => {
            console.error("短信请求失败:", error);
            alert("发送验证码失败,请重试。");
          });
    },
  },
};
</script>

<style scoped>
.phone-input {
  margin-bottom: 20px;
}

input {
  padding: 10px;
  width: 200px;
  border: 1px solid #ccc;
  border-radius: 4px;
}

.error {
  color: red;
  font-size: 12px;
  margin-top: 5px;
  display: block;
}
</style>

3.配置路由

src/router/index.js

js 复制代码
import { createRouter, createWebHistory } from 'vue-router';
import LoginPage from '../views/LoginPage.vue';
import SliderVerification from '../views/SliderVerification.vue';

const routes = [
  { path: '/login', component: LoginPage },
  {
    path: '/slider',
    component: SliderVerification,
    props: (route) => ({ phone: route.query.phone })
  },
  { path: '/', redirect: '/login' }
];

const router = createRouter({
  history: createWebHistory(),
  routes
});

export default router;

后端实现

新增发送验证码接口

java 复制代码
/**
 * 发送验证码接口,需要二次验证
 *
 * @param captchaVO
 * @return
 */
@RequestMapping(value = "/sendLoginCode", method = RequestMethod.POST)
public ResponseModel sendRegisterCode(@RequestBody CaptchaVO captchaVO) {
    // 判断是否已经通过滑块验证
    ResponseModel response = captchaService.verification(captchaVO);
    System.out.println(response.isSuccess() ? "二次验证通过" : "二次验证未通过");

    // todo 发送验证码

    return response;
}

访问与实现效果

访问地址:http://localhost:8081/login

其他问题整合

移动端滑动与浏览器默认左滑返回上一页冲突

解决方案:限制滑动区域不贴边

避免滑块靠近屏幕左侧边缘(<20px),可在布局上做调整,例如:

css 复制代码
.slider-container {
  margin-left: 20px; /* 留出边缘,避免触发左滑返回 */
  max-width: 90%;
}

参考链接:阻止移动端H5开发浏览器默认左右滑动行为_h5 touch 与 浏览器左右滑冲突-CSDN博客

Demo地址

复制代码
https://zhouquanquan.lanzn.com/iTLVG2y754ng
密码:hb4d
相关推荐
To_OC9 小时前
别再串行写 await 了,Promise.all 才是并行请求的正确打开方式
前端·javascript·promise
vipbic10 小时前
中后台越做越乱后,我用插件化把它救回来了
前端·vue.js
Hyyy10 小时前
Computer Use 适合做什么,不适合做什么——一次真实使用后的思考
前端
小和尚同志10 小时前
前端 AI 单元测试思考与落地
前端·人工智能·aigc
invicinble12 小时前
c端系统,其实更像一个信息展示平台
前端
李姆斯12 小时前
管理是否可以被完全量化
前端·产品经理·团队管理
名字还没想好☜13 小时前
Next.js 中间件实战:鉴权、重定向与 A/B 分流
开发语言·前端·javascript·中间件·react·next.js
广州灵眸科技有限公司13 小时前
瑞芯微RV1126B开发板(EASY-EAI-PI2) INI文件操作
java·前端·javascript·网络·人工智能
kaiking_g14 小时前
vue项目中,静态导入 vs 动态导入 详解
前端·javascript·vue.js
江华森14 小时前
Web 开发基础:HTTP 与 REST
前端·网络协议·http