獲取用戶真實IP

簡介

在我們日常項目中,可能遇到這么一個需求“獲取用戶請求的真實IP”,比如說打印用戶登錄信息,用戶ip白名單等。那該如何準確地獲取用戶請求真實IP呢?本文主要圍繞著沒有使用代理的服務使用了代理的服務兩種場景展開討論。

溫馨提示:本文的代理服務器使用Nginx,后端服務器使用tomcat。

沒有使用代理的服務

image

如圖所示,用戶在瀏覽器發起一個請求,直接到tomcat服務器。因此可以通過servletRequest.getRemoteAddr()獲得。

使用了代理的服務

image

如圖所示,用戶在瀏覽器發起一個請求,先經過Proxy1,接著經過Proxy2,再經過Proxy3,最后才到達tomcat服務器。

那此時,通過servletRequest.getRemoteAddr()是否可以獲得用戶的真實IP呢?答案顯然是不可以的。servletRequest.getRemoteAddr()只能獲取到tomcat服務器前面的Proxy3的ip。

那么我們應該如何獲取用戶真實ip呢?我們先來認識下X-Forwarded-For

X-Forwarded-For

X-Forwarded-For(簡稱XFF)是一個 HTTP 擴展頭部。HTTP/1.1(RFC 2616)協議并沒有對它的定義,它最開始是由 Squid 這個緩存代理軟件引入,用來表示 HTTP 請求端真實IP。如今它已經成為事實上的標準,被各大 HTTP 代理、負載均衡等轉發服務廣泛使用,并被寫入 RFC 7239(Forwarded HTTP Extension)標準之中。

參考上圖,假設Proxy1的nginx配置為:

   location / {
      proxy_pass Proxy2;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
   }

Proxy2的nginx配置為:

   location / {
      proxy_pass Proxy3;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
   }

Proxy3的nginx配置為:

   location / {
      proxy_pass tomcat服務器;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
   }

X-Forwarded-For數據格式:X-Forwarded-For: client_IP, proxy1_IP, proxy2_IP

  • client_IP:客戶端真實ip;
  • proxy1_IP:第一個代理服務器IP;
  • proxy2_IP:第二個代理服務器IP;

可能有人會有疑問,怎么沒有proxy3_IP呢?首先了解下X-Forwarded-For數據生成的原理:

  • 瀏覽器 -> Proxy1,Proxy1會將瀏覽器所在的IP寫入到X-Forwarded-For: client_IP
  • Proxy1 -> Proxy2,Proxy2會將Proxy1所在的IP追加到X-Forwarded-For: client_IP, proxy1_IP
  • Proxy2 -> Proxy3,Proxy3會將Proxy2所在的IP追加到X-Forwarded-For: client_IP, proxy1_IP, proxy2_IP
  • Proxy3 -> tomcat服務器,tomcat不需要追加;

那此時我們怎么獲取到proxy3_IP呢?servletRequest.getRemoteAddr()即是proxy3_IP。

因此,在使用了代理服務器的場景,后臺服務器可以通過servletRequest.getHeader("X-Forwarded-For")截取第一個ip得到用戶請求真實ip。

問題延伸

如何獲取用戶真實協議、端口?

Proxy的nginx可以配置為:

   location / {
      proxy_pass XXXX;
      # 協議傳遞給下一個代理(或者后臺服務器)
      proxy_set_header X-Forwarded-Proto $scheme; 
      # 端口號傳遞給下一個代理(或者后臺服務器)
      proxy_set_header X-Forwarded-Port $server_port;
   }

注意,這里跟X-Forwarded-For有點區別,X-Forwarded-For是采用追加的方式,X-Forwarded-Proto和X-Forwarded-Port是直接傳遞過去,就是最終只有一個。

tomcat是如何處理X-Forwarded-For、X-Forwarded-Proto、X-Forwarded-Port?

添加配置

  • server.xml方式:
<Valve className="org.apache.catalina.valves.RemoteIpValve" remoteIpHeader="X-Forwarded-For" protocolHeader="X-Forwarded-Proto" portHeader="X-Forwarded-Port"/>
  • springboot方式:
server.tomcat.remote-ip-header=X-Forwarded-For
server.tomcat.protocol-header=X-Forwarded-Proto
server.tomcat.port-header=X-Forwarded-Port
# 2.2.0之前使用這個配置
server.use-forward-headers=true
# 2.2.0以上使用下面這個
# server.forward-headers-strategy=NATIVE

RemoteIpVavle

