六、大话HTTP协议-HTTP协议之长短连接简介、代理、网关

本文深入探讨了在HTTP代理服务器存在的情况下,如何获取客户端真实IP的问题。介绍了HTTP的长连接与短连接,强调了X-Forwarded-For和X-Real-IP头字段在代理场景中的作用。通过实验展示了在Nginx反向代理下,如何配置以传递客户端IP,并讨论了伪造X-Forwarded-For头的风险以及应对策略。同时,提到了网关的角色,尤其是其在协议转换中的功能。

本篇文章的核心实际上并不是介绍长连接是什么,代理是什么,网关又是什么,相信简单翻下书或者搜下关键字就可以找到,没有什么太多好说的,本篇文章真正想探讨的是存在代理服务器的情况下,WEB服务端如何能获取到客户端真实IP问题。围绕此话题进行了相关的佐证和实验。

一、HTTP的长连接和短连接

  • HTTP协议是基于请求/响应模式的,因此只要服务端给了响应,本次HTTP请求就结束了。
  • HTTP的长连接和短连接本质上是TCP的长连接和短连接。
    • 一个形象的例子就是,拿你在网上购物来说,HTTP协议是指的那个快递单,你寄件的时候填的单子就像是发了一个HTTP请求,等货物运到地方了,快递员会根据你发的请求把货物送给相应的收货人。而TCP协议就是中间运货的那个大货车,也可能是火车或者飞机,但不管是什么,它是负责运输的,因此必须要有路,不管是地上还是天上。那么这个路就是所谓的TCP连接,也就是一个双向的数据通道。
  • HTTP/1.0默认使用的是短连接。也就是说,浏览器和服务器每进行一次HTTP操作,就建立一次连接,结束就中断。
  • HTTP/1.1起,默认使用长连接,用以保持连接特性。
    • 比如一个页面,有图片、CSS、JS等文件需要下载,就可以共用一个TCP通道去处理,而不是重复开启多个TCP连接去请求。

理解起来其实很简单,简单来说他两的过程如下:

短连接的操作步骤是:
建立连接——数据传输——关闭连接...建立连接——数据传输——关闭连接
长连接的操作步骤是:
建立连接——数据传输...(保持连接)...数据传输——关闭连接

我们随手打开F12看一个请求就知道了:

image

当然了,这个长连接的持续时间是可以在服务端进行设置的,避免连接长期占用而无活动,白白浪费了服务器资源。

二、HTTP中介之代理

所谓的“代理服务”就是指服务本身不生产内容,而是处于中间位置转发上下游的请求和响应,具有双重身份:面向下游的用户时,表现为服务器,代表源服务器响应客户端的请求;而面向上游的源服务器时,又表现为客户端,代表客户端发送请求。

image

这很好理解,打个比方,之前你都是从超市里买东西,现在楼底下新开了一家 24 小时便利店,由超市直接供货,于是你就可以在便利店里买到原本必须去超市才能买到的商品。这样超市就不直接和你打交道了,成了“源服务器”,便利店就成了超市的“代理服务器”。

为什么要有代理呢?换句话说,代理能干什么、带来什么好处呢?

在计算机科学领域里的任何问题,都可以通过引入一个中间层来解决,不行就再加一层,我们的TCP/IP五层模型或者OSI七层模型正是体现了此思想。

代理到底能解决什么问题呢?代理最常用的一个功能是负载均衡,因为在面向客户端时屏蔽了源服务器,客户端看到的只是代理服务器,源服务器究竟有多少台、是哪些 IP 地址都不知道。于是代理服务器就可以掌握请求分发的“大权”,决定由后面的哪台服务器来响应请求。这就是大名鼎鼎的反向代理

image

由于代理处在 HTTP 通信过程的中间位置,相应地就对上屏蔽了真实客户端,对下屏蔽了真实服务器,简单的说就是“欺上瞒下”。在这个中间层的“小天地”里就可以做很多的事情,为 HTTP 协议增加更多的灵活性,实现客户端和服务器的“双赢”。

因为它“欺上瞒下”的特点,隐藏了真实客户端和服务器,如果双方想要获得这些“丢失”的原始信息,该怎么办呢?

首先,我们如何知道通信链路中是否经过了中间代理呢?

可以通过 Via 这个通用字段,请求头或响应头里都可以出现。每当报文经过一个代理节点,代理服务器就会把自身的信息追加到字段的末尾,就像是经手人盖了一个章。

例如下图中有两个代理:proxy1proxy2,客户端发送请求会经过这两个代理,依次添加就是Via: proxy1, proxy2,等到服务器返回响应报文的时候就要反过来走,头字段就是Via: proxy2, proxy1

