一、背景
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/get | post |
核对验证码 | /captcha/check | post |
接口调用流程
获取验证码接口:/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呗!!!