前言
最近組內(nèi)同事在開發(fā)需求時,需要獲取一個第三方線上的所有車型,但是他們的線上服務(wù)對我們的線上服務(wù)器做了白名單。
同事的做法是,單獨拉一個分支,在預(yù)發(fā)暴露一個http接口,用于觸發(fā)這個掉接口拖庫的行為,然后保存到我們的數(shù)據(jù)庫。
我覺得這樣的做法一點也不工程師,會在應(yīng)用內(nèi)冗余很多一次性代碼,而且也沒必要上到預(yù)發(fā)去做這事。
我的第一個思路,在預(yù)發(fā)服務(wù)器通過nginx開代理服務(wù)器,本地電腦連這個代理調(diào)用不就得了,后來因為跟運維部門溝通不順利作罷。
因為我也做過網(wǎng)關(guān)的白名單插件,因此我嘗試性的給本地的請求加了幾個頭。
@Headers({
"X-Real-ip:xxxx",
"x-forwarded-for:xxxx",
"x-remote-IP:xxxx",
})
嗯,果不其然,成功了。
本文會分享獲取ip的一些小知識,以及為何我加的頭能破解ip白名單和如何防范ip白名單被破解。
常用部署架構(gòu)

一般的部署結(jié)構(gòu)就是,一個nginx后面反向代理多個服務(wù)器。
IP獲取原理
- 從應(yīng)用層獲取(L7)
對于http來講,就是從header中獲取。
- 從tcp層獲取(L4)
tcp層的話就是從tcp報文中獲取了,其實就是通過socket api獲取。
tomcat
經(jīng)過研究tomcat,支持從L4和L7獲取ip。
具體代碼見org.apache.catalina.connector.Request#getRemoteAddr
public String getRemoteAddr() {
if (remoteAddr == null) {
coyoteRequest.action(ActionCode.REQ_HOST_ADDR_ATTRIBUTE, coyoteRequest);
remoteAddr = coyoteRequest.remoteAddr().toString();
}
return remoteAddr;
}
coyoteRequest.action(ActionCode.REQ_HOST_ADDR_ATTRIBUTE, coyoteRequest); 用來做緩存優(yōu)化,只有g(shù)et具體某個值的時候,才去做對應(yīng)操作獲取。
對于ActionCode.REQ_HOST_ADDR_ATTRIBUTE的處理邏輯如下
見org.apache.coyote.AbstractProcessor#action
case REQ_HOST_ADDR_ATTRIBUTE: {
if (getPopulateRequestAttributesFromSocket() && socketWrapper != null) {
request.remoteAddr().setString(socketWrapper.getRemoteAddr());
}
break;
}
很明顯能感知到調(diào)用的是socket api了吧。
最終調(diào)用一下方法
org.apache.tomcat.util.net.NioEndpoint.NioSocketWrapper#populateRemoteAddr
protected void populateRemoteAddr() {
SocketChannel sc = getSocket().getIOChannel();
if (sc != null) {
InetAddress inetAddr = sc.socket().getInetAddress();
if (inetAddr != null) {
remoteAddr = inetAddr.getHostAddress();
}
}
}
至于L7,代碼邏輯是找到了,在org.apache.catalina.valves.RemoteIpValve,主要通過x-forwarded-for來兜底,但是SpringBoot下默認沒走這套邏輯,不深入研究了。
需要注意的是,對于以上的部署結(jié)構(gòu),我們從remoteAddr獲取到的是nginx的ip,所以肯定是無效的。
所以tomcat這邊的ip肯定在上游通過header傳下來的。
nginx
第一個知識點,在nginx中通過$remote_addr內(nèi)置變量獲取tcp層的客戶端ip,這是我們需要的ip。
就像這樣
proxy_set_header X-real-ip $remote_addr;
第二個知識點,針對多層代理的情況,可以在每一層的nginx設(shè)置$proxy_add_x_forwarded_for
就像這樣
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
它會吧每一層的ip疊加起來。
比如1.0.0.0訪問2.0.0.0,然后2.0.0.0訪問3.0.0.0。最終我在3.0.0.0看到的X-Forwarded-For
為1.0.0.0,``2.0.0.0。
如果上一層的http請求沒有X-Forwarded-For頭,默認取$remote_addr的值。
測試代碼