image

Via 只是解决了如何判断是否有代理,如果我想获取原始客户端的真实IP呢

2.1 X-Forwarded-For字段

我们先来了解一个重要的HTTP头字段:X-Forwarded-For

X-Forwarded-For的字面意思是“为谁而转发”,形式上和Via差不多,也是每经过一个代理节点就会在字段里追加一个信息。但Via追加的是代理主机名(或者域名),而X-Forwarded-For追加的是请求方的 IP 地址。所以,在字段里最左边的 IP 地址就客户端的地址。

X-Forwarded-For: IP0, IP1, IP2

X-Real-IP是另一种获取客户端真实 IP 的手段,它的作用很简单,就是记录客户端 IP 地址,没有中间的代理信息,相当于是X-Forwarded-For的简化版。如果客户端和源服务器之间只有一个代理,那么这两个字段的值就是相同的。

由于HTTP的header可以随意地构造,对于安全要求较高的场景,获取IP需要格外的小心,因为没有代理服务器的情况和有代理服务器的情况,获取IP的方式可能不一样。

如果确定是直连服务的话,request.getRemoteAddr()获取到的就是用户最真实的IP,为什么这么说呢?

众所周知TCP/IP建立连接时是需要三次握手的,并且,只有知道了client端请求的IP地址,server端的数据才能返回给client,所以client想要获取到数据就必须提供真实的IP。因此Remote Address无法伪造,因为建立 TCP 连接需要三次握手,如果伪造了源 IP,无法建立 TCP 连接,更不会有后面的 HTTP 请求。因此说,request.getRemoteAddr()获取到的就是用户最真实的IP。在没有任何反向代理的情况下,这个方式是可行的。

但是,现在大多数据服务器为了安全都会使用nginx作为代理服务器,而用户对代理服务器发起的HTTP请求,代理服务器对服务集群中的真实部署的对应服务进行“二次请求”,所以最终获取的IP是代理服务器在内网中的ip地址,如192.168.xx.xx/10.xx.xx.xx等等。

好了,理论说的差不多了,不动手试试,谁知道说的对不对,让我们来动起手来吧。

2.2 获取客户端实际IP

创建一个SpringBoot项目,写一个接口:

@RestController
public class TestController {

    @RequestMapping("getIpAddr")
    public String getIpAddr(HttpServletRequest request){
        String clientIp = request.getRemoteAddr();
        String clientUrl = request.getRequestURL().toString();
        return "clientIp="+clientIp+",clientUrl="+clientUrl;
    }

}

端口设置为server.port=9999

下面进行maven打包为xxx.jar,上传到服务器上,服务器提前安装好nginxjdk,直接执行nohup java -jar xxx.jar &即可启动服务。

image

我们先不通过nginx直接去访问下此服务(注意云服务器要放开9999端口的安全组设置)

image

这里的114.221.179.60确实是我本地当前的出访IP:

image

事实证明,如果没有nginx,我直接通过request.getRemoteAddr()是可以拿到客户端的真实IP地址的。

下面通过nginx进行访问,在nginx上配置代理地址,例如在我的IP为111.231.119.253的云服务器上,tomcat端口号为9999Nginx端口号80Nginx反向代理9999端口:

server {
    listen 80;
    location / {
        proxy_pass http://127.0.0.1:9999; # 反向代理应用服务器HTTP地址
    }
}

在另一台机器上浏览器打开http://111.231.119.253/getIpAddr访问此应用,获取客户端IPURL,结果为:

image

佐证了上面说的:程序获取到的客户端IP是Nginx的IP而非浏览器所在机器IP,获取到的URL是Nginx proxy_pass配置的URL组成的地址,而非浏览器地址上的真实地址。

对于Web应用来说,这次HTTP请求的客户端是Nginx而非真实的客户端浏览器,如果不加特殊处理的话,Web应用会把Nginx当做请求的客户端,获取到的客户端信息就是Nginx的信息。

那么如何解决呢?办法总比困难多,按照网络教程:https://imququ.com/post/x-forwarded-for-header-in-http.html,需要对nginx做下配置才行。

server {
        listen       80;
        #server_name  www.oursnail.cn;

        location / {
            proxy_set_header Host $http_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-Proto $scheme;
            proxy_pass http://127.0.0.1:9999;
        }

        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }

  • Host:包含客户端真实的域名和端口号。
  • X-Forwarded-Proto:表示客户端真实的协议(http还是https)。
  • X-Real-IP:表示客户端真实的IP
  • X-Forwarded-For:这个HeaderX-Real-IP类似,但是它在多层代理时会包含真实客户端及中间每个代理服务器的IP。

