受欢迎的博客标签

ASP.NET Core + Nginx 获取客户端的真实IP address地址

Published

get the real client IP address

$remote_addr
是nginx与客户端进行TCP连接过程中,获得的客户端真实地址. Remote Address 无法伪造,因为建立 TCP 连接需要三次握手,如果伪造了源 IP,无法建立 TCP 连接,更不会有后面的 HTTP 请求

 

X-Real-IP

是一个自定义头。X-Real-IP是最后一级代理将上游IP地址添加到该头中。

在每个HTTP请求头中加入X-Real-IP信息,若只存在1级的代理服务器,则该参数的确就是客户端的真实IP,但若是存在多级代理时,此信息为上级代理的IP信息,并不能获取到真实的用户IP.

X-Forwarded-For

X-Forwarded-For 是一个扩展头。在每个HTTP请求头中加入X-Forwarded-For信息,若只存在1级的代理服务器,则该参数为客户端的真实IP,若是存在多级代理时,每经过一级代理服务器,则追加上级代理服务的IP,可以获取到真实的用户IP,但若是遇到伪造的X-Forwarded-For信息或第一级代理未启用X-Forwarded-For都不能获取到真实用户IP,此法是目前比较常用的方法

X-Forwarded-For 请求头格式

X-Forwarded-For: client, proxy1, proxy2

客户端(例如浏览器)发送HTTP请求是没有X-Forwarded-For头的,当请求到达第一个代理服务器时,代理服务器会加上X-Forwarded-For请求头,并将值设为客户端的IP地址(也就是最左边第一个值).

如果有多个代理,会依次将IP追加到X-Forwarded-For头最右边,最终请求到达Web应用服务器,应用通过获取X-Forwarded-For头取左边第一个IP即为客户端真实IP。

一个 HTTP 请求到达服务器之前,经过了三个代理 Proxy1、Proxy2、Proxy3,IP 分别为 IP1、IP2、IP3,用户真实 IP 为 IP0,那么按照 XFF 标准,服务端最终会收到以下信息:

X-Forwarded-For: IP0, IP1, IP2

proxy1 拿到的是真实IP(36.157.229.110是我的IP),proxy2拿到的是proxy1的IP,proxy3 拿到的是proxy2的IP。

小结

X-Real-IP            客户端或上一级代理ip
X-Real-Port          客户端或上一级端口
X-Forwarded-For      包含了客户端和各级代理ip的完整ip链路</code>

其中X-Real-IP是必需的,后两项选填。当只存在一级nginx代理的时候X-Real-IP和X-Forwarded-For是一致的,而当存在多级代理的时候,X-Forwarded-For 就变成了如下形式

X-Forwarded-For: 客户端ip, 一级代理ip, 二级代理ip...

 

一.直连模式:直接使用Kestrel作为服务器监听

首先,我们先搞清楚 asp .net core 是如何获得客户端的IP 地址的。

计算机之间的数据通信,是通过一个一个的数据包来传送的。把收到的客户端发送的离散的数据包按照某种约定的规则重新组装起来,便成了各种协议。下面图片是IPv4数据报格式:

IPv4数据报

IPv4数据报

 

很明显,要获取客户端的地址,把这个数据包的32比特源IP地址取出来就是客户端的ip地址了。

 

 

 ASP.NET Core 应用部署在Linux上,使用了 Nginx 反向代理后,我们程序中获取真实IP(客户端真实ip,本文简称“真实IP”)的问题。

二.代理模式:使用nginx作为反向代理服务器

 

Configure ASP.NET Core to work with proxy servers and load balancers

https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer?view=aspnetcore-3.1

https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer?view=aspnetcore-8.0

我们的网站启用了阿里云的SLB,也就是负载均衡、反向代理服务器。我们把ssl证书配置到了SLB上,为了提升性能,SLB到我们的Web服务器用的是http通讯。

用户访问我们的网站的时候,其实是访问的SLB服务器,SLB服务器再把请求转发给我们的Web服务器,因此对于Web服务器看来,Web请求是来自于SLB服务器的http请求,因此应用在构造redirect_uri的时候识别的Request.Scheme时是http而非https。

解决这个问题很简单,.Net Core提供了很好的支持,只要在SLB反向代理上配置向Web服务器转发X-Forwarded-Proto(原始请求的协议)即可,这样反向代理服务器就会把原始的请求协议通过X-Forwarded-Proto这个报文头转发给Web服务器,Web服务器读取它就可以知道原始的协议是什么了。