RemoteIpVavle意思是遠程ip閥門,也就是tomcat用來處理X-Forwarded-For、X-Forwarded-Proto、X-Forwarded-Port的類。接下來我們來看這么一段源碼(以tomcat-embed-core:9.0.31為例,不同版本可能代碼會稍微有點差異):

 /**
    * 代碼還是按照之前的例子:瀏覽器(https) -> Proxy1(http) -> Proxy2(http) -> Proxy3(http) -> tomcat服務器
    * 每個nginx配置如下:
    * location / {
    *   proxy_pass XXXX;
    *   # 協議傳遞給下一個代理(或者后臺服務器)
    *   proxy_set_header X-Forwarded-Proto $scheme; 
    *   # 端口號傳遞給下一個代理(或者后臺服務器)
    *   proxy_set_header X-Forwarded-Port $server_port;
    *   # ip追加并傳給下一個代理
    *   proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    * }
     * {@inheritDoc}
     */
    @Override
    public void invoke(Request request, Response response) throws IOException, ServletException {
        // Proxy3的IP
        final String originalRemoteAddr = request.getRemoteAddr();
        // Proxy3的主機
        final String originalRemoteHost = request.getRemoteHost();
        // tomcat的協議比如http
        final String originalScheme = request.getScheme();
        // tomcat的協議是否安全
        final boolean originalSecure = request.isSecure();
        // tomcat服務器名稱(如果配置了proxy_set_header Host $http_host; 就會取最外層代理的服務器名稱)
        final String originalServerName = request.getServerName();
        // tomcat服務器主機名
        final String originalLocalName = request.getLocalName();
        // tomcat的端口(如果配置了proxy_set_header Host $http_host; 就會取最外層代理的服務器端口)
        final int originalServerPort = request.getServerPort();
        // tomcat服務器端口
        final int originalLocalPort = request.getLocalPort();
        // X-Forwarded-By請求頭數據
        final String originalProxiesHeader = request.getHeader(proxiesHeader);
        // X-Forwarded-For請求頭數據
        final String originalRemoteIpHeader = request.getHeader(remoteIpHeader);
        /**
         * internalProxies即內網代理IP,這些默認都是受信任的IP,
         * 判斷Proxy3服務器ip是否是內網ip
         *
        */ 
        boolean isInternal = internalProxies != null &&
                internalProxies.matcher(originalRemoteAddr).matches();
        // 如果Proxy3服務器ip是內網ip,或者是trustedProxies(自定義信任ip列表)的ip
        if (isInternal || (trustedProxies != null &&
                trustedProxies.matcher(originalRemoteAddr).matches())) {
            String remoteIp = null;
            // In java 6, proxiesHeaderValue should be declared as a java.util.Deque
            LinkedList<String> proxiesHeaderValue = new LinkedList<>();
            StringBuilder concatRemoteIpHeaderValue = new StringBuilder();

            // 獲取X-Forwarded-For請求頭ip列表
            for (Enumeration<String> e = request.getHeaders(remoteIpHeader); e.hasMoreElements();) {
                if (concatRemoteIpHeaderValue.length() > 0) {
                    concatRemoteIpHeaderValue.append(", ");
                }

                concatRemoteIpHeaderValue.append(e.nextElement());
            }

            // 獲取X-Forwarded-For請求頭ip列表置換成數組,例如:[192.168.1.10, 192.168.2.10]
            String[] remoteIpHeaderValue = commaDelimitedListToStringArray(concatRemoteIpHeaderValue.toString());
            int idx;
            // 如果是自定義信任ip處理,則將Proxy3_IP加入到proxiesHeaderValue鏈表
            if (!isInternal) {
                proxiesHeaderValue.addFirst(originalRemoteAddr);
            }
            // loop on remoteIpHeaderValue to find the first trusted remote ip and to build the proxies chain
            // 從右向左循環X-Forwarded-For請求頭ip列表
            for (idx = remoteIpHeaderValue.length - 1; idx >= 0; idx--) {
                String currentRemoteIp = remoteIpHeaderValue[idx];
                remoteIp = currentRemoteIp;
                // 如果是內網ip則不作處理
                if (internalProxies !=null && internalProxies.matcher(currentRemoteIp).matches()) {
                    // do nothing, internalProxies IPs are not appended to the
                    
                    // 如果是自定義信任ip則加入到proxiesHeaderValue鏈表中
                } else if (trustedProxies != null &&
                        trustedProxies.matcher(currentRemoteIp).matches()) { 
                    proxiesHeaderValue.addFirst(currentRemoteIp);
                } else {
                    // 如果沒有匹配上,終止循環;否則消耗一次idx
                    idx--; // decrement idx because break statement doesn't do it
                    break;
                }
            }
            // continue to loop on remoteIpHeaderValue to build the new value of the remoteIpHeader
            /* 
             * 如果之前idx沒有被消耗完,則繼續循環,加入newRemoteIpHeaderValue鏈表
             * 假設X-Forwarded-For請求頭ip列表[223.104.6.34, 223.104.7.45, 
             * 192.168.2.10],被消耗后,剩余[223.104.6.34]。
             */
            LinkedList<String> newRemoteIpHeaderValue = new LinkedList<>();
            for (; idx >= 0; idx--) {
                String currentRemoteIp = remoteIpHeaderValue[idx];
                newRemoteIpHeaderValue.addFirst(currentRemoteIp);
            }
            // 客戶端ip或者第一個非內網并且非自定義信任的ip
            if (remoteIp != null) {
               
                request.setRemoteAddr(remoteIp);
                request.setRemoteHost(remoteIp);
                // proxiesHeaderValue鏈表為空,即沒有匹配到自定義信任ip,
                // 則刪除X-Forwarded-By請求頭信息;否則設置該請求頭信息
                if (proxiesHeaderValue.size() == 0) {
                    request.getCoyoteRequest().getMimeHeaders().removeHeader(proxiesHeader);
                } else {
                    String commaDelimitedListOfProxies = listToCommaDelimitedString(proxiesHeaderValue);
                    request.getCoyoteRequest().getMimeHeaders().setValue(proxiesHeader).setString(commaDelimitedListOfProxies);
                }
                // newRemoteIpHeaderValue鏈表為空,即X-Forwarded-For都匹配到內網ip,
                // 則刪除X-Forwarded-For請求頭信息;否則設置該請求頭信息
                if (newRemoteIpHeaderValue.size() == 0) {
                    request.getCoyoteRequest().getMimeHeaders().removeHeader(remoteIpHeader);
                } else {
                    String commaDelimitedRemoteIpHeaderValue = listToCommaDelimitedString(newRemoteIpHeaderValue);
                    request.getCoyoteRequest().getMimeHeaders().setValue(remoteIpHeader).setString(commaDelimitedRemoteIpHeaderValue);
                }
            }
            // 判斷tomcat是否配置了 X-Forwarded-Proto協議請求頭
            if (protocolHeader != null) {
                String protocolHeaderValue = request.getHeader(protocolHeader);
                if (protocolHeaderValue == null) {
                    // Don't modify the secure, scheme and serverPort attributes
                    // of the request
                    
                    // 如果是https協議,則重新設置secure=true,scheme=https,端口
                } else if (isForwardedProtoHeaderValueSecure(protocolHeaderValue)) { 
                    request.setSecure(true);
                    request.getCoyoteRequest().scheme().setString("https");
                    setPorts(request, httpsServerPort);
                } else {// 其他則重新設置secure=false,scheme=http,端口
                    request.setSecure(false);
                    request.getCoyoteRequest().scheme().setString("http");
                    setPorts(request, httpServerPort);
                }
            }
            // 判斷tomcat是否配置了X-Forwarded-Host,如果有則設置serverName、localName為Proxy1的主機
            if (hostHeader != null) {
                String hostHeaderValue = request.getHeader(hostHeader);
                if (hostHeaderValue != null) {
                    try {
                        int portIndex = Host.parse(hostHeaderValue);
                        if (portIndex > -1) {
                            log.debug(sm.getString("remoteIpValve.invalidHostWithPort", hostHeaderValue, hostHeader));
                            hostHeaderValue = hostHeaderValue.substring(0, portIndex);
                        }

                        request.getCoyoteRequest().serverName().setString(hostHeaderValue);
                        if (isChangeLocalName()) {
                            request.getCoyoteRequest().localName().setString(hostHeaderValue);
                        }

                    } catch (IllegalArgumentException iae) {
                        log.debug(sm.getString("remoteIpValve.invalidHostHeader", hostHeaderValue, hostHeader));
                    }
                }
            }

            request.setAttribute(Globals.REQUEST_FORWARDED_ATTRIBUTE, Boolean.TRUE);

            if (log.isDebugEnabled()) {
                log.debug("Incoming request " + request.getRequestURI() + " with originalRemoteAddr [" + originalRemoteAddr +
                          "], originalRemoteHost=[" + originalRemoteHost + "], originalSecure=[" + originalSecure +
                          "], originalScheme=[" + originalScheme + "], originalServerName=[" + originalServerName +
                          "], originalServerPort=[" + originalServerPort +
                          "] will be seen as newRemoteAddr=[" + request.getRemoteAddr() +
                          "], newRemoteHost=[" + request.getRemoteHost() + "], newSecure=[" + request.isSecure() +
                          "], newScheme=[" + request.getScheme() + "], newServerName=[" + request.getServerName() +
                          "], newServerPort=[" + request.getServerPort() + "]");
            }
        } else {
            if (log.isDebugEnabled()) {
                log.debug("Skip RemoteIpValve for request " + request.getRequestURI() + " with originalRemoteAddr '"
                        + request.getRemoteAddr() + "'");
            }
        }
        // tomcat accesslog相關參數賦值
        if (requestAttributesEnabled) {
            request.setAttribute(AccessLog.REMOTE_ADDR_ATTRIBUTE,
                    request.getRemoteAddr());
            request.setAttribute(Globals.REMOTE_ADDR_ATTRIBUTE,
                    request.getRemoteAddr());
            request.setAttribute(AccessLog.REMOTE_HOST_ATTRIBUTE,
                    request.getRemoteHost());
            request.setAttribute(AccessLog.PROTOCOL_ATTRIBUTE,
                    request.getProtocol());
            request.setAttribute(AccessLog.SERVER_NAME_ATTRIBUTE,
                    request.getServerName());
            request.setAttribute(AccessLog.SERVER_PORT_ATTRIBUTE,
                    Integer.valueOf(request.getServerPort()));
        }
        try {
            // 進入下一個閥門,最后進入controller進行業務處理
            getNext().invoke(request, response);
        } finally {
            request.setRemoteAddr(originalRemoteAddr);
            request.setRemoteHost(originalRemoteHost);
            request.setSecure(originalSecure);
            request.getCoyoteRequest().scheme().setString(originalScheme);
            request.getCoyoteRequest().serverName().setString(originalServerName);
            request.getCoyoteRequest().localName().setString(originalLocalName);
            request.setServerPort(originalServerPort);
            request.setLocalPort(originalLocalPort);

            MimeHeaders headers = request.getCoyoteRequest().getMimeHeaders();
            if (originalProxiesHeader == null || originalProxiesHeader.length() == 0) {
                headers.removeHeader(proxiesHeader);
            } else {
                headers.setValue(proxiesHeader).setString(originalProxiesHeader);
            }

            if (originalRemoteIpHeader == null || originalRemoteIpHeader.length() == 0) {
                headers.removeHeader(remoteIpHeader);
            } else {
                headers.setValue(remoteIpHeader).setString(originalRemoteIpHeader);
            }
        }
    }
    
    /*
     * Considers the value to be secure if it exclusively holds forwards for
     * {@link #protocolHeaderHttpsValue}.
     */
    private boolean isForwardedProtoHeaderValueSecure(String protocolHeaderValue) {
        if (!protocolHeaderValue.contains(",")) {
            return protocolHeaderHttpsValue.equalsIgnoreCase(protocolHeaderValue);
        }
        String[] forwardedProtocols = commaDelimitedListToStringArray(protocolHeaderValue);
        if (forwardedProtocols.length == 0) {
            return false;
        }
        for (int i = 0; i < forwardedProtocols.length; i++) {
            if (!protocolHeaderHttpsValue.equalsIgnoreCase(forwardedProtocols[i])) {
                return false;
            }
        }
        return true;
    }
    /**
     * 端口設置
     *
     */
    private void setPorts(Request request, int defaultPort) {
        int port = defaultPort;
        // 判斷tomcat是否配置了X-Forwarded-Port
        if (portHeader != null) {
            // 獲取請求頭X-Forwarded-Port值,即Proxy1的端口
            String portHeaderValue = request.getHeader(portHeader);
            if (portHeaderValue != null) {
                try {
                    port = Integer.parseInt(portHeaderValue);
                } catch (NumberFormatException nfe) {
                    if (log.isDebugEnabled()) {
                        log.debug(sm.getString(
                                "remoteIpValve.invalidPortHeader",
                                portHeaderValue, portHeader), nfe);
                    }
                }
            }
        }
        request.setServerPort(port);
        if (changeLocalPort) {
            request.setLocalPort(port);
        }
    }

