• java nginx等代理或网关转发请求后获取客户端的ip地址,原理


    在没有网关或者反向代理软件情况下,java里获取客户端ip地址的方法是request.getRemoteAddr()
    先解释下http协议和TCP协议:
    网页默认是进行http连接了,http协议即超文本传送协议(Hypertext Transfer Protocol ),是工作TCP协议之上的协议
    tcp连接需要三次握手,也就是调用底层的socket进行连接确认。而socket连接需要知道通信双方的ip地址和端口才可以进行数据的正确传输。
    由上面可以知道request.getRemoteAddr()方法其实就是获取的连接的客户端socket的ip地址。
     
    但如果我们客户端将要发送接口请求先发送到一台代理请求服务器或者网关后,再由他们进行数据请求,
    这时我们使用上面的getRemoteAddr()方法获取到了ip地址就是代理请求服务器或网关的ip地址。
    而不是真实的客户端ip地址
    也就是说通过nginx等反向代理软件时,不能通过这个方法获取客户端真是ip地址。
     
    如真实ip:223.104.1.240 广东省广州市 移动
    访问域名www.zwh.com
    nginx所在服务器193.112.28.110
    这个域名和nginx所在服务器绑定,
    客户端访问时,如www.zwh.com/项目名/index.jsp
    这个访问了是nginx的80端口,然后nginx再将这个请求重定向到localhost:8080/项目名/index.jsp
    而nginx在将请求进行重定向时会在请求头header中增加一个x-forwarded-for信息,用以跟踪原有客户端ip地址和原来客户端请求的服务器地址
    存放的就是x-forwarded-for223.104.1.240
     
    对应java方法来说,request.getRemoteAddr(),因为直接请求的客户端是nginx,在同一服务器上,所以这个方法获取到的ip地址是127.0.0.1或者193.112.28.110
     
    要获取真实ip地址就要使用request.getHeader("x-forwarded-for")获取请求头部真实ip地址
    ,但是当xxx.conf配置server节点时,forwarded_for设置为off,则X-Forwarded-For:unknown
    默认forwarded_for是on
     
    先看下nginx的部分配置
    location / {
                proxy_redirect off;
                proxy_pass  http://localhost:8080;
                proxy_set_header  Host $host;
                proxy_set_header   X-real-ip $remote_addr;
                proxy_set_header    X-Forwarded-For $proxy_add_x_forwarded_for;
            }

    继续使用上面数据

    这个$host 其实是nginx服务器本身的ip地址:193.112.28.110
     $remote_addr这个地址是想nginx请求的客户端地址:223.104.1.240
     $proxy_add_x_forwarded_for 这个变量和remote_addr的区别是
    proxy_add_x_forwarded_for会将经过的所有代理ip相加起来,
    remote_addr则只是获取到直接连接到nginx的ip地址
      在有多个代理服务器的情况下,会将代理服务器的X-Forwarded-For头部的ip地址和客户端的ip地址相加,
      即:X-Forwarded-For请求头部header的ip字符串  + 直接向nginx服务器发送请求的客户端地址
       换个表达请求的过程是client--->nginx1--->nginx2---->tomcat
      而假设client:ip1, nginx1:ip2, nginx2:ip3, tomcat:ip4
      那么请求request到tomcat时,
      请求头部header中的X-Forwarded-For信息是=ip1,ip2,ip3

    简单了方法

    public String getIpAddr(HttpServletRequest request) {
        String ip = request.getHeader("x-forwarded-for");
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }

    上面三个请求头,大部分的代理或者网关都会加上x-forwarded-for:clientip,proxy1ip,proxy2ip

    Proxy-Client-IP这个一般是apache,http服务器才会有,经过apache代理时,apache会在请求头加上x-forwarded-for和Proxy-Client-IP

    WL-Proxy-Client-IP这个一般是weblogic才会加上的头部

    注意:

    1、这些请求头都不是http协议里的标准请求头,也就是说这个是各个代理服务器自己规定的表示客户端地址的请求头。如果哪天有一个代理服务器软件用oooo-client-ip这个请求头代表客户端请求,那上面的代码就不行了。

    2、这些请求头不是代理服务器一定会带上的,网络上的很多匿名代理就没有这些请求头,所以获取到的客户端ip不一定是真实的客户端ip。代理服务器一般都可以自定义请求头设置。

    3、即使请求经过的代理都会按自己的规范附上代理请求头,上面的代码也不能确保获得的一定是客户端ip。不同的网络架构,判断请求头的顺序是不一样的。

    4、最重要的一点,请求头都是可以伪造的。如果一些对客户端校验较严格的应用(比如投票)要获取客户端ip,应该直接使用ip = request.getRemoteAddr (),虽然获取到的可能是代理的ip而不是客户端的ip,但这个获取到的ip基本上是不可能伪造的,也就杜绝了刷票的可能。(有分析说arp欺骗+syn有可能伪造此ip,如果真的可以,这是所有基于TCP协议都存在的漏洞),这个ip是tcp连接里的ip。
    链接

    ip工具类

    import javax.servlet.http.HttpServletRequest;
    /**
    * IP地址工具类
    * @author xudongdong
    *
    */
    public class IpUtil {
        
        /**
         * 私有化构造器
         */
        private IpUtil() {
        }
        /**
         * 获取真实IP地址
         * <p>使用getRealIP代替该方法</p>
         * @param request req
         * @return ip
         */
        @Deprecated
        public static String getClinetIpByReq(HttpServletRequest request) {
            // 获取客户端ip地址
            String clientIp = request.getHeader("x-forwarded-for");
            if (clientIp == null || clientIp.length() == 0 || "unknown".equalsIgnoreCase(clientIp)) {
                clientIp = request.getHeader("Proxy-Client-IP");
            }
            if (clientIp == null || clientIp.length() == 0 || "unknown".equalsIgnoreCase(clientIp)) {
                clientIp = request.getHeader("WL-Proxy-Client-IP");
            }
            if (clientIp == null || clientIp.length() == 0 || "unknown".equalsIgnoreCase(clientIp)) {
                clientIp = request.getRemoteAddr();
            }
            /*
             * 对于获取到多ip的情况下,找到公网ip.
             */
            String sIP = null;
            if (clientIp != null && !clientIp.contains("unknown") && clientIp.indexOf(",") > 0) {
                String[] ipsz = clientIp.split(",");
                for (String anIpsz : ipsz) {
                    if (!isInnerIP(anIpsz.trim())) {
                        sIP = anIpsz.trim();
                        break;
                    }
                }
                /*
                 * 如果多ip都是内网ip,则取第一个ip.
                 */
                if (null == sIP) {
                    sIP = ipsz[0].trim();
                }
                clientIp = sIP;
            }
            if (clientIp != null && clientIp.contains("unknown")){
                clientIp =clientIp.replaceAll("unknown,", "");
                clientIp = clientIp.trim();
            }
            if ("".equals(clientIp) || null == clientIp){
                clientIp = "127.0.0.1";
            }
            return clientIp;
        }
        
        /**
         * 判断IP是否是内网地址
         * @param ipAddress ip地址
         * @return 是否是内网地址
         */
        public static boolean isInnerIP(String ipAddress) {
            boolean isInnerIp;
            long ipNum = getIpNum(ipAddress);
            /**   
            私有IP:A类  10.0.0.0-10.255.255.255   
                   B类  172.16.0.0-172.31.255.255   
                   C类  192.168.0.0-192.168.255.255   
            当然,还有127这个网段是环回地址   
            **/
            long aBegin = getIpNum("10.0.0.0");
            long aEnd = getIpNum("10.255.255.255");
            
            long bBegin = getIpNum("172.16.0.0");
            long bEnd = getIpNum("172.31.255.255");
            
            long cBegin = getIpNum("192.168.0.0");
            long cEnd = getIpNum("192.168.255.255");
            isInnerIp = isInner(ipNum, aBegin, aEnd) || isInner(ipNum, bBegin, bEnd) || isInner(ipNum, cBegin, cEnd)
                    || ipAddress.equals("127.0.0.1");
            return isInnerIp;
        }
        private static long getIpNum(String ipAddress) {
            String[] ip = ipAddress.split("\.");
            long a = Integer.parseInt(ip[0]);
            long b = Integer.parseInt(ip[1]);
            long c = Integer.parseInt(ip[2]);
            long d = Integer.parseInt(ip[3]);
            return a * 256 * 256 * 256 + b * 256 * 256 + c * 256 + d;
        }
        
        private static boolean isInner(long userIp, long begin, long end) {
            return (userIp >= begin) && (userIp <= end);
        }
        public static String getRealIP(HttpServletRequest request){
            // 获取客户端ip地址
            String clientIp = request.getHeader("x-forwarded-for");
            if (clientIp == null || clientIp.length() == 0 || "unknown".equalsIgnoreCase(clientIp)) {
                clientIp = request.getRemoteAddr();
            }
            String[] clientIps = clientIp.split(",");
            if(clientIps.length <= 1) return clientIp.trim();
            // 判断是否来自CDN
            if(isComefromCDN(request)){
                if(clientIps.length>=2) return clientIps[clientIps.length-2].trim();
            }
            return clientIps[clientIps.length-1].trim();
        }
        private static boolean isComefromCDN(HttpServletRequest request) {
            String host = request.getHeader("host");
            return host.contains("www.189.cn") ||host.contains("shouji.189.cn") || host.contains(
                    "image2.chinatelecom-ec.com") || host.contains(
                    "image1.chinatelecom-ec.com");
        }
    }
     
  • 相关阅读:
    Vue 多环境的配置 look
    01 java基本类型和包装类型的区别? look
    03 java自动装箱与拆箱了解吗?原理是什么? look
    Windows下MySQL的安装和删除 look
    02 java包装类型的缓存机制 look
    test
    keepalived 主备搭建及配置
    rename批量重命名文件名
    keepalived执行stop命令无法退出进程问题
    职场PUA
  • 原文地址:https://www.cnblogs.com/gne-hwz/p/9922301.html
Copyright © 2020-2023  润新知