5 分钟快速上手图形验证码,防止接口被恶意刷量!
大家好,我是程序员小白条,今天来给大家介绍一个快速实现图形验证码的优秀框架 AJ-Captcha。
需求分析
如果注册接口没有验证码这种类型的限制,很容易会被刷量,因此,一般都会使用邮箱验证码或者图形验证码进行限制,防止被恶意刷接口。邮箱验证码比较容易的是 QQ 验证码,直接配合 SpringMail 即可实现,本文主要实现图形验证码。
文字验证如下
滑动验证如下
后端
1)pom.xml 引入官方依赖包
<dependency> <groupId>com.anji-plus</groupId> <artifactId>spring-boot-starter-captcha</artifactId> <version>1.3.0</version> </dependency>
|
2)设置配置文件 properties 或者 yml 格式
aj: captcha:
cache-type: local
type: default water-mark: "\u6211\u7684\u6c34\u5370" slip-offset: 5 aes-status: true interference-options: 2
font-style: 1 font-size: 25
history-data-clear-enable: false
req-frequency-limit-enable: false req-get-lock-limit: 5 req-get-lock-seconds: 360 req-get-minute-limit: 30 req-check-minute-limit: 60 req-verify-minute-limit: 60
|
3)创建一个配置类,让 SpringBoot 启动时,扫描到即可
@Configuration public class CaptchaConfig {
@Bean(name = "CaptchaCacheService") @Primary public CaptchaCacheService captchaCacheService(AjCaptchaProperties config){ CaptchaCacheService ret = CaptchaServiceFactory.getCache(config.getCacheType().name()); return ret; } }
|
4)创建默认实现类,跟 application.yml 的配置有关
public class DefaultCaptchaServiceImpl extends AbstractCaptchaService { DefaultCaptchaServiceImpl() { }
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", "clientUid": "唯一标识" }
|
响应参数:
{ "repCode": "0000", "repData": { "originalImageBase64": "底图base64", "point": { "x": 205, "y": 5 }, "jigsawImageBase64": "滑块图base64", "token": "71dd26999e314f9abb0c635336976635", "secretKey": "16位随机字符串", "result": false, "opAdmin": false }, "success": true, "error": false }
|
核对验证码接口接口:http://:/captcha/check
请求参数:
{ "captchaType": "blockPuzzle", "pointJson": "QxIVdlJoWUi04iM+65hTow==", "token": "71dd26999e314f9abb0c635336976635" }
|
响应参数:
{ "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()) { 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) => { const {userPassword, checkPassword} = values; if (userPassword != checkPassword) { return message.info("两次输入的密码不一致", 1.5 ) } 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~⭐