伺服器受到網路攻擊時,如何獲取請求客戶端的真實 IP?

  • 2021 年 8 月 25 日
  • 筆記

網路攻擊

前不久公司遭受了一次網路攻擊。

早晨剛到公司,就發現登錄介面的調用次數飆升,很快就確認是被惡意攻擊,讓安全部門做網關入口針對對方 IP 加了限制。

並統一對所有的 IP 加了調用的頻率限制。

登錄

基本每一家公司都會有登錄介面,然而無論大小,多少都會存在一些問題。

最核心的準則這裡稍微提一下,以後有機會展開:

(1)密碼一定要加密存儲(而且不能是簡單的 MD5),日誌一定要脫敏。

(2)登錄介面一定要添加驗證碼,防止介面被惡意調用的第一步

(3)禁止用戶使用弱口令,弱口令字典可自行 github

(4)異地登錄等要求用戶進行二次驗證

當然,個人認為最好的方式還是限制調用的次數和頻率。

比如銀行的密碼錯誤 3 次,直接凍結 24H 之類的。

限流

限流功能,建議統一做在網關這一層,沒有必要每個業務應用都去實現。

網關和限流的框架以前寫過,感興趣的話以後可以重點講一下。

獲取 IP

本來被攻擊也是家常便飯,時間一久,也就淡忘了。

不過同事最近接了一個需求,其中涉及到獲取 HTTP 請求客戶端的真實 IP。

機智的小夥伴們,能說出你平時獲取 IP 的方法嗎?百度也行。

複製黏貼

同事接到這個需求,感覺也不難。

巧的是以前應用里就有獲取 IP 的程式碼,更巧的查了一下,發現獲取的不對。

於是就去百度了一下,複製黏貼,三下五除二上線了。

比如:

public static String getIp(final HttpServletRequest req) {
    String ip = req.getHeader("X-Forwarded-For");
    if (StringUtil.isEmpty(ip)) {
        ip = req.getHeader("Proxy-Client-IP");
    }
    if (StringUtil.isEmpty(ip)) {
        ip = req.getHeader("WL-Proxy-Client-IP");
    }
    if (StringUtil.isEmpty(ip)) {
        ip = req.getRemoteAddr();
    }
    return ip;
}

稍微靠譜點的:

public class IPUtils {

    public static String getClientAddress(HttpServletRequest request) {
        if (request == null) {
            return "unknown";
        }
        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("X-Forwarded-For");
        }
        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.getHeader("X-Real-IP");
        }

        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip.equals("0:0:0:0:0:0:0:1") ? "127.0.0.1" : ip;
    }

}

第二天發現,獲取的 IP 地址還是不對,白忙活一場,還要重新上線。

應該如何獲取 IP 呢?

其實獲取客戶端的真實 IP,首先和我們的應用架構是緊密相關的。

我們先看一下網上最常見的解釋:

網路架構主要分為兩種情況:

(1)客戶端未經過代理,直接訪問伺服器端(nginx,squid,haproxy);

(2)客戶端通過多級代理,最終到達伺服器端(nginx,squid,haproxy);

客戶端請求資訊都包含在HttpServletRequest中,可以通過方法 getRemoteAddr() 獲得該客戶端IP。

方式一形式,可以直接獲得該客戶端真實IP。

方式二中通過代理的形式,此時經過多級反向的代理,通過方法 getRemoteAddr() 得不到客戶端真實IP,可以通過 x-forwarded-for 等獲得轉發後請求資訊。

當客戶端請求被轉發,IP將會追加在其後並以逗號隔開,例如:10.47.103.13,4.2.2.2,10.96.112.230。

術業有專攻

吸取教訓

這次同事吸取了上一次的教育,去和安全部門請教了一波。

得到的答案是:

真實客戶端 IP 的建議獲取方式如下:

X-Client-IP ,X-Real-IP, X-Real-Ip, WL-Proxy-Client-IP,PROXY_CLIENT_IP, X_Forwarded_For,  request.getRemoteAddr()     

屬性

這些都是個啥?

在這裡插入圖片描述

第一次看感覺還是有點蒙,於是去簡單整理,便於以後查閱。

X-Forwarded-For

