一、背景

1.1 概述

传统字符型验证码展示-填写字符-比对答案的流程,目前已可被机器暴力破解,应用程序容易被自动化脚本和机器人攻击。
在这里插入图片描述
摒弃传统字符型验证码,采用行为验证码采用嵌入式集成方式,接入方便,安全,高效。验证码展示-采集用户行为-分析用户行为流程,用户只需要产生指定的行为轨迹,不需要键盘手动输入,极大优化了传统验证码用户体验不佳的问题;同时,快速、准确的返回人机判定结果。
在这里插入图片描述

1.2 应用场景

  • 网站登录:保护用户账号免受非法登录尝试
  • 在线表单提交:避免垃圾邮件和恶意数据填充
  • 论坛或社区:防止机器人自动发帖和灌水
  • 支付验证:保障交易安全,防止欺诈行为

二、anji-plus

AJ-Captcha行为验证码,包含滑动拼图、文字点选两种方式,UI支持弹出和嵌入两种方式。后端提供Java实现,前端提供了php、angular、html、vue、uni-app、flutter、android、ios等代码示例。

代码开源地址:https://gitee.com/anji-plus/captcha
文档地址:https://ajcaptcha.beliefteam.cn/captcha-doc/

2.1 功能简介

功能描述
验证码类型滑动拼图 blockPuzzle / 文字点选 clickWord
验证用户拖动/点击一次验证码拼图即视为一次“验证”,不论拼图/点击是否正确
二次校验验证数据随表单提交到后台后,后台需要调用captchaService.verification做二次校验。目的是核实验证数据的有效性。

2.2 交互流程

① 用户访问应用页面,请求显示行为验证码
② 用户按照提示要求完成验证码拼图/点击
③ 用户提交表单,前端将第二步的输出一同提交到后台
④ 验证数据随表单提交到后台后,后台需要调用captchaService.verification做二次校验。
⑤ 第4步返回校验通过/失败到产品应用后端,再返回到前端。如下图所示。
在这里插入图片描述

三、代码实现

3.1 引入依赖

<dependency>
    <groupId>com.anji-plus</groupId>
    <artifactId>spring-boot-starter-captcha</artifactId>
    <version>1.3.0</version>
</dependency>

3.2 配置

# 验证码配置
aj:
  captcha:
    ########## 重点关注 ##########
    # 验证码类型:default,blockPuzzle,clickWord
    type: default
    # 底图路径,支持全路径、项目资源路径
    jigsaw: classpath:images/jigsaw
    # 滑动图路径,支持全路径、项目资源路径
    pic-click: classpath:images/pic-click
    # 缓存类型:local,redis,other
    cache-type: redis
    # local缓存的阈值,达到这个值,清除缓存
    cache-number: 1000
    # local定时清除过期缓存(单位秒),设置为0代表不执行
    timing-clear: 180
    # 滑块验证码的偏移量
    slip-offset: 5
    # 滑块验证码的加密坐标
    aes-status: true
    # 滑块验证码的滑块干扰项
    interference-options: 2
    # 文字验证码的文字数量【暂不可用】
    click-word-count: 4
    # 文字验证码的文字字体
    font-type: WenQuanZhengHei.ttf
    # 文字验证码的字体样式
    font-style: 1
    # 文字验证码的字体大小
    font-size: 25
    # 水印文字
    water-mark: 强哥软件
    # 水印文字字体
    water-font: WenQuanZhengHei.ttf
    ########## 接口相关配置 ##########
    # 历史数据清理是否开启
    history-data-clear-enable: false
    # 接口请求次数一分钟限制是否开启
    req-frequency-limit-enable: true
    # 验证失败5次,get接口锁定
    req-get-lock-limit: 5
    # 验证失败后,锁定时间间隔,s
    req-get-lock-seconds: 360
    # get接口一分钟内请求数限制
    req-get-minute-limit: 30
    # check接口一分钟内请求数限制
    req-check-minute-limit: 60
    # verify接口一分钟内请求数限制
    req-verify-minute-limit: 60

3.3 自定义验证码存储

使用redis存储验证码

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

实现CaptchaCacheService 接口

package com.qiangesoft.captcha.cache;

import com.anji.captcha.service.CaptchaCacheService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;

import java.util.concurrent.TimeUnit;

/**
 * redis缓存验证码
 *
 * @author qiangesoft
 * @date 2024-05-10
 */
public class CaptchaCacheServiceRedisImpl 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";
    }
}

配置
在resources目录新建META-INF.services文件夹,新建文件名为com.anji.captcha.service.CaptchaCacheService,内容为com.qiangesoft.captcha.cache.CaptchaCacheServiceRedisImpl

3.4 登录验证接口