只要在Startup.cs的app.UseForwardedHeaders();即可,代码如下:

 // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {

            app.UseForwardedHeaders(new ForwardedHeadersOptions
            {
                ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost
            });
         

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    }

ForwardedHeaders中间件会自动把反向代理服务器转发过来的X-Forwarded-For(客户端真实IP)以及X-Forwarded-Proto(客户端请求的协议)自动填充到HttpContext.Connection.RemoteIPAddress和HttpContext.Request.Scheme中,这样应用代码中读取到的就是真实的IP和真实的协议了,不需要应用做特殊处理。

 
1.安装 Nginx

 

2.新建 ASP.NET Core 项目

 

3.编写代码

编辑 ValuesController

private readonly HttpContext _context; public ValuesController(IHttpContextAccessor accessor) { _context = accessor.HttpContext; } // GET api/values [HttpGet] public ActionResult<IEnumerable<string>> Get() { return Ok($"获取到的真实IP:{_context.Connection.RemoteIpAddress}"); }

        

编辑 Startup

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
            services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
        }
4.测试

 

 

 

 

X-Forwarded-For - IP address (x.x.x.x)
X-Forwarded-Proto - Protocol (http, https)
X-Forwarded-Host - Host (something.com:54321)
Mapping
X-Forwarded-For - HttpContext.Connection.RemoteIpAddress & HttpContext.Connection.RemotePort
X-Forwarded-Proto - HttpContext.Request.Scheme
X-Forwarded-Host - HttpContext.Request.Host

(1)将程序部署到服务器

本文略此步

(2)配置 Nginx 反向代理

新建配置文件 realiptest.conf

