// ============================================================================
// 【文件说明】HwPortalIpRateLimitAttribute.cs - IP限流特性
// ============================================================================
// 这是一个"限流过滤器",用于防止接口被恶意刷调用。
//
// 【业务场景】
// 比如用户留言接口,如果不限流,恶意用户可以1秒提交100次,
// 导致数据库被打爆、短信接口被刷爆。所以需要限制:
// "同一IP在60秒内最多提交10次"。
//
// 【什么是 Attribute(特性)?】
// 特性是 C# 的"声明式编程"机制,可以给代码元素(类、方法等)贴标签。
//
// 对比 Java 注解:
// Java: @RateLimit(key = "contact", time = 60, count = 10)
// C#: [HwPortalIpRateLimit("contact", 60, 10)]
//
// 两者概念完全一样,只是:
// - Java 用 @ 符号,叫"注解"(Annotation)
// - C# 用 [] 方括号,叫"特性"(Attribute)
//
// 【什么是 Filter(过滤器)?】
// 过滤器是 ASP.NET Core 的中间件机制,可以在请求到达控制器前后执行代码。
//
// 执行顺序:
// 请求 -> 中间件 -> Filter(OnActionExecuting) -> 控制器方法 -> Filter(OnActionExecuted) -> 响应
//
// 对比 Java Spring:
// Java Spring 用 HandlerInterceptor 或 AOP @Aspect 实现类似功能。
// 若依框架的限流就是用 AOP 切面实现的。
// ============================================================================
namespace Admin.NET.Plugin.HwPortal;
using Microsoft.Extensions.DependencyInjection;
///
/// IP限流特性。
///
/// 【C# 语法知识点 - 特性定义】
/// public sealed class HwPortalIpRateLimitAttribute : Attribute, IAsyncActionFilter
///
/// 1. 继承 Attribute:这是定义特性的基础要求
/// 2. 实现 IAsyncActionFilter:这是 ASP.NET Core 的过滤器接口
/// 3. sealed 关键字:密封类,不能被继承(特性通常不需要继承)
///
/// 对比 Java:
/// Java 定义注解用 @interface 关键字:
/// @Target(ElementType.METHOD)
/// @Retention(RetentionPolicy.RUNTIME)
/// public @interface RateLimit {
/// String key();
/// int time() default 60;
/// int count() default 10;
/// }
///
/// C# 的特性是真正的类,可以有构造函数、属性、方法。
/// Java 的注解更像接口,只能定义属性签名。
///
///
/// 【AttributeUsage 特性】
/// [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
///
/// 这是"元特性"——给特性本身贴的特性:
/// - AttributeTargets.Method:只能用在方法上(不能用在类上)
/// - AllowMultiple = false:同一个方法只能贴一个
/// - Inherited = true:子类继承父类方法时,特性也会继承
///
/// 对比 Java:
/// Java 用 @Target 和 @Retention 注解定义:
/// @Target(ElementType.METHOD) // 只能用在方法上
/// @Retention(RetentionPolicy.RUNTIME) // 运行时保留
///
///
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public sealed class HwPortalIpRateLimitAttribute : Attribute, IAsyncActionFilter
{
///
/// 限流键名(用于区分不同业务场景)。
///
public string Key { get; }
///
/// 时间窗口(秒)。
///
public int Time { get; }
///
/// 最大访问次数。
///
public int Count { get; }
///
/// 限流提示消息。
///
/// 【C# 语法知识点 - 属性默认值】
/// public string Message { get; set; } = "访问过于频繁,请稍后再试";
///
/// = "..." 是属性的默认值初始化。
/// 调用方可以覆盖:[HwPortalIpRateLimit("key", 60, 10, Message = "自定义消息")]
///
/// 对比 Java:
/// Java 注解用 default 关键字:
/// String message() default "访问过于频繁,请稍后再试";
///
///
public string Message { get; set; } = "访问过于频繁,请稍后再试";
///
/// 构造函数。
///
/// 【C# 语法知识点 - 构造函数参数】
/// public HwPortalIpRateLimitAttribute(string key, int time, int count)
///
/// 使用方式:
/// [HwPortalIpRateLimit("contact", 60, 10)]
/// public async Task<HwPortalAjaxResult> AddContactUsInfo(...) { }
///
/// 对比 Java:
/// Java 注解没有构造函数,使用时写属性名:
/// @RateLimit(key = "contact", time = 60, count = 10)
///
/// C# 特性更接近普通类,构造函数参数是位置参数(必填),
/// 属性是命名参数(可选)。
///
///
/// 【设计意图】
/// 把限流配置固化在特性参数里,让控制器方法声明时就能看出限流策略:
/// - 一眼看出限流配置
/// - 不用翻配置文件
/// - 代码即文档
///
///
/// 限流键名
/// 时间窗口(秒)
/// 最大次数
public HwPortalIpRateLimitAttribute(string key, int time, int count)
{
Key = key;
Time = time;
Count = count;
}
///
/// 异步过滤器执行方法。
///
/// 【C# 语法知识点 - IAsyncActionFilter 接口】
/// Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
///
/// 这是 ASP.NET Core 的过滤器核心接口:
/// - ActionExecutingContext:执行前上下文(可以拿到请求信息、修改结果)
/// - ActionExecutionDelegate next:继续执行的委托(调用它就放行)
/// - Task:异步方法返回类型
///
/// 执行流程:
/// 1. 框架调用 OnActionExecutionAsync
/// 2. 过滤器做前置处理(限流检查)
/// 3. 如果通过,调用 await next() 放行
/// 4. 控制器方法执行
/// 5. 过滤器做后置处理(本例没有)
///
/// 对比 Java Spring:
/// Java 用 HandlerInterceptor 的三个方法:
/// preHandle() -> 对应 next() 之前的代码
/// postHandle() -> 对应 next() 之后的代码
/// afterCompletion() -> 完成后的清理
///
/// 或者用 AOP @Around:
/// @Around("切点表达式")
/// public Object around(ProceedingJoinPoint pjp) {
/// // 前置处理
/// Object result = pjp.proceed(); // 相当于 await next()
/// // 后置处理
/// return result;
/// }
///
///
/// 执行上下文
/// 继续执行的委托
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
// 【获取 HTTP 上下文】
// context.HttpContext 包含当前请求的所有信息:
// - Request:请求对象(Headers、Query、Body、Cookies 等)
// - Response:响应对象
// - User:当前登录用户(如果有)
// - RequestServices:DI 服务容器
//
// 对比 Java Spring:
// Java 通常通过注入 HttpServletRequest:
// @Autowired private HttpServletRequest request;
// 或者在 Controller 方法参数中声明:
// public Result method(HttpServletRequest request) { ... }
HttpContext httpContext = context.HttpContext;
// 【从 DI 容器获取服务】
// httpContext.RequestServices.GetRequiredService<SysCacheService>()
//
// RequestServices 是"请求级 DI 容器"。
// GetRequiredService<T>() 获取服务实例,如果不存在会抛异常。
//
// 对比 Java Spring:
// Java 用 @Autowired 注入:
// @Autowired private RedisCache redisCache;
//
// C# 的写法更显式,从容器直接获取。
// 两种方式本质一样:都是依赖注入。
SysCacheService cacheService = httpContext.RequestServices.GetRequiredService();
// 获取请求 IP。
string requestIp = HwPortalContextHelper.CurrentRequestIp(httpContext);
// 获取当前方法名(用于日志和区分不同接口)。
// context.ActionDescriptor.DisplayName 是方法的完整名称。
// ?? string.Empty 是空合并运算符:如果 DisplayName 为 null,就用空字符串。
string actionName = context.ActionDescriptor.DisplayName ?? string.Empty;
// 【构建缓存键】
// 格式:hwportal:ratelimit:{key}:{ip}:{action}
//
// 为什么要拼接这么多信息?
// 1. key:区分不同业务场景(留言、搜索等)
// 2. ip:区分不同用户
// 3. action:区分同一控制器的不同方法
//
// 这样可以做到:
// - 同一IP访问不同接口,各自独立计数
// - 同一接口不同IP,各自独立计数
//
// 对比 Java 若依:
// 若依的限流键也是类似拼接:
// String key = config.getKey() + ":" + ip + ":" + methodName;
string cacheKey = $"hwportal:ratelimit:{Key}:{requestIp}:{actionName}";
// 时间窗口和最大次数的兜底处理。
// 如果传入的值 <= 0,就用默认值 60。
int windowSeconds = Time > 0 ? Time : 60;
int maxCount = Count > 0 ? Count : 60;
// 【分布式锁】
// 为什么需要锁?
// 限流必须原子性,避免并发请求同时穿透。
// 比如当前计数=9,两个请求同时读取,都认为可以通过,结果变成11次。
//
// BeginCacheLock 在缓存层获取分布式锁:
// - "lock:{cacheKey}":锁的键名
// - 500:获取锁的超时时间(毫秒)
// - 5000:锁的持有时间(毫秒)
// - throwOnFailure: false:获取失败不抛异常,返回 null
//
// using 语法:自动释放锁(离开作用域时调用 Dispose)
//
// 对比 Java 若依:
// 若依用 Redis 的 SETNX 实现分布式锁:
// Boolean locked = redisCache.setCacheObject(lockKey, "1", 5, TimeUnit.SECONDS);
using IDisposable? cacheLock = cacheService.BeginCacheLock($"lock:{cacheKey}", 500, 5000, throwOnFailure: false);
if (cacheLock == null)
{
// 获取锁失败,说明并发太高,直接拒绝。
// 业务取舍:宁可错杀少量请求,也不能放开限流。
//
// 【短路返回】
// context.Result = ... 会直接设置响应结果,不再执行控制器方法。
// 这就是"拦截"的实现方式。
//
// ObjectResult 是 ASP.NET Core 的返回类型,
// 会自动把对象序列化成 JSON 响应。
context.Result = new ObjectResult(HwPortalAjaxResult.Error(Message));
return;
}
// 【读取计数器】
// 从缓存获取当前时间窗口的访问次数。
// HwPortalRateLimitCounter 是内部类,包含 Count 和 ExpireAt。
HwPortalRateLimitCounter counter = cacheService.Get(cacheKey);
DateTime now = DateTime.UtcNow;
if (counter == null || counter.ExpireAt <= now)
{
// 【新建计数窗口】
// 没有计数器,或窗口已过期,创建新窗口。
//
// 为什么新窗口 Count = 1?
// 当前请求本身也算一次,所以从 1 开始。
counter = new HwPortalRateLimitCounter
{
Count = 1,
ExpireAt = now.AddSeconds(windowSeconds)
};
// 写入缓存,设置过期时间。
// 缓存到期后自动删除,不需要后台清理。
cacheService.Set(cacheKey, counter, TimeSpan.FromSeconds(windowSeconds));
// 【放行】
// await next() 调用下一个过滤器或控制器方法。
// 如果前面已经 return,就说明请求被拦截了。
await next();
return;
}
if (counter.Count >= maxCount)
{
// 【限流触发】
// 计数器已达到上限,拒绝请求。
//
// 注意:这里返回的是正常响应(code=500),不是抛异常。
// 限流是"预期中的业务拒绝",不应该走全局异常处理。
context.Result = new ObjectResult(HwPortalAjaxResult.Error(Message));
return;
}
// 【计数器+1】
// 窗口内后续请求,只增加计数,不重置过期时间。
// 这样是"固定窗口"限流,不是"滑动窗口"。
counter.Count++;
// 计算剩余时间。
TimeSpan expire = counter.ExpireAt - now;
// 更新缓存,用剩余时间作为过期时间。
cacheService.Set(cacheKey, counter, expire > TimeSpan.Zero ? expire : TimeSpan.FromSeconds(windowSeconds));
// 放行请求。
await next();
}
///
/// 限流计数器(内部类)。
///
/// 【C# 语法知识点 - 嵌套类】
/// private sealed class HwPortalRateLimitCounter
///
/// 嵌套类定义在另一个类内部:
/// - private:只能在外部类中使用
/// - sealed:不能被继承
///
/// 对比 Java:
/// Java 的内部类有两种:
/// - 静态嵌套类:static class Inner { } - 类似 C# 的嵌套类
/// - 非静态内部类:class Inner { } - 可以访问外部类实例成员
///
/// C# 的嵌套类默认是"静态"的(不能访问外部类实例成员),
/// 除非显式传入外部类引用。
///
///
/// 【为什么定义为嵌套类?】
/// 1. 只在这个特性内使用,不需要暴露给外部
/// 2. 封装性:外部代码不需要知道这个类的存在
/// 3. 避免污染命名空间
///
///
private sealed class HwPortalRateLimitCounter
{
///
/// 访问次数。
///
public int Count { get; set; }
///
/// 窗口过期时间。
///
public DateTime ExpireAt { get; set; }
}
}