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></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:
# local定时清除过期缓存(单位秒),设置为0代表不执行
#timing-clear:
#spring.redis.host:
#spring.redis.port:
#spring.redis.password:
#spring.redis.database: 2
#spring.redis.timeout:
# 验证码类型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:
#点选文字个数,存在问题,暂不支持修改
#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:
# get接口一分钟内请求数限制
req-get-minute-limit:
# check接口一分钟内请求数限制
req-check-minute-limit:
# verify接口一分钟内请求数限制
req-verify-minute-limit:
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: ,
repData: {
originalImageBase64: 底图base64,
point: { //默认不返回的,校验的就是该坐标信息,允许误差范围
x: ,
y: 5
},
jigsawImageBase64: 滑块图base64,
token: 71dd26999e314f9abb0c635336976635, //一次校验唯一标识
secretKey: 位随机字符串, //aes秘钥,开关控制,前端根据此值决定是否加密
result: false,
opAdmin: false
},
success: true,
error: false
}
核对验证码接口接口:http://:/captcha/check
请求参数:
{
captchaType: blockPuzzle,
pointJson: QxIVdlJoWUi04iM+65hTow==, //aes加密坐标信息
token: 71dd26999e314f9abb0c635336976635 //get请求返回的token
}
响应参数:
{
repCode: ,
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 无异常,代表成功
//repCode 服务器内部异常
//repCode 参数不能为空
//repCode 验证码已失效,请重新获取
//repCode 验证失败
//repCode 获取验证码失败,请联系管理员
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 是项目的前缀路径,你的项目可能是 端口,这边是 端口,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:/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: % %,
};
});
const [valueData, setValueData] = useState<API.UserRegisterRequest>();
const handleSubmit = async (values: API.UserRegisterRequest) => {
// 1.先判断两次输入的密码是否一致
const {userPassword, checkPassword} = values;
if (userPassword != checkPassword) {
return message.info(两次输入的密码不一致,
)
}
// 2.密码一致
const res = await userRegister(values);
if(res.code !== 0){
return message.error(res.message+-注册失败,请重试,)
}
message.success(注册成功,请登录账号,);
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: ,
maxWidth: 75vw,
}}
logo={<img alt=logo style={{height: %}} 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: ,
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:/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~