server { listen 5002; access_log off; location / { proxy_pass http://localhost:5000; } }


(3)测试访问

服务器地址:192.168.157.132

我本机地址:192.168.157.1

那么我本机通过访问 http://192.168.157.132:5002/api/values api获取到的ip地址应该是我本机的,即 192.168.157.1

通过浏览器访问验证:

 

可是却获取到了 127.0.0.1,这是因为 们的请求到了 Nginx,然后 Nginx 再将我们的请求转发到 ASP.NET Core 应用程序,实际上与 ASP.NET Core 应用程序 建立连接的是 Nginx ,所以获取到了服务器本地 IP (Nginx和程序部署在一台机子上)。请求流程如下图:

 

三.解决问题

修改程序代码以便显示更详细的信息:

ValuesController

        // GET api/values
        [HttpGet]
        public ActionResult<IEnumerable<string>> Get()
        {
            StringBuilder sb=new StringBuilder();
            sb.AppendLine($"RemoteIpAddress:{_context.Connection.RemoteIpAddress}");

            if (Request.Headers.ContainsKey("X-Real-IP"))
            {
                sb.AppendLine($"X-Real-IP:{Request.Headers["X-Real-IP"].ToString()}");
            }

            if (Request.Headers.ContainsKey("X-Forwarded-For"))
            {
                sb.AppendLine($"X-Forwarded-For:{Request.Headers["X-Forwarded-For"].ToString()}");
            }
            return Ok(sb.ToString());
        }

修改反向代理配置:

server {
    listen 5002;
    access_log  off;
    location / {
       proxy_set_header   X-Real-IP        $remote_addr;
       proxy_set_header   Host             $host;
       proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;
       proxy_pass                          http://localhost:5000;
    }
}

再次访问:

 

可以看到 X-Real-IPX-Forwarded-For请求头获取到了真实IP,我们通过修改 Nginx 配置,让程序接收到的请求信息携带真实IP。Nginx 通过在 X-Real-IP 、X-Forwarded-For 请求头设置了与它连接的远程ip

以上解决办法对于没有使用CDN是适用的。

三.使用CDN如何解决

 1.网络架构

源站 <-->  1层nginx代理 <-->  2层nginx代理 <--> CDN <-->客户端
访客通过浏览器访问您的网址,会首先到达Cloudflare的CDN服务器,再经过 Cloudflare 的中转抵达你的源站。
在没有进行额外设置的情况下,你源服务器得到的访客IP并不是真实的IP。这个IP地址是Cloudflare的CDN服务器IP地址。
 
在使用Cloudflare CDN时,CDN传给后端服务器中只传递了有限的 http request hearder。
 
在使用了CDN做加速站点静态资源加速后,当用户请求的静态资源没能命中,此时CDN会到源站请求内容,那么此时访问源站的IP为CDN节点的IP,不仅如此,可能经我们的WAF防火墙和前端的负载均衡(SLB)后更不容易获取到真实的用户IP信息,我们如果要统计用户的访问IP和地区就变得比较麻烦,因为可能不是真实的IP,必须使用一个什么机制将用户IP传递到最终后端的应用服务器才行.

我们的请求经过一个或者多个cdn结点以后,我们的程序如何获取真实IP呢,这就要看cdn服务商提供的解决办法了,一般有两种:

1.cdn服务商支持设置真实ip到某个指定的请求头,这样我们通过这个请求头就能获取了 。

2.一般经过cdn都会把真实ip经过的结点ip信息添加到头 X-Forwarded-For,我们取这个头里的第一个ip就是真实ip。

添加 nginx 配置,让他再次代理 5002 端口(前面添加的代理ASP.NET Core 程序),模拟cdn第二种方案:

server {
  listen 5003;
  access_log off;
  location /
  {
    proxy_set_header X-Real-IP    $remote_addr;
    proxy_set_header     Host $host;
    proxy_set_header X-Forwarded-For     $proxy_add_x_forwarded_for; 
    proxy_pass      http: //192.168.157.132:5002; 
  }

}



我们再次访问:

 

可以看到我们的真实ip被放到 X-Forwarded-For 请求头的第一个IP,X-Real-IP 获取到的是上一层代理的ip

X-Forwarded-For 来自百度百科的解释:X-Forwarded-For 简称XFF头,它代表客户端,也就是HTTP的请求端真实的IP,只有在通过了HTTP 代理或者负载均衡服务器时才会添加该项。它不是RFC中定义的标准请求头信息,在squid缓存代理服务器开发文档中可以找到该项的详细介绍。标准格式如下:X-Forwarded-For: client1, proxy1, proxy2。请求流程如下图:

借助 Cloudflare Workers 实现查询 ip 信息的API

在使用Cloudflare CDN时,CDN传给后端服务器中只传递了有限的 http request hearder。
其中只包含了非常简陋的 ip 信息,例如只有 CF-IPCountry ,不足以实现一个查询ip信息的api,要想实现查询ip详细信息需要借助 Cloudflare Workers 来实现。

Cloudflare Workers 传入的 HTTP 请求都被称为 fetch 事件,fetch 事件中都包含一个 Request 接口实例,在这个 Request 实例中就包括了访问 ip 的详细信息

通过访问 Request.cf 可以获得 Cloudflare 提供的请求信息,包含了 ip 的详细信息;访问 Request.headers 则可以获得访客的 HTTP headers,写一个简单的 Workers 处理一下 Request.cf 、Request.headers 中的信息,并返回 Json 格式即可。

访问该 Cloudflare Workers 后返回包含访客 IP、Continent、Country、Region、ASN 等 ip 信息及请求的Header信息,同时也会把 Cloudflare 的 https 连接信息及 bot 识别的相关信息一起返回。

https://opswill.com/articles/cloudflare-worker-ip-info-api.html

五.如何在代码里最小改动

经过上面的讲解,显而易见我们在代码里无法直接通过 RemoteIpAddress 获取真实ip,那么如果我们在编写代码时,很多地方直接采用 RemoteIpAddress获取真实ip怎么办,难道需要修改每一处吗,这里分享一个简单的解决办法,就是利用 ASP.NET Core 中间件给 RemoteIpAddress 重新赋值。

编写 RealIpMiddleware 中间件:

public class RealIpMiddleware
{
    private readonly RequestDelegate _next;

    public RealIpMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public Task Invoke(HttpContext context)
    {
        var headers = context.Request.Headers;
        if (headers.ContainsKey("X-Forwarded-For"))
        {
            context.Connection.RemoteIpAddress=IPAddress.Parse(headers["X-Forwarded-For"].ToString().Split(',', StringSplitOptions.RemoveEmptyEntries)[0]);
        }
        return _next(context);
    }
}

如果是前面提到的cdn的第一种情况,只需判断cdn服务商提供的特殊请求头就行了。

在Startup中配置

 

应放在最靠前的位置,以免有中间件获取到了未重置的IP地址。

保持前面的模拟cdn第二中情况架构,再次进行测试:

 

可以看到通过 RemoteIpAddress 获取到了真实ip。这种解决方案算是比较好的了。

这里提一下 Nginx RealIP Module 是 Nginx 获取真实ip的一个模块,有兴趣的同学可以自己去研究一下。

六.使用组件 Unicorn.AspNetCore

Unicorn.AspNetCore 里面我有封装处理ip的中间件。

通过nuget安装:

Install-Package Unicorn.AspNetCore

然后在 Program 中添加:

 

开源地址:https://github.com/UCPlan/Unicorn/tree/master/src/Infrastructure/Unicorn.AspNetCore/Middleware/RealIp