package com.qiangesoft.captcha.controller;

import com.anji.captcha.model.common.ResponseModel;
import com.anji.captcha.model.vo.CaptchaVO;
import com.anji.captcha.service.CaptchaService;
import com.qiangesoft.captcha.pojo.LoginDTO;
import com.qiangesoft.captcha.pojo.ResultVO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

/**
 * 登录接口
 *
 * @author qiangesoft
 * @date 2024-05-10
 */
@Controller
public class LoginController {

    @Autowired
    private CaptchaService captchaService;

    @GetMapping("/")
    public String index() {
        return "login";
    }

    @ResponseBody
    @PostMapping("/login")
    public ResultVO login(@RequestBody LoginDTO loginDTO) {
        // 登录二次校验
        CaptchaVO captchaVO = new CaptchaVO();
        captchaVO.setCaptchaVerification(loginDTO.getCaptcha());
        ResponseModel response = captchaService.verification(captchaVO);
        if (!response.isSuccess()) {
            throw new RuntimeException("图片验证码校验失败");
        }

        // todo 认证逻辑
        return ResultVO.ok();
    }

}

3.5 vue方式

主要代码如下:

<template>
  <div class="login-bg">
    <el-form style="width: 500px;height: 40px;margin: auto">
      <el-form-item label="账号" prop="username">
        <el-input v-model="username" placeholder="账号"></el-input>
      </el-form-item>
      <el-form-item label="密码" prop="password">
        <el-input v-model="password" placeholder="密码"></el-input>
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="checkParam">登录</el-button>
      </el-form-item>
    </el-form>

    <Verify
      ref="verify"
      captcha-type="blockPuzzle"
      :img-size="{width:'400px',height:'200px'}"
      @success="login"
    />

    <!--    <Verify-->
    <!--      ref="verify"-->
    <!--      captcha-type="clickWord"-->
    <!--      :img-size="{width:'400px',height:'200px'}"-->
    <!--      @success="login"-->
    <!--    />-->
  </div>
</template>

<script>
import Verify from './../components/verifition/Verify'
import { Message } from 'element-ui'
import { login } from '@/api'

export default {
  components: {
    Verify
  },
  data () {
    return {
      username: 'admin',
      password: '123456'
    }
  },
  beforeDestroy () {
    document.removeEventListener('keyup', this.handlerKeyup)
  },
  created () {
    document.addEventListener('keyup', this.handlerKeyup)
  },
  methods: {
    handlerKeyup (e) {
      const keycode = document.all ? event.keyCode : e.which
      if (keycode === 13) {
        this.checkParam()
      }
    },
    checkParam () {
      if (!this.username || !this.password) {
        Message.error('请先输入账号密码')
      }
      this.$refs.verify.show()
    },
    login (params) {
      login({
        username: this.username,
        password: this.password,
        captcha: params.captchaVerification
      }).then(res => {
        const code = res.code
        if (code === 200) {
          Message.success('登录成功')
          this.$router.push('/index')
        } else {
          Message.error(res.message)
        }
      })
    }
  }
}
</script>

3.6 html方式

引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

配置静态资源

package com.qiangesoft.captcha.config;

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

/**
 * 全局异常处理
 *
 * @author qiangesoft
 * @date 2024-03-19
 */
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");
    }
}

html代码

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <meta name="viewport"
          content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no"/>
    <title>verify插件demo</title>
    <link rel="stylesheet" type="text/css" th:href="@{/static/css/verify.css}">

    <script>
        if (!window.Promise) {
            document.writeln('<script src="https://cdnjs.cloudflare.com/ajax/libs/es6-promise/4.1.1/es6-promise.min.js"><' + '/' + 'script>');
        }
    </script>

    <style>
        .btn {
            border: none;
            outline: none;
            width: 110px;
            height: 40px;
            line-height: 40px;
            text-align: center;
            cursor: pointer;
            background-color: #409EFF;
            color: #fff;
            font-size: 16px;
        }
    </style>
</head>

<body>
<div class="box">
    <h3>用户登录</h3>
    账号:<input type="text" id="username" placeholder="账号" value="admin"/> <br/><br/>
    密码:<input type="password" id="password" placeholder="密码" value="123456"/><br/><br/>
    <button class="btn" id='btn'>滑块登录</button>
    <button class="btn" id='btn1'>文字登录</button>
    <div id="mpanel" style="margin-top:50px;">
    </div>
    <div id="mpanel1" style="margin-top:50px;">
    </div>
</div>

<script type="text/javascript" src="https://cdn.bootcdn.net/ajax/libs/jquery/1.9.1/jquery.js"></script>
<script th:src="@{/static/js/crypto-js.js}"></script>
<script th:src="@{/static/js/ase.js}"></script>
<script th:src="@{/static/js/verify.js}"></script>

