diff --git a/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/core/OssClient.java b/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/core/OssClient.java index ba924c0b..99bc294e 100644 --- a/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/core/OssClient.java +++ b/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/core/OssClient.java @@ -8,8 +8,7 @@ import org.dromara.common.core.utils.StringUtils; import org.dromara.common.core.utils.file.FileUtils; import org.dromara.common.oss.constant.OssConstant; import org.dromara.common.oss.entity.UploadResult; -import org.dromara.common.oss.enumd.AccessPolicyType; -import org.dromara.common.oss.enumd.PolicyType; +import org.dromara.common.oss.enums.AccessPolicyType; import org.dromara.common.oss.exception.OssException; import org.dromara.common.oss.properties.OssProperties; import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; @@ -17,13 +16,11 @@ import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; import software.amazon.awssdk.core.ResponseInputStream; import software.amazon.awssdk.core.async.AsyncResponseTransformer; import software.amazon.awssdk.core.async.BlockingInputStreamAsyncRequestBody; +import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.s3.S3AsyncClient; import software.amazon.awssdk.services.s3.S3Configuration; -import software.amazon.awssdk.services.s3.crt.S3CrtHttpConfiguration; import software.amazon.awssdk.services.s3.model.GetObjectResponse; -import software.amazon.awssdk.services.s3.model.NoSuchBucketException; -import software.amazon.awssdk.services.s3.model.S3Exception; import software.amazon.awssdk.services.s3.presigner.S3Presigner; import software.amazon.awssdk.transfer.s3.S3TransferManager; import software.amazon.awssdk.transfer.s3.model.*; @@ -35,6 +32,7 @@ import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.time.Duration; +import java.util.function.Consumer; /** * S3 存储协议 所有兼容S3协议的云厂商均支持 @@ -86,18 +84,14 @@ public class OssClient { // MinIO 使用 HTTPS 限制使用域名访问,站点填域名。需要启用路径样式访问 boolean isStyle = !StringUtils.containsAny(properties.getEndpoint(), OssConstant.CLOUD_SERVICE); - // 创建AWS基于 CRT 的 S3 客户端 - this.client = S3AsyncClient.crtBuilder() + // 创建AWS基于 Netty 的 S3 客户端 + this.client = S3AsyncClient.builder() .credentialsProvider(credentialsProvider) .endpointOverride(URI.create(getEndpoint())) .region(of()) - .targetThroughputInGbps(20.0) - .minimumPartSizeInBytes(10 * 1025 * 1024L) - .checksumValidationEnabled(false) .forcePathStyle(isStyle) - .httpConfiguration(S3CrtHttpConfiguration.builder() - .connectionTimeout(Duration.ofSeconds(60)) // 设置连接超时 - .build()) + .httpClient(NettyNioAsyncHttpClient.builder() + .connectionTimeout(Duration.ofSeconds(60)).build()) .build(); //AWS基于 CRT 的 S3 AsyncClient 实例用作 S3 传输管理器的底层客户端 @@ -115,8 +109,6 @@ public class OssClient { .serviceConfiguration(config) .build(); - // 创建存储桶 - createBucket(); } catch (Exception e) { if (e instanceof OssException) { throw e; @@ -125,43 +117,6 @@ public class OssClient { } } - /** - * 同步创建存储桶 - * 如果存储桶不存在,会进行创建;如果存储桶存在,不执行任何操作 - * - * @throws OssException 当创建存储桶时发生异常时抛出 - */ - public void createBucket() { - String bucketName = properties.getBucketName(); - try { - // 尝试获取存储桶的信息 - client.headBucket( - x -> x.bucket(bucketName) - .build()) - .join(); - } catch (Exception ex) { - if (ex.getCause() instanceof NoSuchBucketException) { - try { - // 存储桶不存在,尝试创建存储桶 - client.createBucket( - x -> x.bucket(bucketName)) - .join(); - - // 设置存储桶的访问策略(Bucket Policy) - client.putBucketPolicy( - x -> x.bucket(bucketName) - .policy(getPolicy(bucketName, getAccessPolicy().getPolicyType()))) - .join(); - } catch (S3Exception e) { - // 存储桶创建或策略设置失败 - throw new OssException("创建Bucket失败, 请核对配置信息:[" + e.getMessage() + "]"); - } - } else { - throw new OssException("判断Bucket是否存在失败,请核对配置信息:[" + ex.getMessage() + "]"); - } - } - } - /** * 上传文件到 Amazon S3,并返回上传结果 * @@ -284,7 +239,7 @@ public class OssClient { * @return 输出流中写入的字节数(长度) * @throws OssException 如果下载失败,抛出自定义异常 */ - public long download(String key, OutputStream out) { + public void download(String key, OutputStream out, Consumer consumer) { try { // 构建下载请求 DownloadRequest> downloadRequest = DownloadRequest.builder() @@ -300,7 +255,10 @@ public class OssClient { Download> responseFuture = transferManager.download(downloadRequest); // 输出到流中 try (ResponseInputStream responseStream = responseFuture.completionFuture().join().result()) { // auto-closeable stream - return responseStream.transferTo(out); // 阻塞调用线程 blocks the calling thread + if (consumer != null) { + consumer.accept(responseStream.response().contentLength()); + } + responseStream.transferTo(out); // 阻塞调用线程 blocks the calling thread } } catch (Exception e) { throw new OssException("文件下载失败,错误信息:[" + e.getMessage() + "]"); @@ -326,13 +284,13 @@ public class OssClient { /** * 获取私有URL链接 * - * @param objectKey 对象KEY - * @param second 授权时间 + * @param objectKey 对象KEY + * @param expiredTime 链接授权到期时间 */ - public String getPrivateUrl(String objectKey, Integer second) { + public String getPrivateUrl(String objectKey, Duration expiredTime) { // 使用 AWS S3 预签名 URL 的生成器 获取对象的预签名 URL URL url = presigner.presignGetObject( - x -> x.signatureDuration(Duration.ofSeconds(second)) + x -> x.signatureDuration(expiredTime) .getObjectRequest( y -> y.bucket(properties.getBucketName()) .key(objectKey) @@ -529,77 +487,4 @@ public class OssClient { return AccessPolicyType.getByType(properties.getAccessPolicy()); } - /** - * 生成 AWS S3 存储桶访问策略 - * - * @param bucketName 存储桶 - * @param policyType 桶策略类型 - * @return 符合 AWS S3 存储桶访问策略格式的字符串 - */ - private static String getPolicy(String bucketName, PolicyType policyType) { - String policy = switch (policyType) { - case WRITE -> """ - { - "Version": "2012-10-17", - "Statement": [] - } - """; - case READ_WRITE -> """ - { - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Principal": "*", - "Action": [ - "s3:GetBucketLocation", - "s3:ListBucket", - "s3:ListBucketMultipartUploads" - ], - "Resource": "arn:aws:s3:::bucketName" - }, - { - "Effect": "Allow", - "Principal": "*", - "Action": [ - "s3:AbortMultipartUpload", - "s3:DeleteObject", - "s3:GetObject", - "s3:ListMultipartUploadParts", - "s3:PutObject" - ], - "Resource": "arn:aws:s3:::bucketName/*" - } - ] - } - """; - case READ -> """ - { - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Principal": "*", - "Action": ["s3:GetBucketLocation"], - "Resource": "arn:aws:s3:::bucketName" - }, - { - "Effect": "Deny", - "Principal": "*", - "Action": ["s3:ListBucket"], - "Resource": "arn:aws:s3:::bucketName" - }, - { - "Effect": "Allow", - "Principal": "*", - "Action": "s3:GetObject", - "Resource": "arn:aws:s3:::bucketName/*" - } - ] - } - """; - }; - return policy.replaceAll("bucketName", bucketName); - } - } diff --git a/ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/utils/RedisUtils.java b/ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/utils/RedisUtils.java index 6fa3b748..355cd293 100644 --- a/ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/utils/RedisUtils.java +++ b/ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/utils/RedisUtils.java @@ -4,6 +4,7 @@ import lombok.AccessLevel; import lombok.NoArgsConstructor; import org.dromara.common.core.utils.SpringUtils; import org.redisson.api.*; +import org.redisson.api.options.KeysScanOptions; import java.time.Duration; import java.util.Collection; @@ -36,8 +37,22 @@ public class RedisUtils { * @return -1 表示失败 */ public static long rateLimiter(String key, RateType rateType, int rate, int rateInterval) { + return rateLimiter(key, rateType, rate, rateInterval, 0); + } + + /** + * 限流 + * + * @param key 限流key + * @param rateType 限流类型 + * @param rate 速率 + * @param rateInterval 速率间隔 + * @param timeout 超时时间 + * @return -1 表示失败 + */ + public static long rateLimiter(String key, RateType rateType, int rate, int rateInterval, int timeout) { RRateLimiter rateLimiter = CLIENT.getRateLimiter(key); - rateLimiter.trySetRate(rateType, rate, rateInterval, RateIntervalUnit.SECONDS); + rateLimiter.trySetRate(rateType, rate, Duration.ofSeconds(rateInterval), Duration.ofSeconds(timeout)); if (rateLimiter.tryAcquire()) { return rateLimiter.availablePermits(); } else { @@ -518,13 +533,34 @@ public class RedisUtils { /** * 获得缓存的基本对象列表(全局匹配忽略租户 自行拼接租户id) - * + *

+ * limit-设置扫描的限制数量(默认为0,查询全部) + * pattern-设置键的匹配模式(默认为null) + * chunkSize-设置每次扫描的块大小(默认为0,本方法设置为1000) + * type-设置键的类型(默认为null,查询全部类型) + *

+ * @see KeysScanOptions * @param pattern 字符串前缀 * @return 对象列表 */ public static Collection keys(final String pattern) { - Stream stream = CLIENT.getKeys().getKeysStreamByPattern(pattern); - return stream.collect(Collectors.toList()); + return keys(KeysScanOptions.defaults().pattern(pattern).chunkSize(1000)); + } + + /** + * 通过扫描参数获取缓存的基本对象列表 + * @param keysScanOptions 扫描参数 + *

+ * limit-设置扫描的限制数量(默认为0,查询全部) + * pattern-设置键的匹配模式(默认为null) + * chunkSize-设置每次扫描的块大小(默认为0) + * type-设置键的类型(默认为null,查询全部类型) + *

+ * @see KeysScanOptions + */ + public static Collection keys(final KeysScanOptions keysScanOptions) { + Stream keysStream = CLIENT.getKeys().getKeysStream(keysScanOptions); + return keysStream.collect(Collectors.toList()); } /** diff --git a/ruoyi-modules/ruoyi-resource/src/main/java/org/dromara/resource/controller/SysEmailController.java b/ruoyi-modules/ruoyi-resource/src/main/java/org/dromara/resource/controller/SysEmailController.java index 19da5ba6..01c72776 100644 --- a/ruoyi-modules/ruoyi-resource/src/main/java/org/dromara/resource/controller/SysEmailController.java +++ b/ruoyi-modules/ruoyi-resource/src/main/java/org/dromara/resource/controller/SysEmailController.java @@ -7,6 +7,8 @@ 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.ratelimiter.annotation.RateLimiter; import org.dromara.common.web.core.BaseController; import org.dromara.common.mail.config.properties.MailProperties; @@ -39,12 +41,21 @@ public class SysEmailController extends BaseController { * * @param email 邮箱 */ - @RateLimiter(key = "#email", time = 60, count = 1) @GetMapping("/code") public R emailCode(@NotBlank(message = "{user.email.not.blank}") String email) { if (!mailProperties.getEnabled()) { return R.fail("当前系统没有开启邮箱功能!"); } + SpringUtils.getAopProxy(this).emailCodeImpl(email); + return R.ok(); + } + + /** + * 邮箱验证码 + * 独立方法避免验证码关闭之后仍然走限流 + */ + @RateLimiter(key = "#email", time = 60, count = 1) + public void emailCodeImpl(String email) { String key = GlobalConstants.CAPTCHA_CODE_KEY + email; String code = RandomUtil.randomNumbers(4); RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION)); @@ -52,9 +63,8 @@ public class SysEmailController extends BaseController { MailUtils.sendText(email, "登录验证码", "您本次验证码为:" + code + ",有效性为" + Constants.CAPTCHA_EXPIRATION + "分钟,请尽快填写。"); } catch (Exception e) { log.error("验证码短信发送异常 => {}", e.getMessage()); - return R.fail(e.getMessage()); + throw new ServiceException(e.getMessage()); } - return R.ok(); } } diff --git a/ruoyi-modules/ruoyi-resource/src/main/java/org/dromara/resource/service/impl/SysOssServiceImpl.java b/ruoyi-modules/ruoyi-resource/src/main/java/org/dromara/resource/service/impl/SysOssServiceImpl.java index ab3ddc6e..2f4a0478 100644 --- a/ruoyi-modules/ruoyi-resource/src/main/java/org/dromara/resource/service/impl/SysOssServiceImpl.java +++ b/ruoyi-modules/ruoyi-resource/src/main/java/org/dromara/resource/service/impl/SysOssServiceImpl.java @@ -18,7 +18,7 @@ import org.dromara.common.mybatis.core.page.PageQuery; import org.dromara.common.mybatis.core.page.TableDataInfo; import org.dromara.common.oss.core.OssClient; import org.dromara.common.oss.entity.UploadResult; -import org.dromara.common.oss.enumd.AccessPolicyType; +import org.dromara.common.oss.enums.AccessPolicyType; import org.dromara.common.oss.factory.OssFactory; import org.dromara.resource.domain.SysOss; import org.dromara.resource.domain.bo.SysOssBo; @@ -32,6 +32,7 @@ import org.springframework.web.multipart.MultipartFile; import java.io.File; import java.io.IOException; +import java.time.Duration; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -155,8 +156,7 @@ public class SysOssServiceImpl implements ISysOssService { FileUtils.setAttachmentResponseHeader(response, sysOss.getOriginalName()); response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE + "; charset=UTF-8"); OssClient storage = OssFactory.instance(sysOss.getService()); - long contentLength = storage.download(sysOss.getFileName(), response.getOutputStream()); - response.setContentLengthLong(contentLength); + storage.download(sysOss.getFileName(), response.getOutputStream(), response::setContentLengthLong); } /** @@ -255,7 +255,7 @@ public class SysOssServiceImpl implements ISysOssService { OssClient storage = OssFactory.instance(oss.getService()); // 仅修改桶类型为 private 的URL,临时URL时长为120s if (AccessPolicyType.PRIVATE == storage.getAccessPolicy()) { - oss.setUrl(storage.getPrivateUrl(oss.getFileName(), 120)); + oss.setUrl(storage.getPrivateUrl(oss.getFileName(), Duration.ofSeconds(120))); } return oss; }