因此,tomcat的RemoteIpVavle作用主要有以下幾點:

  • 通過內網IP默認列表internalProxies,去掉X-Forwarded-For中的內網IP,如果都是內網IP,X-Forwarded-For整個請求頭會被刪掉;

  • 如果X-Forwarded-For中IP在用戶自定義IP信任列表trustedProxies里,則加到X-Forwarded-By請求頭,否則X-Forwarded-For整個請求頭會被刪掉;

  • 如果tomcat配置X-Forwarded-Proto,則按照X-Forwarded-Proto(一般是最外層代理協議)請求頭數據來設置,具體如下:

    • X-Forwarded-Proto請求頭=https,則重新設置secure=true、scheme="https"、端口(默認443);
    • X-Forwarded-Proto請求頭!=null && !=https,則重新設置secure=false、scheme="http"、端口(默認80);
    • 如果tomcat配置X-Forwarded-Port,則按照X-Forwarded-Port(一般是最外層代理端口)請求頭數據設置端口,否則使用默認端口;
  • 如果tomcat配置了X-Forwarded-Host,則按照X-Forwarded-Host(一般是最外層代理主機)請求頭數據設置serverName、localName;

  • tomcat accesslog相關參數賦值;

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,428評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,024評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,285評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,548評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,328評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,878評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,971評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,098評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,616評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,554評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,725評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,243評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,971評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,361評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,613評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,339評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,695評論 2 370

推薦閱讀更多精彩內容