5 分钟快速上手图形验证码,防止接口被恶意刷量!

大家好,我是程序员小白条,今天来给大家介绍一个快速实现图形验证码的优秀框架 AJ-Captcha。

需求分析

如果注册接口没有验证码这种类型的限制,很容易会被刷量,因此,一般都会使用邮箱验证码或者图形验证码进行限制,防止被恶意刷接口。邮箱验证码比较容易的是 QQ 验证码,直接配合 SpringMail 即可实现,本文主要实现图形验证码。

文字验证如下

滑动验证如下

后端

1)pom.xml 引入官方依赖包

<!-- 接入滑动验证 https://ajcaptcha.beliefteam.cn/captcha-doc/captchaDoc/java.html-->
<dependency>
<groupId>com.anji-plus</groupId>
<artifactId>spring-boot-starter-captcha</artifactId>
<version>1.3.0</version>
</dependency>

2)设置配置文件 properties 或者 yml 格式

# 滑动验证,底图路径,不配置将使用默认图片
# 支持全路径
# 支持项目路径,以classpath:开头,取resource目录下路径,例:classpath:images/jigsaw
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
#spring.redis.host: 10.108.11.46
#spring.redis.port: 6379
#spring.redis.password:
#spring.redis.database: 2
#spring.redis.timeout: 6000

# 验证码类型default两种都实例化。
type: default
# 汉字统一使用Unicode,保证程序通过@value读取到是中文,可通过这个在线转换;yml格式不需要转换
# https://tool.chinaz.com/tools/unicode.aspx 中文转Unicode
# 右下角水印文字(我的水印)
water-mark: "\u6211\u7684\u6c34\u5370"
# 右下角水印字体(不配置时,默认使用文泉驿正黑)
# 由于宋体等涉及到版权,我们jar中内置了开源字体【文泉驿正黑】
# 方式一:直接配置OS层的现有的字体名称,比如:宋体
# 方式二:自定义特定字体,请将字体放到工程resources下fonts文件夹,支持ttf\ttc\otf字体
#water-font: WenQuanZhengHei.ttf
# 点选文字验证码的文字字体(文泉驿正黑)
#font-type: WenQuanZhengHei.ttf
# 校验滑动拼图允许误差偏移量(默认5像素)
slip-offset: 5
# aes加密坐标开启或者禁用(true|false)
aes-status: true
# 滑动干扰项(0/1/2)
interference-options: 2

#点选字体样式 默认Font.BOLD
font-style: 1
#点选字体字体大小
font-size: 25
#点选文字个数,存在问题,暂不支持修改
#click-word-count: 4

history-data-clear-enable: false

# 接口请求次数一分钟限制是否开启 true|false
req-frequency-limit-enable: false
# 验证失败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)创建一个配置类,让 SpringBoot 启动时,扫描到即可

@Configuration
public class CaptchaConfig {



@Bean(name = "CaptchaCacheService")
@Primary
public CaptchaCacheService captchaCacheService(AjCaptchaProperties config){
//缓存类型redis/local/....
CaptchaCacheService ret = CaptchaServiceFactory.getCache(config.getCacheType().name());
return ret;
}
}

4)创建默认实现类,跟 application.yml 的配置有关

