feat 接入tianai行为验证码
This commit is contained in:
parent
552e543471
commit
4ec0812066
2
pom.xml
2
pom.xml
@ -51,6 +51,8 @@
|
||||
<anyline.version>8.7.2-20241022</anyline.version>
|
||||
<!--工作流配置-->
|
||||
<flowable.version>7.0.1</flowable.version>
|
||||
<!-- 行为验证码配置 -->
|
||||
<tianai-captcha.version>1.5.1</tianai-captcha.version>
|
||||
|
||||
<!-- 插件版本 -->
|
||||
<maven-jar-plugin.version>3.2.2</maven-jar-plugin.version>
|
||||
|
@ -1,5 +1,10 @@
|
||||
package org.dromara.web.controller;
|
||||
|
||||
import cloud.tianai.captcha.application.ImageCaptchaApplication;
|
||||
import cloud.tianai.captcha.application.vo.CaptchaResponse;
|
||||
import cloud.tianai.captcha.application.vo.ImageCaptchaVO;
|
||||
import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
|
||||
import cloud.tianai.captcha.common.response.ApiResponse;
|
||||
import cn.dev33.satoken.annotation.SaIgnore;
|
||||
import cn.hutool.captcha.AbstractCaptcha;
|
||||
import cn.hutool.captcha.generator.CodeGenerator;
|
||||
@ -11,6 +16,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.common.core.constant.Constants;
|
||||
import org.dromara.common.core.constant.GlobalConstants;
|
||||
import org.dromara.common.core.domain.R;
|
||||
import org.dromara.common.core.exception.ServiceException;
|
||||
import org.dromara.common.core.utils.SpringUtils;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.core.utils.reflect.ReflectUtils;
|
||||
@ -21,19 +27,26 @@ import org.dromara.common.ratelimiter.enums.LimitType;
|
||||
import org.dromara.common.redis.utils.RedisUtils;
|
||||
import org.dromara.common.web.config.properties.CaptchaProperties;
|
||||
import org.dromara.common.web.enums.CaptchaType;
|
||||
import org.dromara.common.web.enums.InputCaptchaType;
|
||||
import org.dromara.sms4j.api.SmsBlend;
|
||||
import org.dromara.sms4j.api.entity.SmsResponse;
|
||||
import org.dromara.sms4j.core.factory.SmsFactory;
|
||||
import org.dromara.web.domain.bo.CaptchaBo;
|
||||
import org.dromara.web.domain.vo.CaptchaVo;
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.expression.ExpressionParser;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
/**
|
||||
* 验证码操作处理
|
||||
@ -50,6 +63,8 @@ public class CaptchaController {
|
||||
private final CaptchaProperties captchaProperties;
|
||||
private final MailProperties mailProperties;
|
||||
|
||||
private final ImageCaptchaApplication imageCaptchaApplication;
|
||||
|
||||
/**
|
||||
* 短信验证码
|
||||
*
|
||||
@ -109,15 +124,20 @@ public class CaptchaController {
|
||||
captchaVo.setCaptchaEnabled(false);
|
||||
return R.ok(captchaVo);
|
||||
}
|
||||
captchaVo.setType(captchaProperties.getType());
|
||||
if (CaptchaType.ACT.getType().equalsIgnoreCase(captchaProperties.getType())) {
|
||||
return R.ok(captchaVo);
|
||||
}
|
||||
// 保存验证码信息
|
||||
String uuid = IdUtil.simpleUUID();
|
||||
String verifyKey = GlobalConstants.CAPTCHA_CODE_KEY + uuid;
|
||||
// 生成验证码
|
||||
CaptchaType captchaType = captchaProperties.getType();
|
||||
boolean isMath = CaptchaType.MATH == captchaType;
|
||||
Integer length = isMath ? captchaProperties.getNumberLength() : captchaProperties.getCharLength();
|
||||
CodeGenerator codeGenerator = ReflectUtils.newInstance(captchaType.getClazz(), length);
|
||||
AbstractCaptcha captcha = SpringUtils.getBean(captchaProperties.getCategory().getClazz());
|
||||
CaptchaProperties.InputCaptchaProperties inputCaptchaProperties = captchaProperties.getInput();
|
||||
InputCaptchaType inputCaptchaType = inputCaptchaProperties.getType();
|
||||
boolean isMath = InputCaptchaType.MATH == inputCaptchaType;
|
||||
Integer length = isMath ? inputCaptchaProperties.getNumberLength() : inputCaptchaProperties.getCharLength();
|
||||
CodeGenerator codeGenerator = ReflectUtils.newInstance(inputCaptchaType.getClazz(), length);
|
||||
AbstractCaptcha captcha = SpringUtils.getBean(inputCaptchaProperties.getCategory().getClazz());
|
||||
captcha.setGenerator(codeGenerator);
|
||||
captcha.createCode();
|
||||
// 如果是数学验证码,使用SpEL表达式处理验证码结果
|
||||
@ -133,4 +153,33 @@ public class CaptchaController {
|
||||
return R.ok(captchaVo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成行为验证码
|
||||
*/
|
||||
@RateLimiter(time = 60, count = 10, limitType = LimitType.IP)
|
||||
@PostMapping("/auth/captcha")
|
||||
public CaptchaResponse<ImageCaptchaVO> getCaptcha() {
|
||||
if (!captchaProperties.getEnable() || !CaptchaType.ACT.getType().equalsIgnoreCase(captchaProperties.getType())) {
|
||||
throw new ServiceException("验证码未开启");
|
||||
}
|
||||
String type = captchaProperties.getAct().getType();
|
||||
if ("RANDOM".equalsIgnoreCase(type)) {
|
||||
type = Arrays.asList(CaptchaTypeConstant.SLIDER, CaptchaTypeConstant.CONCAT, CaptchaTypeConstant.ROTATE, CaptchaTypeConstant.WORD_IMAGE_CLICK)
|
||||
.get(ThreadLocalRandom.current().nextInt(4));
|
||||
}
|
||||
return imageCaptchaApplication.generateCaptcha(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验行为验证码
|
||||
*/
|
||||
@PostMapping("/auth/verify")
|
||||
public ApiResponse<?> verify(@RequestBody CaptchaBo data) {
|
||||
ApiResponse<?> response = imageCaptchaApplication.matching(data.getId(), data.getData());
|
||||
if (response.isSuccess()) {
|
||||
return ApiResponse.ofSuccess(Collections.singletonMap("id", data.getId()));
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,13 @@
|
||||
package org.dromara.web.domain.bo;
|
||||
|
||||
import cloud.tianai.captcha.validator.common.model.dto.ImageCaptchaTrack;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class CaptchaBo {
|
||||
|
||||
private String id;
|
||||
|
||||
private ImageCaptchaTrack data;
|
||||
|
||||
}
|
@ -15,6 +15,11 @@ public class CaptchaVo {
|
||||
*/
|
||||
private Boolean captchaEnabled = true;
|
||||
|
||||
/**
|
||||
* 验证码类型
|
||||
*/
|
||||
private String type;
|
||||
|
||||
private String uuid;
|
||||
|
||||
/**
|
||||
|
@ -1,5 +1,7 @@
|
||||
package org.dromara.web.service.impl;
|
||||
|
||||
import cloud.tianai.captcha.application.ImageCaptchaApplication;
|
||||
import cloud.tianai.captcha.spring.plugins.secondary.SecondaryVerificationApplication;
|
||||
import cn.dev33.satoken.secure.BCrypt;
|
||||
import cn.dev33.satoken.stp.SaLoginModel;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
@ -24,6 +26,7 @@ import org.dromara.common.redis.utils.RedisUtils;
|
||||
import org.dromara.common.satoken.utils.LoginHelper;
|
||||
import org.dromara.common.tenant.helper.TenantHelper;
|
||||
import org.dromara.common.web.config.properties.CaptchaProperties;
|
||||
import org.dromara.common.web.enums.CaptchaType;
|
||||
import org.dromara.system.domain.SysUser;
|
||||
import org.dromara.system.domain.vo.SysClientVo;
|
||||
import org.dromara.system.domain.vo.SysUserVo;
|
||||
@ -43,6 +46,8 @@ import org.springframework.stereotype.Service;
|
||||
@RequiredArgsConstructor
|
||||
public class PasswordAuthStrategy implements IAuthStrategy {
|
||||
|
||||
private final ImageCaptchaApplication imageCaptchaApplication;
|
||||
|
||||
private final CaptchaProperties captchaProperties;
|
||||
private final SysLoginService loginService;
|
||||
private final SysUserMapper userMapper;
|
||||
@ -95,6 +100,13 @@ public class PasswordAuthStrategy implements IAuthStrategy {
|
||||
* @param uuid 唯一标识
|
||||
*/
|
||||
private void validateCaptcha(String tenantId, String username, String code, String uuid) {
|
||||
if (CaptchaType.ACT.getType().equalsIgnoreCase(captchaProperties.getType()) && imageCaptchaApplication instanceof SecondaryVerificationApplication) {
|
||||
if (!((SecondaryVerificationApplication) imageCaptchaApplication).secondaryVerification(uuid)) {
|
||||
loginService.recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error"));
|
||||
throw new CaptchaException();
|
||||
}
|
||||
return;
|
||||
}
|
||||
String verifyKey = GlobalConstants.CAPTCHA_CODE_KEY + StringUtils.blankToDefault(uuid, "");
|
||||
String captcha = RedisUtils.getCacheObject(verifyKey);
|
||||
RedisUtils.deleteObject(verifyKey);
|
||||
|
@ -7,18 +7,6 @@ ruoyi:
|
||||
# 版权年份
|
||||
copyrightYear: 2024
|
||||
|
||||
captcha:
|
||||
enable: true
|
||||
# 页面 <参数设置> 可开启关闭 验证码校验
|
||||
# 验证码类型 math 数组计算 char 字符验证
|
||||
type: MATH
|
||||
# line 线段干扰 circle 圆圈干扰 shear 扭曲干扰
|
||||
category: CIRCLE
|
||||
# 数字验证码位数
|
||||
numberLength: 1
|
||||
# 字符验证码长度
|
||||
charLength: 4
|
||||
|
||||
# 开发环境配置
|
||||
server:
|
||||
# 服务器的HTTP端口,默认为8080
|
||||
@ -109,6 +97,24 @@ sa-token:
|
||||
|
||||
# security配置
|
||||
security:
|
||||
#验证码
|
||||
captcha:
|
||||
# 是否开启验证码
|
||||
enable: true
|
||||
# 验证码类型 input 输入验证码 act 行为验证码
|
||||
type: act
|
||||
input:
|
||||
# 验证码类型 math 数组计算 char 字符验证
|
||||
type: char
|
||||
# line 线段干扰 circle 圆圈干扰 shear 扭曲干扰 random 随机行为验证码
|
||||
category: line
|
||||
# 数字验证码位数
|
||||
numberLength: 1
|
||||
# 字符验证码长度
|
||||
charLength: 4
|
||||
act:
|
||||
# RANDOM 随机 SLIDER 滑块验证 ROTATE 旋转验证 CONCAT 滑动还原 WORD_IMAGE_CLICK 文字点选
|
||||
type: RANDOM
|
||||
# 排除路径
|
||||
excludes:
|
||||
- /*.html
|
||||
@ -120,6 +126,37 @@ security:
|
||||
- /*/api-docs
|
||||
- /*/api-docs/**
|
||||
|
||||
# 滑块验证码配置, 详细请看 cloud.tianai.captcha.autoconfiguration.ImageCaptchaProperties 类
|
||||
captcha:
|
||||
# 如果项目中使用到了redis,滑块验证码会自动把验证码数据存到redis中, 这里配置redis的key的前缀,默认是captcha:slider
|
||||
prefix: "captcha_act:"
|
||||
# 验证码过期时间,默认是2分钟,单位毫秒, 可以根据自身业务进行调整
|
||||
expire:
|
||||
# 默认缓存时间 2分钟
|
||||
default: 20000
|
||||
# 针对 点选验证码 过期时间设置为 2分钟, 因为点选验证码验证比较慢,把过期时间调整大一些
|
||||
WORD_IMAGE_CLICK: 30000
|
||||
# 使用加载系统自带的资源, 默认是 false(这里系统的默认资源包含 滑动验证码模板/旋转验证码模板,如果想使用系统的模板,这里设置为true)
|
||||
init-default-resource: true
|
||||
# 缓存控制, 默认为false不开启
|
||||
local-cache-enabled: true
|
||||
# 验证码会提前缓存一些生成好的验证数据, 默认是20
|
||||
local-cache-size: 20
|
||||
# 缓存拉取失败后等待时间 默认是 5秒钟
|
||||
local-cache-wait-time: 5000
|
||||
# 缓存检查间隔 默认是2秒钟
|
||||
local-cache-period: 2000
|
||||
# 配置字体包,供文字点选验证码使用,可以配置多个,不配置使用默认的字体
|
||||
font-path:
|
||||
# - classpath:font/SimHei.ttf
|
||||
secondary:
|
||||
# 二次验证, 默认false 不开启
|
||||
enabled: true
|
||||
# 二次验证过期时间, 默认 2分钟
|
||||
expire: 120000
|
||||
# 二次验证缓存key前缀,默认是 captcha:secondary
|
||||
keyPrefix: "captcha_secondary:"
|
||||
|
||||
# 多租户配置
|
||||
tenant:
|
||||
# 是否开启
|
||||
|
BIN
ruoyi-admin/src/main/resources/bgimages/1.jpg
Normal file
BIN
ruoyi-admin/src/main/resources/bgimages/1.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 71 KiB |
BIN
ruoyi-admin/src/main/resources/bgimages/2.jpg
Normal file
BIN
ruoyi-admin/src/main/resources/bgimages/2.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 106 KiB |
BIN
ruoyi-admin/src/main/resources/bgimages/3.jpg
Normal file
BIN
ruoyi-admin/src/main/resources/bgimages/3.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 158 KiB |
BIN
ruoyi-admin/src/main/resources/bgimages/4.jpg
Normal file
BIN
ruoyi-admin/src/main/resources/bgimages/4.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 46 KiB |
BIN
ruoyi-admin/src/main/resources/bgimages/5.jpg
Normal file
BIN
ruoyi-admin/src/main/resources/bgimages/5.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 55 KiB |
@ -57,6 +57,12 @@
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-crypto</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>cloud.tianai.captcha</groupId>
|
||||
<artifactId>tianai-captcha-springboot-starter</artifactId>
|
||||
<version>${tianai-captcha.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
@ -0,0 +1,44 @@
|
||||
package org.dromara.common.web.config;
|
||||
|
||||
import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
|
||||
import cloud.tianai.captcha.resource.ResourceStore;
|
||||
import cloud.tianai.captcha.resource.common.model.dto.Resource;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
public class CaptchaResourceConfig {
|
||||
|
||||
private final ResourceStore resourceStore;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
// 添加自定义背景图片
|
||||
resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/1.jpg", "default"));
|
||||
resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/2.jpg", "default"));
|
||||
resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/3.jpg", "default"));
|
||||
resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/4.jpg", "default"));
|
||||
resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/5.jpg", "default"));
|
||||
|
||||
resourceStore.addResource(CaptchaTypeConstant.ROTATE, new Resource("classpath", "bgimages/1.jpg", "default"));
|
||||
resourceStore.addResource(CaptchaTypeConstant.ROTATE, new Resource("classpath", "bgimages/2.jpg", "default"));
|
||||
resourceStore.addResource(CaptchaTypeConstant.ROTATE, new Resource("classpath", "bgimages/3.jpg", "default"));
|
||||
resourceStore.addResource(CaptchaTypeConstant.ROTATE, new Resource("classpath", "bgimages/4.jpg", "default"));
|
||||
resourceStore.addResource(CaptchaTypeConstant.ROTATE, new Resource("classpath", "bgimages/5.jpg", "default"));
|
||||
|
||||
resourceStore.addResource(CaptchaTypeConstant.CONCAT, new Resource("classpath", "bgimages/1.jpg", "default"));
|
||||
resourceStore.addResource(CaptchaTypeConstant.CONCAT, new Resource("classpath", "bgimages/2.jpg", "default"));
|
||||
resourceStore.addResource(CaptchaTypeConstant.CONCAT, new Resource("classpath", "bgimages/3.jpg", "default"));
|
||||
resourceStore.addResource(CaptchaTypeConstant.CONCAT, new Resource("classpath", "bgimages/4.jpg", "default"));
|
||||
resourceStore.addResource(CaptchaTypeConstant.CONCAT, new Resource("classpath", "bgimages/5.jpg", "default"));
|
||||
|
||||
resourceStore.addResource(CaptchaTypeConstant.WORD_IMAGE_CLICK, new Resource("classpath", "bgimages/1.jpg", "default"));
|
||||
resourceStore.addResource(CaptchaTypeConstant.WORD_IMAGE_CLICK, new Resource("classpath", "bgimages/2.jpg", "default"));
|
||||
resourceStore.addResource(CaptchaTypeConstant.WORD_IMAGE_CLICK, new Resource("classpath", "bgimages/3.jpg", "default"));
|
||||
resourceStore.addResource(CaptchaTypeConstant.WORD_IMAGE_CLICK, new Resource("classpath", "bgimages/4.jpg", "default"));
|
||||
resourceStore.addResource(CaptchaTypeConstant.WORD_IMAGE_CLICK, new Resource("classpath", "bgimages/5.jpg", "default"));
|
||||
}
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
package org.dromara.common.web.config.properties;
|
||||
|
||||
import org.dromara.common.web.enums.CaptchaCategory;
|
||||
import org.dromara.common.web.enums.CaptchaType;
|
||||
import lombok.Data;
|
||||
import org.dromara.common.web.enums.InputCaptchaType;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/**
|
||||
@ -11,7 +11,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
* @author Lion Li
|
||||
*/
|
||||
@Data
|
||||
@ConfigurationProperties(prefix = "captcha")
|
||||
@ConfigurationProperties(prefix = "security.captcha")
|
||||
public class CaptchaProperties {
|
||||
|
||||
private Boolean enable;
|
||||
@ -19,20 +19,48 @@ public class CaptchaProperties {
|
||||
/**
|
||||
* 验证码类型
|
||||
*/
|
||||
private CaptchaType type;
|
||||
private String type;
|
||||
|
||||
/**
|
||||
* 验证码类别
|
||||
* 输入验证码的相关配置
|
||||
*/
|
||||
private CaptchaCategory category;
|
||||
private InputCaptchaProperties input;
|
||||
|
||||
/**
|
||||
* 数字验证码位数
|
||||
* 行为验证码的相关配置
|
||||
*/
|
||||
private Integer numberLength;
|
||||
private ActCaptchaProperties act;
|
||||
|
||||
/**
|
||||
* 字符验证码长度
|
||||
*/
|
||||
private Integer charLength;
|
||||
@Data
|
||||
public static class InputCaptchaProperties {
|
||||
|
||||
/**
|
||||
* 验证码类型 math 数组计算, char 字符验证
|
||||
*/
|
||||
private InputCaptchaType type;
|
||||
|
||||
/**
|
||||
* 验证码类别 line 线段干扰, circle 圆圈干扰, shear 扭曲干扰, random 随机行为验证码
|
||||
*/
|
||||
private CaptchaCategory category;
|
||||
|
||||
/**
|
||||
* 数字验证码位数
|
||||
*/
|
||||
private Integer numberLength;
|
||||
|
||||
/**
|
||||
* 字符验证码长度
|
||||
*/
|
||||
private Integer charLength;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class ActCaptchaProperties {
|
||||
|
||||
/**
|
||||
* 行为验证码类型 random 随机行为验证码
|
||||
*/
|
||||
private String type;
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,5 @@
|
||||
package org.dromara.common.web.enums;
|
||||
|
||||
import cn.hutool.captcha.generator.CodeGenerator;
|
||||
import cn.hutool.captcha.generator.RandomGenerator;
|
||||
import org.dromara.common.web.utils.UnsignedMathGenerator;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@ -18,12 +15,12 @@ public enum CaptchaType {
|
||||
/**
|
||||
* 数字
|
||||
*/
|
||||
MATH(UnsignedMathGenerator.class),
|
||||
INPUT("INPUT"),
|
||||
|
||||
/**
|
||||
* 字符
|
||||
*/
|
||||
CHAR(RandomGenerator.class);
|
||||
ACT("ACT");
|
||||
|
||||
private final Class<? extends CodeGenerator> clazz;
|
||||
private final String type;
|
||||
}
|
||||
|
@ -0,0 +1,29 @@
|
||||
package org.dromara.common.web.enums;
|
||||
|
||||
import cn.hutool.captcha.generator.CodeGenerator;
|
||||
import cn.hutool.captcha.generator.RandomGenerator;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import org.dromara.common.web.utils.UnsignedMathGenerator;
|
||||
|
||||
/**
|
||||
* 验证码类型
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum InputCaptchaType {
|
||||
|
||||
/**
|
||||
* 数字
|
||||
*/
|
||||
MATH(UnsignedMathGenerator.class),
|
||||
|
||||
/**
|
||||
* 字符
|
||||
*/
|
||||
CHAR(RandomGenerator.class);
|
||||
|
||||
private final Class<? extends CodeGenerator> clazz;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user