服务端接口被人暴力请求了,老司机教你这样做?

327 阅读4分钟

调查:有多少后端同学是没有做暴力请求限制的 ❔❔❔❔❔❔❔
欢迎大家评论区一起讨论 😄😄😄

  • 本文已参与「新人创作礼」活动,一起开启掘金创作之路。

前言

暴力请求,指在很短时间段内,对接口发起大量请求。

会有什么影响?
如果接口包含了请求数据库,并且未作缓存,会导致某一短时间内数据库压力骤增,很有可能数据库服务器挂掉然后导致整个系统崩溃

如何避免

过滤器:允许代码在请求处理管道中的特定阶段之前或之后运行。
📋 过滤器管道在ASP.NET Core选择要执行的操作后运行

image.png

过滤器类型

每种筛选器类型都在筛选器管道中的不同阶段执行,详细内容大家可以去看官方文档哈!

  • 授权筛选器
    • 首先运行。
    • 确定用户是否获得请求授权。
    • 如果请求未获授权,可以让管道短路。
  • 资源筛选器
    • 授权后运行。
    • OnResourceExecuting 在筛选器管道的其余阶段之前运行代码。 例如,OnResourceExecuting 在模型绑定之前运行代码。
    • OnResourceExecuted 在管道的其余阶段完成之后运行代码。
  • 操作筛选器
    • 在调用接口方法之前和之后立即运行。
    • 可以更改传递到操作中的参数。
    • 可以更改从操作返回的结果。
    • 不可在 Razor Pages 中使用。
  • 异常筛选器
  • 结果筛选器

下图为各种类型过滤器在管道中的交互方式

image.png

使用过滤器

我们要实现限制请求,即在接口请求之前(Action Filters)就需要验证用户是否出现了大量重复请求,我们自定义一个限制请求的过滤器。

// 自定义RequestLimit特性继承ActionFilter 重写OnActionExecuted()方法 即在请求之前实现自己逻辑
public class RequestLimitAttribute : ActionFilterAttribute
{
    private readonly int requstLimit = 10; // 定义时间周期内可重复执行次数
    public RequestLimitAttribute(int RequstLimit)
    {
        requstLimit = RequstLimit;
    }
    public RequestLimitAttribute()
    {
    }
    public override void OnActionExecuted(ActionExecutedContext context)
    {
        // 类型转换ControllerActionDescriptor 可以获取到Action 和 Controller相关信息
        var descriptor = context.ActionDescriptor as ControllerActionDescriptor;
        // 定义客户端请求头唯一性,可以是设备Id 也可以是IP根据业务场景来
            var only = context.HttpContext.Request.Headers["XXXX"];
        if (string.IsNullOrWhiteSpace(only))
        {
            // 来源不明直接拒绝访问
            throw new Exception("访问来源不详,禁止访问!");
        }
        // 定义缓存key 为 唯一标识=> 控制器 => 方法
        string key = $"{only}:{descriptor.ControllerName}:{descriptor.ActionName}";
        // 获取缓存请求次数Value
        var cacheVal = WebApiRedisHelper.Get(key);
        int count = 1;
        if (cacheVal != null)
        {
            // 获取缓存key剩余时间
            var time = WebApiRedisHelper.Ttl(key);
            count = int.Parse(cacheVal) + 1;
            if (count > requstLimit)
                throw new Exception("访问频次太高了,请休息一下再来访问");
            // 更新缓存Key的次数,将缓存剩余时间写入
            WebApiRedisHelper.Set(key, count, Convert.ToInt32(time));
            return;
        }
        
        // 第一次缓存 次数 1,周期为60s
        WebApiRedisHelper.Set(key, count, 60);
    }
}
  • 使用:局部使用(直接使用 [RequestLimit] 特性即可应用于控制器所有方法)
[ApiController]
[RequestLimit]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    ......
    ......
    ......
}
  • 使用:全局使用(StartUp类中ConfigureServices添加相关代码即可全局使用,默认覆盖所有Controller)

注意:全局注册不能出现两个构造函数,可以直接使用一个赋默认值即可。

services.AddControllers(x => x.Filters.Add<RequestLimitAttribute>());

如何忽略过滤器?

即在某些特定场景下某个API不需要执行指定过滤器,我们如何处理?

// 新增AllowRequestLimit同样继承ActionFilterAttribute 重写之后不做任何处理即可,在需要忽略的方法使用特性标注即可
public class AllowRequestLimitAttribute : ActionFilterAttribute
{

    public override void OnActionExecuted(ActionExecutedContext context)
    {

    }

    public override void OnActionExecuting(ActionExecutingContext context)
    {

    }
}
  • 程序如何知道你想要忽略呢
// 验证控制器 或者 Action标注了AllowRequest
public bool SkipValidationFilter(ActionExecutedContext context) 
{
    //判断是否跳过授权过滤器 两种维度,一种是控制器级别,一种是Action级别,如果忽略我们需要验证是否添加了跳过授权头
    // 1.验证Controller
    //判断Controller是否跳过验证
    if (context.Controller.GetType().GetCustomAttributes(typeof(AllowRequestLimitAttribute), false).Any())
    {
        return true;
    }

    //判断Action是否跳过验证
    if (context.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor)
    {
        if (controllerActionDescriptor.MethodInfo.GetCustomAttributes(inherit: true)
           .Any(a => a.GetType().Equals(typeof(AllowRequestLimitAttribute))))
        {
            return true;
        }
    }
    return false;
}

public override void OnActionExecuted(ActionExecutedContext context)
{
    var skip = SkipValidationFilter(context);
    if (!skip)
    {
        // 走过滤器验证逻辑
    }
}

验证

  • 验证Redis存储是否正确(key,val,time)都正常

image.png

  • 验证暴力访问

image.png