public class DefaultCaptchaServiceImpl extends AbstractCaptchaService {
DefaultCaptchaServiceImpl() {
//document why this constructor is empty
}

//这个需要实现,如果返回 redis 那就是使用 redis 那套
public String captchaType() {return "default";}
@Override
public void init(Properties config) {
for (String s : CaptchaServiceFactory.instances.keySet()) {
if (!this.captchaType().equals(s)) {
this.getService(s).init(config);
}
}
}
@Override
public void destroy(Properties config) {
for (String s : CaptchaServiceFactory.instances.keySet()) {
if (!this.captchaType().equals(s)) {
this.getService(s).destroy(config);
}
}
}
private CaptchaService getService(String captchaType) {return CaptchaServiceFactory.instances.get(captchaType);}
@Override
public ResponseModel get(CaptchaVO captchaVO) {
if (captchaVO == null) {
return RepCodeEnum.NULL_ERROR.parseError("captchaVO");
} else {
return StringUtils.isEmpty(captchaVO.getCaptchaType()) ? RepCodeEnum.NULL_ERROR.parseError("类型") : this.getService(captchaVO.getCaptchaType()).get(captchaVO);
}
}

@Override
public ResponseModel check(CaptchaVO captchaVO) {
if (captchaVO == null) {
return RepCodeEnum.NULL_ERROR.parseError("captchaVO");
} else if (StringUtils.isEmpty(captchaVO.getCaptchaType())) {
return RepCodeEnum.NULL_ERROR.parseError("类型");
} else {
return StringUtils.isEmpty(captchaVO.getToken()) ? RepCodeEnum.NULL_ERROR.parseError("token") : this.getService(captchaVO.getCaptchaType()).check(captchaVO);
}
}
@Override
public ResponseModel verification(CaptchaVO captchaVO) {
if (captchaVO == null) {
return RepCodeEnum.NULL_ERROR.parseError("captchaVO");
} else if (StringUtils.isEmpty(captchaVO.getCaptchaVerification())) {
return RepCodeEnum.NULL_ERROR.parseError("二次校验参数");
} else {
try {
String codeKey = String.format(REDIS_SECOND_CAPTCHA_KEY, captchaVO.getCaptchaVerification());
if (!CaptchaServiceFactory.getCache(cacheType).exists(codeKey)) {
return ResponseModel.errorMsg(RepCodeEnum.API_CAPTCHA_INVALID);
}

CaptchaServiceFactory.getCache(cacheType).delete(codeKey);
} catch (Exception var3) {
this.logger.error("验证码坐标解析失败", var3);
return ResponseModel.errorMsg(var3.getMessage());
}

return ResponseModel.success();
}
}
}

后端接口

获取验证码接口:http://你的项目地址/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
}

核对验证码接口接口:http://:/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
}

5)完成前面四步后,即可测试接口是否成功被调用,可以用 postman 或者 apifox 等测试工具。

6)在用户注册的 dto 实体类加入新字段 captchaVerification。

@Data
public class UserRegisterRequest implements Serializable {

private static final long serialVersionUID = 3191241716373120793L;

private String userAccount;

private String userPassword;

private String checkPassword;

private String captchaVerification;

}

7)在注册接口,加上检验图形验证码的服务。

先自动注入依赖

@Resource
private CaptchaService captchaService;

再加上这段代码即可

CaptchaVO captchaVO = new CaptchaVO();
captchaVO.setCaptchaVerification(userRegisterRequest.getCaptchaVerification());
ResponseModel response = captchaService.verification(captchaVO);
if(!response.isSuccess()) {
//验证码校验失败,返回信息告诉前端
//repCode 0000 无异常,代表成功
//repCode 9999 服务器内部异常
//repCode 0011 参数不能为空
//repCode 6110 验证码已失效,请重新获取
//repCode 6111 验证失败
//repCode 6112 获取验证码失败,请联系管理员
throw new BusinessException(ErrorCode.FORBIDDEN_ERROR, "验证码错误请重试");
}

前端

1)引入依赖

npm install aj-captcha-react

2)定义一个函数

const ref = useRef();
const click = () => {
ref.current?.verify();
console.log(ref.current?.verify());
};

3)使用组件,这边 valueData 就是注册时带的数据,注意:path 是项目的前缀路径,你的项目可能是 8101 端口,这边是 8204 端口,api 是项目的前缀!

const [valueData, setValueData] = useState<API.UserRegisterRequest>();
<Captcha
onSuccess={async (data) => {
const value = valueData;
if (value) {
value.captchaVerification = data.captchaVerification;
await handleSubmit(value);
}
}}
path="http://localhost:8204/api"
type="auto"
ref={ref}></Captcha>