java
默認端口8080
@GetMapping("/hello")
public String hello(HttpServletRequest request){
System.out.println(request.getRemoteAddr());
System.out.println(request.getHeader("X-Forwarded-For"));
System.out.println(request.getHeader("X-real-ip"));
System.out.println(request.getHeader("Host"));
return "hello";
}
nginx
關(guān)于nginx使用,參考http://openresty.org/en/
nginx分為2種情況,單層和多層。
下面的是多層的配置,因為我使用最近的一層,就能模擬單層的場景了。
worker_processes 1;
error_log logs/error.log;
events {
worker_connections 1024;
}
http {
server {
listen 8081;
location / {
proxy_set_header Host $host;
proxy_set_header X-real-ip $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://localhost:8080;
}
}
server {
listen 8082;
location / {
proxy_set_header Host $host;
proxy_set_header X-real-ip $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://localhost:8081;
}
}
server {
listen 8083;
location / {
proxy_set_header Host $host;
proxy_set_header X-real-ip $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_pass http://localhost:8082;
}
}
}
保證你的手機和電腦在一個網(wǎng)絡(luò),然后通過ifconfig en0 獲取你的電腦網(wǎng)卡地址,比如我電腦地址為192.168.3.2,我的手機地址為192.168.3.23
通過手機訪問以下地址
http://192.168.3.2:8080/hello
http://192.168.3.2:8081/hello
http://192.168.3.2:8082/hello
http://192.168.3.2:8083/hello
通過電腦調(diào)用以下命令
curl http://localhost:8083/hello -H "Host:192.178.1.1"
curl http://localhost:8083/hello -H "X-Forwarded-For:192.178.1.1"
curl http://localhost:8082/hello -H "X-Forwarded-For:192.178.1.1"
對應(yīng)輸出分別為
#http://192.168.3.2:8080/hello
192.168.3.23
null
null
192.168.3.2:8080
#http://192.168.3.2:8081/hello
127.0.0.1
192.168.3.23
192.168.3.23
192.168.3.2
#http://192.168.3.2:8082/hello
0:0:0:0:0:0:0:1
192.168.3.23, 127.0.0.1
127.0.0.1
192.168.3.2
#http://192.168.3.2:8083/hello
0:0:0:0:0:0:0:1
192.168.3.23, 127.0.0.1
127.0.0.1
192.168.3.2
#curl http://localhost:8083/hello -H "Host:192.178.1.1"
0:0:0:0:0:0:0:1
127.0.0.1, 127.0.0.1, 127.0.0.1
127.0.0.1
192.178.1.1
#curl http://localhost:8083/hello -H "X-Forwarded-For:192.178.1.1"
127.0.0.1
127.0.0.1, 127.0.0.1, 127.0.0.1
127.0.0.1
localhost
#curl http://localhost:8082/hello -H "X-Forwarded-For:192.178.1.1"
127.0.0.1
192.178.1.1, 127.0.0.1, 127.0.0.1
127.0.0.1
localhost
可以發(fā)現(xiàn)
- X-Forwarded-For記錄的是整條鏈路請求經(jīng)過節(jié)點的ip地址,并且上一條的header不存在X-Forwarded-For頭的情況下,它是取客戶端的ip,可以被篡改
- X-real-ip返回的是上一跳的ip地址,無效
- Host默認從訪問地址中拿,一層層往下傳遞,如果客戶端有取客戶端的,可以被篡改
最佳實踐
一般情況下,代理服務(wù)器都是一層,所以我們直接用proxy_set_header X-real-ip $remote_addr 即可,或者proxy_set_header X-Forwarded-For $remote_addr;也是一個道理
但是在多代理服務(wù)存在的可能性下,首先我們必須使用X-Forwarded-For,其次最外層的nginx服務(wù)器需要配置為proxy_set_header X-Forwarded-For $remote_addr;
為何我能繞過白名單
天下代碼一大抄。
首先我猜測,對方的對外nginx服務(wù)器的配置肯定是
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
其次,針對java代碼中獲取ip的邏輯
取我自己網(wǎng)關(guān)項目ip白名單的邏輯
public static String getRemoteAddr(HttpServletRequest request) {
List<String> headers = Lists.newArrayList("remoteip", "X-Real-IP","X-Forwarded-For");
for (String header : headers) {
String ip = request.getHeader(header);
if (isValid(ip)) {
if (header.equals("X-Forwarded-For")) {
ip = ip.split(",")[0];
}
return ip;
}
}
log.info("未獲取到客戶端IP");
return Constants.UNKNOWN_IP_ADDRESS;
}
存在取多個header的邏輯,勢必可以通過模擬header的方式進行繞過。
參考
https://www.nginx.cn/doc/index.html
https://www.cnblogs.com/lvcisco/p/10309834.html