remoteaddr只能获取到与服务器本身直连的上层请求ip,所以设置remote_addr 只能获取到与服务器本身直连的上层请求ip,所以设置remoteaddripremote_addr一般都是设置第一个代理上面;但是问题是,有时候是通过cdn访问过来的,那么后面web服务器获取到的,永远都是cdn 的ip 而非真是用户ip,那么这个时候就要用到X-Forwarded-For 了。这个参数,从上面学习到,是从客户真实IP为起点,穿过多层proxy到达最终的web服务器的,所有的IP都会被记录下来,所以下面获取IP的逻辑是优先从X-Forwarded-For的IP列表中获取,获取不到才去从X-Real-IP中获取。

通过./nginx -s reload进行重启,并且我需要修改代码。写一个获取IP的工具类IpUtil

public class IpUtil {
    public static String getIp(HttpServletRequest request) throws Exception {
        String ip = request.getHeader("X-Forwarded-For");
        if (ip != null){
            if (!ip.isEmpty() && !"unKnown".equalsIgnoreCase(ip)) {
                int index = ip.indexOf(",");
                if (index != -1) {
                    return ip.substring(0, index);//获取x-forwarded-for中的第一个
                } else {
                    return ip;
                }
            }
        }
        ip = request.getHeader("X-Real-IP");
        if (ip != null) {
            if (!ip.isEmpty() && !"unKnown".equalsIgnoreCase(ip)) {
                return ip;
            }
        }
        ip = request.getHeader("Proxy-Client-IP");
        if (ip != null) {
            if (!ip.isEmpty() && !"unKnown".equalsIgnoreCase(ip)) {
                return ip;
            }
        }
        ip = request.getHeader("WL-Proxy-Client-IP");
        if (ip != null) {
            if (!ip.isEmpty() && !"unKnown".equalsIgnoreCase(ip)) {
                return ip;
            }
        }
        ip =  request.getRemoteAddr();
        return ip.equals("0:0:0:0:0:0:0:1") ? "127.0.0.1" : ip;
    }
}

新增一个接口:

@RequestMapping("getIpAddrByNginx")
public String getIpAddrByNginx(HttpServletRequest request){
    //获取nginx带过来的x-forwarded-for字段
    String xForwardedFor = request.getHeader("x-forwarded-for");
    String xRealIp = request.getHeader("x-real-ip");
    String clientIp = null;
    try {
        clientIp = IpUtil.getIp(request);
    } catch (Exception e) {
        e.printStackTrace();
    }
    return "x-forwarded-for的值为:"+xForwardedFor+",x-real-ip的值为:"+xRealIp+",客户端真实IP为="+clientIp;
}

重新发布此服务,浏览器访问:http://111.231.119.253/getIpAddrByNginx结果为:

image

可以看到,结合上面关于X-Forwarded-For的介绍,我们可以知道,对于部署了 Nginx 这样反向代理的 Web 应用,在正确配置了 Set Header 行为后,可以使用 Nginx 传过来的 X-Real-IPX-Forwarded-For 的第一个IP作为客户端实际的IP。

2.3 伪造X-Forwarded-For参数

一般客户端(例如浏览器)发送HTTP请求是没有X-Forwarded-For头,当请求到达第一个代理服务器时,代理服务器会加上X-Forwarded-For请求头,并将值设为客户端的IP地址(也就是最左边的第一个值),后面如果还有多个代理,会依次将IP追加到X-Forwarded-For头最右边,最终请求到达Web服务器,应用通过获取X-Forwarded-For头取左边第一个IP即为客户端真实IP。

正如上面所说,X-Forwarded-For只是追加地址,就会给伪造IP可乘之机。

但是如果客户端在发起请求时,请求头上带上一个伪造的X-Forwarded-For,由于后续每层代理只会追加不会覆盖,那么最终到达服务器时,获取到的左边第一个IP地址将会是客户端伪造的IP。也就是上面Java代码中getClientIp()方法获取到的IP地址很可能是伪造的IP地址。

伪造X-Forwarded-For头的方法很简单,例如Postman就可以轻松做到:

image

之前的代码逻辑获取到的IP就不是真实的客户端IP了,而是构造出来的第一个IP。如何解决这样的问题呢?

