# 思路

记录访问服务器的 IP,当一定时间段内访问次数或错误次数超过一定限度时,将请求直接返回 403 不经过逻辑代码

# 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
using Microsoft.AspNetCore.Http;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Helper
{
public class DefenceCCMiddleware
{
private readonly RequestDelegate _next;

public DefenceCCMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
string ip = GetRequestIP(context);
// Console.WriteLine(ip);
if (BlackList.Contains(ip))
{
context.Response.StatusCode = (int)HttpStatusCode.Forbidden;
context.Response.ContentLength = 0;
return;
}
SaveError(context);
await _next.Invoke(context);
}
public class ResponseError
{
public string IpAddress { get; set; }
public DateTime OccurTime { get; set; } = DateTime.Now;
public ResponseError()
{
new Thread(() =>
{
Thread.Sleep(60 * 1000);
RemoveError(IpAddress);
}).Start();
}
}
private static List<ResponseError> ErrorSave = new();
public static List<string> BlackList = new();
public bool SaveError(HttpContext context)
{
string ip = GenericHelper.GetRequestIP(context);
int secondReq = 0, minuteReq = 0, hourReq = 0;
foreach (var item in ErrorSave)
{
if (item.IpAddress == ip)
{
if (item.OccurTime.AddSeconds(1) > DateTime.Now)
{
secondReq++;
}
else if (item.OccurTime.AddMinutes(1) > DateTime.Now)
{
minuteReq++;
}
else if (item.OccurTime.AddHours(1) > DateTime.Now)
{
hourReq++;
}
}
}
int maxSecond = ConfigHelper.GetConfig<int>("MaxRequestPerSecond");
int maxminute = ConfigHelper.GetConfig<int>("MaxRequestPerMinute");
if (secondReq > maxSecond || minuteReq > maxminute)
{
BlackList.Add(ip);
new Thread(() =>
{
Thread.Sleep(60 * 60 * 1000);
BlackList.Remove(ip);
}).Start();
return true;
}
else
{
ErrorSave.Add(new ResponseError { IpAddress = ip });
}
return false;
}
public static void RemoveError(string ip)
{
ErrorSave.Remove(ErrorSave.First(x => x.IpAddress == ip));
}
public static string GetRequestIP(HttpContext context)
{
var ip = context.Request.Headers["X-Forwarded-For"].FirstOrDefault();
if (ip == null)
{
return context.Connection.RemoteIpAddress.ToString();
}
if (ip.Contains(","))
{
ip = ip.Split(',').First().Trim();
}
return ip;
}
}
}

# 解释

  • 由于服务器使用了 nginx,常规获取 IP 的方法将只能获取到 localhost,后续章节讲如何配置
  • 将请求的 IP 与请求时间存在数组中,每次请求时统计数组中的该 IP 在一秒钟内、一分钟内、一小时内的请求数量,如果有超过限额的 IP 就将此 IP 放置于黑名单中,再次碰到此 IP 将直接返回 403 而不经过逻辑代码
  • 写法并不完善,对于每个请求都将开启一个线程用于定时将 IP 从数组中移除,对于大型网站而言可能会造成性能损失与负载增加。想要改进可以使用一整个线程对整个数组进行维护

# 配置 Nginx 转发 IP 请求

  • 在 nginx 的 location 中添加以下配置
1
2
3
4
proxy_set_header   Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $server_name;

这样就会真实 IP 写在请求头内传递给后端

  • Asp.net 安装 Microsoft.AspNetCore.HttpOverrides
  • Startup.csConfigureServices 函数中添加以下代码
1
2
3
4
5
6
7
services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor |
ForwardedHeaders.XForwardedProto;
options.KnownNetworks.Clear();
options.KnownProxies.Clear();
});