bindMobile(@Valid @RequestBody WxMaBindMobileReq req) {
AuthenticationToken result = wxMaAuthService.bindMobile(req.getOpenid(), req.getMobile(), req.getSmsCode());
return Result.success(result);
diff --git a/src/main/java/com/youlai/boot/common/annotation/RateLimit.java b/src/main/java/com/youlai/boot/common/annotation/RateLimit.java
new file mode 100644
index 00000000..d8e9dfc7
--- /dev/null
+++ b/src/main/java/com/youlai/boot/common/annotation/RateLimit.java
@@ -0,0 +1,22 @@
+package com.youlai.boot.common.annotation;
+
+import java.lang.annotation.*;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 接口限流
+ * 标注在 Controller 方法上,基于 Redis 计数窗口实现
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface RateLimit {
+
+ int limit() default 0;
+
+ int window() default 0;
+
+ TimeUnit timeUnit() default TimeUnit.SECONDS;
+
+ String prefix() default "rate_limit:";
+}
diff --git a/src/main/java/com/youlai/boot/framework/captcha/service/CaptchaService.java b/src/main/java/com/youlai/boot/framework/captcha/service/CaptchaService.java
index 05bdbbf2..6e9d9859 100644
--- a/src/main/java/com/youlai/boot/framework/captcha/service/CaptchaService.java
+++ b/src/main/java/com/youlai/boot/framework/captcha/service/CaptchaService.java
@@ -71,7 +71,6 @@ public class CaptchaService {
return CaptchaInfo.builder()
.captchaId(captchaId)
.captchaBase64(imageBase64Data)
- .captchaCode(captchaCode)
.build();
}
diff --git a/src/main/java/com/youlai/boot/framework/web/aspect/RateLimitAspect.java b/src/main/java/com/youlai/boot/framework/web/aspect/RateLimitAspect.java
new file mode 100644
index 00000000..4276a604
--- /dev/null
+++ b/src/main/java/com/youlai/boot/framework/web/aspect/RateLimitAspect.java
@@ -0,0 +1,69 @@
+package com.youlai.boot.framework.web.aspect;
+
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.crypto.digest.DigestUtil;
+import com.youlai.boot.common.annotation.RateLimit;
+import com.youlai.boot.common.constant.RedisConstants;
+import com.youlai.boot.common.constant.SecurityConstants;
+import com.youlai.boot.common.exception.BusinessException;
+import com.youlai.boot.common.result.ResultCode;
+import com.youlai.boot.common.util.IPUtils;
+import com.youlai.boot.framework.web.config.RateLimitProperties;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.http.HttpHeaders;
+import org.springframework.stereotype.Component;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 接口限流切面
+ * 对 @RateLimit 标注的方法做 Redis 计数,超阈值则拒绝
+ */
+@Aspect
+@Component
+@RequiredArgsConstructor
+@Slf4j
+public class RateLimitAspect {
+
+ private final RedisTemplate redisTemplate;
+ private final RateLimitProperties rateLimitProperties;
+
+ @Around("@annotation(rateLimit)")
+ public Object handle(ProceedingJoinPoint jp, RateLimit rateLimit) throws Throwable {
+ HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
+ String key = buildKey(request, rateLimit);
+
+ int limit = rateLimit.limit() > 0 ? rateLimit.limit() : rateLimitProperties.getDefaultLimit();
+ Long count = redisTemplate.opsForValue().increment(key);
+ if (count != null && count == 1) {
+ int window = rateLimit.window() > 0 ? rateLimit.window() : rateLimitProperties.getDefaultWindow();
+ redisTemplate.expire(key, window, rateLimit.timeUnit());
+ }
+ if (count != null && count > limit) {
+ log.warn("限流触发 key={} count={} limit={}", key, count, limit);
+ throw new BusinessException(ResultCode.REQUEST_CONCURRENCY_LIMIT_EXCEEDED);
+ }
+ return jp.proceed();
+ }
+
+ private String buildKey(HttpServletRequest request, RateLimit rateLimit) {
+ String user = resolveUser(request);
+ return StrUtil.format(RedisConstants.RateLimiter.API, rateLimit.prefix(), user, request.getRequestURI());
+ }
+
+ private String resolveUser(HttpServletRequest request) {
+ String header = request.getHeader(HttpHeaders.AUTHORIZATION);
+ if (StrUtil.isNotBlank(header) && header.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) {
+ return DigestUtil.sha256Hex(header.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length()));
+ }
+ return IPUtils.getIpAddr(request);
+ }
+}
diff --git a/src/main/java/com/youlai/boot/framework/web/config/RateLimitProperties.java b/src/main/java/com/youlai/boot/framework/web/config/RateLimitProperties.java
new file mode 100644
index 00000000..6ea46c6d
--- /dev/null
+++ b/src/main/java/com/youlai/boot/framework/web/config/RateLimitProperties.java
@@ -0,0 +1,18 @@
+package com.youlai.boot.framework.web.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+/**
+ * 限流配置
+ */
+@Data
+@Component
+@ConfigurationProperties(prefix = "rate-limit")
+public class RateLimitProperties {
+
+ private int defaultLimit = 5;
+
+ private int defaultWindow = 1;
+}
diff --git a/src/main/java/com/youlai/boot/framework/web/util/ResponseWriter.java b/src/main/java/com/youlai/boot/framework/web/util/ResponseWriter.java
index a2573451..02f972ef 100644
--- a/src/main/java/com/youlai/boot/framework/web/util/ResponseWriter.java
+++ b/src/main/java/com/youlai/boot/framework/web/util/ResponseWriter.java
@@ -2,6 +2,8 @@ package com.youlai.boot.framework.web.util;
import cn.hutool.extra.servlet.JakartaServletUtil;
import cn.hutool.json.JSONUtil;
+import com.youlai.boot.common.result.Result;
+import com.youlai.boot.common.result.ResultCode;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml
index c002ae7b..fd91e7e5 100644
--- a/src/main/resources/application-dev.yml
+++ b/src/main/resources/application-dev.yml
@@ -5,9 +5,9 @@ spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: org.postgresql.Driver
- url: jdbc:postgresql://localhost:5432/youlai_admin?currentSchema=public&stringtype=unspecified
+ url: jdbc:postgresql://www.youlai.tech:15432/youlai_admin?currentSchema=public&stringtype=unspecified
username: postgres
- password: postgres
+ password: Q1w2e3r4
data:
redis:
@@ -185,6 +185,11 @@ xxl:
logpath: /data/applogs/xxl-job/jobhandler
logretentiondays: 30
+# 限流配置
+rate-limit:
+ default-limit: 5
+ default-window: 1
+
# 验证码配置
captcha:
# 验证码类型 circle-圆圈干扰验证码|gif-Gif验证码|line-干扰线验证码|shear-扭曲干扰验证码