<script>
    // 滑块
    $('#mpanel').slideVerify({
        baseUrl: 'http://localhost:8028',
        mode: 'pop',
        containerId: 'btn',
        imgSize: {
            width: '400px',
            height: '200px',
        },
        barSize: {
            width: '400px',
            height: '40px',
        },
        // 检验参数合法性的函数,mode ="pop"有效
        beforeCheck: function () {
            // todo 可进行参数校验
            return true
        },
        // 加载完毕的回调
        ready: function () {
        },
        // 成功的回调
        success: function (params) {
            const username = $("#username").val();
            const password = $('#password').val();
            $.ajax({
                url: '/login',
                type: 'post',
                contentType: 'application/json',
                dataType: 'json',
                data: JSON.stringify({
                    "username": username,
                    "password": password,
                    "captcha": params.captchaVerification
                }),
                success: function (res) {
                    if (res.code === 200) {
                        alert("登录成功");
                    } else {
                        alert(res.message);
                    }
                },
                error: function (e) {
                    alert('请求失败')
                }
            })
        },
        // 失败的回调
        error: function () {
        }
    });

    // 文字点击
    $('#mpanel1').pointsVerify({
        baseUrl: 'http://localhost:8028',
        containerId: 'btn1',
        mode: 'pop',
        imgSize: {
            width: '400px',
            height: '200px',
        },
        beforeCheck: function () {
            return true
        },
        ready: function () {
        },
        success: function (params) {
            const username = $("#username").val();
            const password = $('#password').val();
            $.ajax({
                url: '/login',
                type: 'post',
                contentType: 'application/json',
                dataType: 'json',
                data: JSON.stringify({
                    "username": username,
                    "password": password,
                    "captcha": params.captchaVerification
                }),
                success: function (res) {
                    if (res.code === 200) {
                        alert("登录成功");
                    } else {
                        alert(res.message);
                    }
                },
                error: function (e) {
                    alert('请求失败')
                }
            })
        },
        error: function () {
        }
    });
</script>
</body>

</html>

四、测试

4.1 接口调用

依赖中默认接口

功能描述请求方式
获取验证码/captcha/getpost
核对验证码/captcha/checkpost

接口调用流程
在这里插入图片描述

获取验证码接口:/captcha/get
请求参数

{
	"captchaType": "blockPuzzle",  // 验证码类型 clickWord
	"clientUid": "唯一标识"  // 客户端UI组件id,组件初始化时设置一次,UUID(非必传参数)
}

响应数据

{
    "repCode": "0000",
    "repData": {
        "originalImageBase64": "底图base64",
        "point": {    // 默认不返回的,校验的就是该坐标信息,允许误差范围
            "x": 205,
            "y": 5
        },
        "jigsawImageBase64": "滑块图base64",
        "token": "71dd26999e314f9abb0c635336976635", // 一次校验唯一标识
        "secretKey": "16位随机字符串", // aes秘钥,开关控制,前端根据此值决定是否加密
        "result": false,
        "opAdmin": false
    },
    "success": true,
    "error": false
}

核对验证码接口:/captcha/check
请求参数

{
	 "captchaType": "blockPuzzle",
	 "pointJson": "QxIVdlJoWUi04iM+65hTow==",  // aes加密坐标信息
	 "token": "71dd26999e314f9abb0c635336976635"  // get请求返回的token
}

响应数据

{
    "repCode": "0000",
    "repData": {
        "captchaType": "blockPuzzle",
        "token": "71dd26999e314f9abb0c635336976635",
        "result": true,
        "opAdmin": false
    },
    "success": true,
    "error": false
}

4.2 vue

在这里插入图片描述
在这里插入图片描述

4.3 html

在这里插入图片描述
在这里插入图片描述

五、源码地址

码云:https://gitee.com/qiangesoft/boot-business/tree/master/boot-business-captcha

方便的话博客点个小心心,码云仓库点个star呗!!!

GitHub 加速计划 / vu / vue
207.53 K
33.66 K
下载
vuejs/vue: 是一个用于构建用户界面的 JavaScript 框架,具有简洁的语法和丰富的组件库,可以用于开发单页面应用程序和多页面应用程序。
最近提交(Master分支:1 个月前 )
73486cb5 * chore: fix link broken Signed-off-by: snoppy <michaleli@foxmail.com> * Update packages/template-compiler/README.md [skip ci] --------- Signed-off-by: snoppy <michaleli@foxmail.com> Co-authored-by: Eduardo San Martin Morote <posva@users.noreply.github.com> 3 个月前
e428d891 Updated Browser Compatibility reference. The previous currently returns HTTP 404. 4 个月前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