簡介
在我們日常項目中,可能遇到這么一個需求“獲取用戶請求的真實IP”,比如說打印用戶登錄信息,用戶ip白名單等。那該如何準確地獲取用戶請求真實IP呢?本文主要圍繞著沒有使用代理的服務和使用了代理的服務兩種場景展開討論。
溫馨提示:本文的代理服務器使用Nginx,后端服務器使用tomcat。
沒有使用代理的服務
如圖所示,用戶在瀏覽器發起一個請求,直接到tomcat服務器。因此可以通過
servletRequest.getRemoteAddr()
獲得。
使用了代理的服務
如圖所示,用戶在瀏覽器發起一個請求,先經過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相關參數賦值;