這是一個 Squid 開發的欄位,只有在通過了HTTP代理或者負載均衡伺服器時才會添加該項。

格式為 X-Forwarded-For:client1,proxy1,proxy2,一般情況下,第一個ip為客戶端真實ip,後面的為經過的代理伺服器ip。

現在大部分的代理都會加上這個請求頭。

Proxy-Client-IP/WL-Proxy-Client-IP

這個一般是經過 apache http 伺服器的請求才會有,用apache http做代理時一般會加上Proxy-Client-IP請求頭,而WL-Proxy-Client-IP是他的weblogic插件加上的頭。

HTTP_CLIENT_IP

有些代理伺服器會加上此請求頭

X-Real-IP

nginx代理一般會加上此請求頭。

remote_addr

remote_addr 指的是當前直接請求的客戶端IP地址,它存在於tcp請求體中,是http協議傳輸時自動添加的,不受請求頭header所控制。

所以,當客戶端與伺服器間不存在任何代理時,通過remote_addr獲取客戶端IP地址是最準確的,也是最安全的。

注意

這些請求頭都不是http協議里的標準請求頭,也就是說這個是各個代理伺服器自己規定的表示客戶端地址的請求頭。

即使請求經過的代理都會按自己的規範附上代理請求頭,上面的屬性也不能確保獲得的一定是客戶端ip。

最重要的一點,請求頭都是可以偽造的

說了這麼多,感覺這裡面水太深了,建議最好還是選擇放棄深究,讓我們直接上程式碼。

在這裡插入圖片描述

java 實現

老馬把上面的實現做了簡單的整理,java 初步實現如下:

import com.github.houbb.heaven.util.lang.StringUtil;
import com.github.houbb.web.core.dto.IpInfo;

import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.List;

/**
 * IP 工具類
 *
 * @author 老馬嘯西風
 * @since 1.0.0
 */
public final class IpUtil {

    private IpUtil(){}

    /**
     * 獲取所有的 IP 資訊
     * @param request 入參
     * @return 結果
     * @since 0.0.3
     */
    public static IpInfo getAllIpInfo(HttpServletRequest request) {
        IpInfo ipInfo = new IpInfo();
        ipInfo.setClientIp(request.getHeader("X-Client-IP"));
        ipInfo.setRealIP(request.getHeader("X-Real-IP"));
        ipInfo.setRealIp(request.getHeader("X-Real-Ip"));
        ipInfo.setWlProxyClientIP(request.getHeader("WL-Proxy-Client-IP"));
        ipInfo.setProxyClientIp(request.getHeader("PROXY_CLIENT_IP"));
        ipInfo.setForwardedFor(request.getHeader("X_Forwarded_For"));
        ipInfo.setRemoteAddress(request.getRemoteAddr());
        return ipInfo;
    }

    /**
     * 獲取 ip 資訊
     * @param request 請求
     * @return 結果
     * @since 1.0.0
     */
    private String getIp(HttpServletRequest request) {
        List<String> keyList = Arrays.asList(
                "X-Client-IP",
                "X-Real-IP",
                "X-Real-Ip",
                "WL-Proxy-Client-IP",
                "PROXY_CLIENT_IP",
                "X_Forwarded_For"
        );

        for(String key : keyList) {
            String ip = request.getHeader(key);
            // 是合法的 IP,直接返回
            if(StringUtil.isNotEmptyTrim(ip)
                && !"unknown".equalsIgnoreCase(ip)) {
                return ip;
            }
        }

        // 結果可能為包含 , 好的多個
        return request.getRemoteAddr();
    }

    /**
     * 獲取 ip 資訊
     * @param request 請求
     * @return 結果
     * @since 1.0.0
     */
    private String getSingleIp(HttpServletRequest request) {
        String ip = getIp(request);

        if(ip.contains(",")) {
            return ip.split(",")[0];
        }

        return ip;
    }

}

小結

獲取客戶端真實 IP 是一個很基礎的方法,但是也是很重要的一個方法。

看起來不起眼的一個方法,寫的不好,可能整個公司的安全就是一張紙。

網上的資料參差不齊,使用時注意甄別。

包括本篇,畢竟老馬對網路安全也是一點不懂。

我是老馬,期待與你的下次重逢。

在這裡插入圖片描述