完整 tsx 代码如下

const Register: React.FC = () => {
const ref = useRef();
const click = () => {
ref.current?.verify();
console.log(ref.current?.verify());
};
const [type, setType] = useState<string>('register');
const containerClassName = useEmotionCss(() => {
return {
display: 'flex',
flexDirection: 'column',
height: '100vh',
overflow: 'auto',
backgroundImage:
"url('https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/V-_oS6r-i7wAAAAAAAAAAAAAFl94AQBr')",
backgroundSize: '100% 100%',
};
});
const [valueData, setValueData] = useState<API.UserRegisterRequest>();

const handleSubmit = async (values: API.UserRegisterRequest) => {
// 1.先判断两次输入的密码是否一致
const {userPassword, checkPassword} = values;
if (userPassword != checkPassword) {
return message.info("两次输入的密码不一致", 1.5
)
}
// 2.密码一致
const res = await userRegister(values);
if(res.code !== 0){
return message.error(res.message+"-注册失败,请重试",1.5)
}
message.success('注册成功,请登录账号',1.5);
history.push('/user/login');
};
return (
<div className={containerClassName}>
<Helmet>
<title>
{Settings.title}
</title>
</Helmet>
<div
style={{
flex: '1',
padding: '32px 0',
}}
>
<LoginForm
submitter={{
searchConfig: {
submitText: '注册',
},
}}
contentStyle={{
minWidth: 280,
maxWidth: '75vw',
}}
logo={<img alt="logo" style={{height: '100%'}} src="/logo.svg"/>}
title="小白条前端模板"
subTitle={'快速开发属于自己的前端项目'}
initialValues={{
autoLogin: true,
}}
onFinish={async (values) => {
click();
setValueData(values);
}}
>
<Tabs
activeKey={type}
onChange={setType}
centered
items={[
{
key: 'register',
label: '用户注册',
},
]}
/>

{type === 'register' && (
<>
<ProFormText
name="userAccount"
fieldProps={{
size: 'large',
prefix: <UserOutlined/>,
}}
placeholder={'请输入账号'}
rules={[
{
required: true,
message: '账号是必填项!',
},
{
min: 6,
type: 'string',
message: '长度不能小于 6',
},
]}
/>
<ProFormText.Password
name="userPassword"
fieldProps={{
size: 'large',
prefix: <LockOutlined/>,
}}
placeholder={'请输入密码'}
rules={[
{
required: true,
message: '密码是必填项!',
},
{
min: 8,
type: 'string',
message: '长度不能小于 8',
},
]}
/>
<ProFormText.Password
name="checkPassword"
fieldProps={{
size: 'large',
prefix: <LockOutlined/>,
}}
placeholder="请再次输入密码"
rules={[
{
required: true,
message: '确认密码是必填项!',
},
{
min: 8,
type: 'string',
message: '长度不能小于 8',
},
]}
/>
</>
)}

<div
style={{
marginBottom: 24,
textAlign: 'right',
}}
>
<a onClick={() => {
history.push("/user/login")
}}>用户登录</a>
</div>
<Captcha
onSuccess={async (data) => {
const value = valueData;
if (value) {
value.captchaVerification = data.captchaVerification;
await handleSubmit(value);
}
}}
path="http://localhost:8102/api"
type="auto"
ref={ref}></Captcha>
</LoginForm>


</div>
<Footer/>
</div>
);
};
export default Register;

完成之后即可看到效果图,可以自定义水印和图片,具体可以看官方文档。

官方文档地址:https://ajcaptcha.beliefteam.cn/captcha-doc/

我的 GitHub 地址:https://github.com/luoye6

个人项目:https://gitee.com/falle22222n-leaves/vue_-book-manage-system

欢迎 Follow 💕 和 Star~⭐