一个思路是:从右向左遍历。遍历时可以根据正则表达式剔除掉内网IP和已知的代理服务器本身的IP(例如192.168开头的),那么拿到的第一个非剔除IP就会是一个可信任的客户端IP。这种方法的巧妙之处在于,即时伪造X-Forwarded-For,那么请求到达应用服务器时,伪造的IP也会在X-Forwarded-For值的左边,从右向左遍历就可以避免取到这些伪造的IP地址。

比如可以通过这个工具类IpCheckutil来对IP做校验,结合上面的那个IPUtil使用:

public class IpCheckutil {
    public static final String _255 = "(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)";
    public static final Pattern pattern = Pattern.compile("^(?:" + _255 + "\\.){3}" + _255 + "$");

    public static String longToIpV4(long longIp) {
        int octet3 = (int) ((longIp >> 24) % 256);
        int octet2 = (int) ((longIp >> 16) % 256);
        int octet1 = (int) ((longIp >> 8) % 256);
        int octet0 = (int) ((longIp) % 256);
        return octet3 + "." + octet2 + "." + octet1 + "." + octet0;
    }

    public static long ipV4ToLong(String ip) {
        String[] octets = ip.split("\\.");
        return (Long.parseLong(octets[0]) << 24) + (Integer.parseInt(octets[1]) << 16)
                + (Integer.parseInt(octets[2]) << 8) + Integer.parseInt(octets[3]);
    }

    public static boolean isIPv4Private(String ip) {
        long longIp = ipV4ToLong(ip);
        return (longIp >= ipV4ToLong("10.0.0.0") && longIp <= ipV4ToLong("10.255.255.255"))
                || (longIp >= ipV4ToLong("172.16.0.0") && longIp <= ipV4ToLong("172.31.255.255"))
                || longIp >= ipV4ToLong("192.168.0.0") && longIp <= ipV4ToLong("192.168.255.255");
    }

    public static boolean isIPv4Valid(String ip) {
        return pattern.matcher(ip).matches();
    }

    public static String getIpFromRequest(HttpServletRequest request) {
        String ip;
        boolean found = false;
        if ((ip = request.getHeader("x-forwarded-for")) != null) {
            String[] iparr = ip.split(",");
            int len = iparr.length;
            for(int i = len-1 ; i>0 ; i--){
                //如果都是外网来访问的话,则可以从右向左遍历,排除掉内网的IP地址,第一个非内网IP就是我们要的客户端IP,而前面伪造的IP不会被遍历到
                if (isIPv4Valid(iparr[i].trim()) && !isIPv4Private(iparr[i].trim())) {
                    ip = iparr[i];
                    found = true;
                    break;
                }
            }
        }
        if (!found) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }
}

image

具体的代码获取IP还得根据实际代理情况去调整,因此不是说代码直接copy就可以拿来用的,这也是前文中为什么说获取IP需要格外的小心。

三、HTTP中介之网关

  • 网关可以作为某种翻译器使用,它抽象出了一种能够到达资源的方法。网关是资源和应用程序之间的粘合剂。
  • 网关扮演的是“协议转换器”的角色。

网关和代理最大的区别是:网关可以进行协议转换。

image

根据不同的协议,我们可以将其分类:

  • (HTTP/*)服务器端Web网关:如上图,客户端发送HTTP请求,咱们的网关将其转换为邮件协议。

    • 请求流入原始服务器时,服务器端Web网关会将客户度的HTTP请求转换成其他协议
  • (HTTP/HTTPS)服务器端安全网关

    • 一个组织可以通过网关对所有的输入Web请求加密,以提供额外的隐私和安全性保护。客户端可以用普通的HTTP浏览Web内容,但网关会自动加密用户的对话
  • (HTTPS/HTTP)客户端安全加速器网关

    • 将HTTPS/HTTP网关作为安全加速器使用的情况是越来越多了。这些HTTPS/HTTP网关位于Web服务器之前,通常作为不可见的拦截网关或反向代理使用。它们接收安全的HTTPS流量,对安全流量进行解密,并向Web服务器发送普通的http请求。
    • 这些网关中通常包含专用的解密硬件,以比原始服务器有效得多的方式来解密安全流量,以减轻原始服务器的负荷。这些网关在网关和原始服务器之间发送的是未加密的流量,所以,要谨慎使用,确保网关和原始服务器之间的网络是安全的。
  • 资源网关

    • 作用类似于接口。比如用户在浏览器上购物,都是跟后台的接口进行交互的,比如获取订单,可以调用订单接口去获取。
  • https://www.jianshu.com/p/23ce44445e75

  • https://cloud.tencent.com/developer/article/1519081

  • https://imququ.com/post/x-forwarded-for-header-in-http.html

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值