diff --git a/core/api/src/main/java/com/wansenai/api/RateLimitException.java b/core/api/src/main/java/com/wansenai/api/RateLimitException.java new file mode 100644 index 00000000..c880c1bc --- /dev/null +++ b/core/api/src/main/java/com/wansenai/api/RateLimitException.java @@ -0,0 +1,25 @@ +/* + * Copyright 2023-2033 WanSen AI Team, Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://opensource.wansenai.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +package com.wansenai.api; + + +public class RateLimitException extends RuntimeException { + private String errorCode; + private String errorMessage; + + public RateLimitException(String errorCode, String errorMessage) { + super(errorMessage); + this.errorCode = errorCode; + this.errorMessage = errorMessage; + } +} diff --git a/core/api/src/main/java/com/wansenai/api/common/CommonController.java b/core/api/src/main/java/com/wansenai/api/common/CommonController.java index 5d68bb73..c50e5e85 100644 --- a/core/api/src/main/java/com/wansenai/api/common/CommonController.java +++ b/core/api/src/main/java/com/wansenai/api/common/CommonController.java @@ -12,8 +12,11 @@ */ package com.wansenai.api.common; +import com.wansenai.api.RateLimitException; +import com.wansenai.api.config.RateLimiter; import com.wansenai.service.common.CommonService; import com.wansenai.utils.ExcelUtil; +import com.wansenai.utils.enums.LimitType; import com.wansenai.utils.response.Response; import com.wansenai.utils.constants.ApiVersionConstants; import com.wansenai.utils.enums.BaseCodeEnum; @@ -46,7 +49,13 @@ public Response getCaptcha() { return Response.responseData(captchaVo); } + @ExceptionHandler(RateLimitException.class) + public Response handleRateLimitException(RateLimitException ex) { + return Response.responseMsg(BaseCodeEnum.FREQUENT_SYSTEM_ACCESS); + } + @GetMapping("sms/{type}/{phoneNumber}") + @RateLimiter(key = "sms", time = 120, count = 1, limitType = LimitType.IP) public Response sendSmsCode(@PathVariable Integer type, @PathVariable String phoneNumber) { boolean result = commonService.sendSmsCode(type, phoneNumber); if(!result) { diff --git a/core/api/src/main/java/com/wansenai/api/config/RateLimiter.java b/core/api/src/main/java/com/wansenai/api/config/RateLimiter.java new file mode 100644 index 00000000..cd71025b --- /dev/null +++ b/core/api/src/main/java/com/wansenai/api/config/RateLimiter.java @@ -0,0 +1,30 @@ +package com.wansenai.api.config; + +import com.wansenai.utils.enums.LimitType; + +import java.lang.annotation.*; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RateLimiter { + /** + * 限流key + */ + String key() default "rate_limit:"; + + /** + * 限流时间,单位秒 + */ + int time() default 60; + + /** + * 限流次数 + */ + int count() default 100; + + /** + * 限流类型 + */ + LimitType limitType() default LimitType.DEFAULT; +} diff --git a/core/api/src/main/java/com/wansenai/api/config/RateLimiterAspect.java b/core/api/src/main/java/com/wansenai/api/config/RateLimiterAspect.java new file mode 100644 index 00000000..ff6a6390 --- /dev/null +++ b/core/api/src/main/java/com/wansenai/api/config/RateLimiterAspect.java @@ -0,0 +1,88 @@ +/* + * Copyright 2023-2033 WanSen AI Team, Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://opensource.wansenai.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +package com.wansenai.api.config; + +import com.wansenai.api.RateLimitException; +import com.wansenai.utils.IpUtils; +import com.wansenai.utils.enums.BaseCodeEnum; +import com.wansenai.utils.enums.LimitType; +import com.wansenai.utils.redis.RedisUtil; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.List; + +@Component +@Aspect +@Slf4j +public class RateLimiterAspect { + + private final RedisUtil redisUtil; + + public RateLimiterAspect(RedisUtil redisUtil) { + this.redisUtil = redisUtil; + } + + + @Before("@annotation(rateLimiter)") + public void doBefore(JoinPoint point, RateLimiter rateLimiter) { + String key = rateLimiter.key(); + int time = rateLimiter.time(); + int count = rateLimiter.count(); + + String combineKey = getCombineKey(rateLimiter, point); + List keys = Collections.singletonList(combineKey); + try { + if (redisUtil.get(keys.get(0)) != null) { + long number = redisUtil.incr(keys.get(0), 1); + if ((int) number > count) { + throw new RateLimitException(BaseCodeEnum.FREQUENT_SYSTEM_ACCESS.getCode(), BaseCodeEnum.FREQUENT_SYSTEM_ACCESS.getMsg()); + } + log.info("限制请求'{}',当前请求'{}',缓存key'{}'", count, (int) number, keys.get(0)); + } else { + redisUtil.set(keys.get(0), 1, time); + } + } catch (Exception e) { + throw new RateLimitException(BaseCodeEnum.SYSTEM_BUSY.getCode(), BaseCodeEnum.SYSTEM_BUSY.getMsg()); + } + } + + /** + * 获取ip为key + * @param rateLimiter + * @param point + * @return + */ + public String getCombineKey(RateLimiter rateLimiter, JoinPoint point) { + StringBuilder stringBuffer = new StringBuilder(rateLimiter.key()); + if (rateLimiter.limitType() == LimitType.IP) { + stringBuffer.append( + IpUtils.getIpAddr(((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()) + .getRequest())) + .append("-"); + } + MethodSignature signature = (MethodSignature) point.getSignature(); + Method method = signature.getMethod(); + Class targetClass = method.getDeclaringClass(); + stringBuffer.append(targetClass.getName()).append("-").append(method.getName()); + return stringBuffer.toString(); + } +} diff --git a/core/pom.xml b/core/pom.xml index 6a13bc20..88c35192 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -33,6 +33,10 @@ + + org.springframework.boot + spring-boot-starter-aop + org.springframework.boot spring-boot-starter diff --git a/core/utils/src/main/java/com/wansenai/utils/IpUtils.java b/core/utils/src/main/java/com/wansenai/utils/IpUtils.java new file mode 100644 index 00000000..57e2830d --- /dev/null +++ b/core/utils/src/main/java/com/wansenai/utils/IpUtils.java @@ -0,0 +1,59 @@ +/* + * Copyright 2023-2033 WanSen AI Team, Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://opensource.wansenai.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +package com.wansenai.utils; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +@Slf4j +public class IpUtils { + public static String getIpAddr(HttpServletRequest request) { + String ipAddress = null; + try { + ipAddress = request.getHeader("x-forwarded-for"); + if (ipAddress == null || ipAddress.isEmpty() || "unknown".equalsIgnoreCase(ipAddress)) { + ipAddress = request.getHeader("Proxy-Client-IP"); + } + if (ipAddress == null || ipAddress.isEmpty() || "unknown".equalsIgnoreCase(ipAddress)) { + ipAddress = request.getHeader("WL-Proxy-Client-IP"); + } + if (ipAddress == null || ipAddress.isEmpty() || "unknown".equalsIgnoreCase(ipAddress)) { + ipAddress = request.getRemoteAddr(); + if (ipAddress.equals("127.0.0.1")) { + // 根据网卡取本机配置的IP + try { + ipAddress = InetAddress.getLocalHost().getHostAddress(); + } catch (UnknownHostException e) { + log.error("获取IP地址异常:" + e.getMessage()); + } + } + } + // 通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割 + if (ipAddress != null) { + if (ipAddress.contains(",")) { + return ipAddress.split(",")[0]; + } else { + return ipAddress; + } + } else { + return ""; + } + } catch (Exception e) { + log.error("获取IP地址异常:" + e.getMessage()); + return ""; + } + } +} diff --git a/core/utils/src/main/java/com/wansenai/utils/enums/BaseCodeEnum.java b/core/utils/src/main/java/com/wansenai/utils/enums/BaseCodeEnum.java index b7a9b4fd..529215a6 100644 --- a/core/utils/src/main/java/com/wansenai/utils/enums/BaseCodeEnum.java +++ b/core/utils/src/main/java/com/wansenai/utils/enums/BaseCodeEnum.java @@ -57,7 +57,11 @@ public enum BaseCodeEnum { OSS_GET_INSTANCE_ERROR("T0501", "腾讯云OSS对象存储实例获取失败"), - SNOWFLAKE_ID_GENERATE_ERROR("B0009", "雪花算法生成ID失败"); + SNOWFLAKE_ID_GENERATE_ERROR("B0009", "雪花算法生成ID失败"), + + FREQUENT_SYSTEM_ACCESS("B0010", "系统请求过于频繁,请稍后再试"), + + SYSTEM_BUSY("B0020", "系统繁忙,请稍后再试"); /** * 响应状态码 diff --git a/core/utils/src/main/java/com/wansenai/utils/enums/LimitType.java b/core/utils/src/main/java/com/wansenai/utils/enums/LimitType.java new file mode 100644 index 00000000..96f91479 --- /dev/null +++ b/core/utils/src/main/java/com/wansenai/utils/enums/LimitType.java @@ -0,0 +1,24 @@ +/* + * Copyright 2023-2033 WanSen AI Team, Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://opensource.wansenai.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +package com.wansenai.utils.enums; + +public enum LimitType { + /** + * 默认策略全局限流 + */ + DEFAULT, + /** + * 根据请求者IP进行限流 + */ + IP +} diff --git a/web/src/views/basic/customer/index.vue b/web/src/views/basic/customer/index.vue index 384125e8..98ab5c46 100644 --- a/web/src/views/basic/customer/index.vue +++ b/web/src/views/basic/customer/index.vue @@ -5,7 +5,7 @@ 新增 批量删除 批量启用 - 批量禁用 + 批量停用 导入 导出 diff --git a/web/src/views/basic/income-expense/index.vue b/web/src/views/basic/income-expense/index.vue index c140bd57..7ee62a82 100644 --- a/web/src/views/basic/income-expense/index.vue +++ b/web/src/views/basic/income-expense/index.vue @@ -5,7 +5,7 @@ 新增 批量删除 批量启用 - 批量禁用 + 批量停用