最稳方案是onactionexecuting中做cidr校验与可信ip提取;禁用list.contains因o(n)低效且不支持网段,须用hashset存ip、ipnetwork实例预解析cidr,统一处理ipv4映射ipv6,并配置useforwardedheaders+knownproxies确保真实客户端ip获取准确。

直接在 OnActionExecuting 里做 CIDR 校验 + 可信 IP 提取,是 ASP.NET Core WebAPI 最稳、最不易翻车的方案。
为什么不能只用 List.Contains 做白名单判断
常见错误是把白名单存成 List<string></string>,每次调用 Contains 都走字符串比对。这不仅慢(O(n)),还完全不支持网段(如 192.168.1.0/24)。更隐蔽的问题是:本地调试时 RemoteIpAddress 常为 ::1 或 127.0.0.1,但上线后经 Nginx 转发就变成内网地址(如 10.0.1.5),硬编码字符串匹配直接失效。
- 改用
HashSet<ipaddress></ipaddress>存纯 IP,查找是 O(1) - 网段必须提前解析成
IPNetwork实例(推荐IPNetwork2NuGet 包),别在OnActionExecuting里现场Parse - IPv4 映射 IPv6 地址(如
::ffff:192.168.1.100)要统一转成 IPv4 再比对,否则本地测试总过不了
如何让 X-Forwarded-For 不被伪造又拿到真实客户端 IP
不配置 UseForwardedHeaders 就直接读 RemoteIpAddress,99% 的生产环境会拿错——你拦的其实是 Nginx 或 Cloudflare 的节点 IP,不是用户真实出口 IP。
- 必须在
Program.cs开头加app.UseForwardedHeaders() -
KnownProxies要填你反向代理的真实 IP(如 Nginx 在10.0.0.5),或用KnownNetworks填整个内网段(如IPNetwork.Parse("10.0.0.0/8")) - Cloudflare 用户额外加它的官方 IP 段(如
173.245.48.0/20),否则仍取不到原始 IP - 开发环境(Kestrel 直连)不需要这些配置,但代码不能写死判断环境——
UseForwardedHeaders在无转发头时自动降级,安全且兼容
黑白名单逻辑冲突时以谁为准
白名单和黑名单同时存在时,顺序决定结果。典型错误是先查黑名单再放行白名单,导致“黑名单里的 IP 却因在白名单中被放过”,违背最小权限原则。
- 正确顺序:先查黑名单,命中则直接
context.Result = new StatusCodeResult(403)并 return - 再查白名单,若白名单非空且当前 IP 不在其中,同样返回 403
- 不要用“白名单优先”逻辑——它会让黑名单形同虚设;也不要合并黑白名单做复杂布尔运算,易出边界 case
- 如果业务真需要“某网段禁用,但其中个别 IP 例外”,应把例外 IP 单独加进白名单,并确保白名单校验逻辑能覆盖到(即白名单检查必须包含精确 IP + CIDR)
IOptionsMonitor<ipfilteroptions></ipfilteroptions> 热更新的坑在哪
配置文件改了,但服务没重启,白名单却没生效?大概率是解析逻辑写在了 OnActionExecuting 里,或者 IPNetwork.Parse 没缓存。
-
IPFilterOptions类里不要存原始字符串数组,而应存已解析的IReadOnlyList<ipnetwork></ipnetwork> - 解析动作放在
ConfigureOptions或PostConfigure阶段,利用IOptionsMonitor的变更通知机制 -
IPNetwork实例是只读的,可安全跨线程共享,不用加锁 - 避免在过滤器里调用
Configuration.GetSection(...).Get<list>>()</list>—— 这会绕过IOptionsMonitor的热更新链路
真正难的不是写通逻辑,而是让 CIDR 解析、代理 IP 提取、IPv4/IPv6 归一化、热更新这四件事在所有环境(本地、测试、K8s、Cloudflare)下行为一致。少一个环节,上线后就可能突然放行一批不该放行的请求,或者误杀关键运维入口。











