[TOC]
# 配置
~~~
proxy_set_header Host $host; #只要用戶在瀏覽器訪問的域名綁定了VIP,VIP下面有RS;則使用$host;host是訪問URL中的域名和端口 www.taobao.com:80
proxy_set_header X-Real-IP $remote_addr; #把源IP【$remote_adr,建立HTTP連接header里的信息】賦值給X-Real-IP,這樣在代碼 $X-Real-IP獲取源IP
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;#在 nginx 作為代理服務器,設置的IP列表,會把經過的機器IP,代理機器IP都記錄下來,用【,】隔開,代碼中用 echo $x-forwarded-for | awl -F, '{print $1}' 作為源IP
~~~
# 背景
通過名字就知道,X-Forwarded-For 是一個 HTTP 擴展頭部。HTTP/1.1(RFC 2616)協議并沒有對它的定義,它最開始是由 Squid 這個緩存代理軟件引入,用來表示 HTTP 請求端真實 IP。如今它已經成為事實上的標準,被各大 HTTP 代理、負載均衡等轉發服務廣泛使用,并被寫入[RFC 7239](http://tools.ietf.org/html/rfc7239)(Forwarded HTTP Extension)標準之中。
X-Forwarded-For 請求頭格式非常簡單,就這樣:
~~~
X-Forwarded-For: client, proxy1, proxy2
~~~
可以看到,XFF 的內容由「英文逗號 + 空格」隔開的多個部分組成,最開始的是離服務端最遠的設備 IP,然后是每一級代理設備的 IP。
<br>
如果一個 HTTP 請求到達服務器之前,經過了三個代理 Proxy1、Proxy2、Proxy3,IP 分別為 IP1、IP2、IP3,用戶真實 IP 為 IP0,那么按照 XFF 標準,服務端最終會收到以下信息:
~~~
X-Forwarded-For: IP0, IP1, IP2
~~~
Proxy3 直連服務器,它會給 XFF 追加 IP2,表示它是在幫 Proxy2 轉發請求。列表中并沒有 IP3,IP3 可以在服務端通過 Remote Address 字段獲得。我們知道 HTTP 連接基于 TCP 連接,HTTP 協議中沒有 IP 的概念,Remote Address 來自 TCP 連接,表示與服務端建立 TCP 連接的設備 IP,在這個例子里就是 IP3。
<br>
Remote Address 無法偽造,因為建立 TCP 連接需要三次握手,如果偽造了源 IP,無法建立 TCP 連接,更不會有后面的 HTTP 請求。不同語言獲取 Remote Address 的方式不一樣,例如 php 是`$_SERVER["REMOTE_ADDR"]`,Node.js 是`req.connection.remoteAddress`,但原理都一樣。
<br>
<br>
# 問題
這段代碼會監聽`9009`端口,并在收到 HTTP 請求后,輸出一些信息:
~~~
var http = require('http');
http.createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.write('remoteAddress: ' + req.connection.remoteAddress + '\n');
res.write('x-forwarded-for: ' + req.headers['x-forwarded-for'] + '\n');
res.write('x-real-ip: ' + req.headers['x-real-ip'] + '\n');
res.end();
}).listen(9009, '0.0.0.0');
~~~
這段代碼除了前面介紹過的 Remote Address 和`X-Forwarded-For`,還有一個`X-Real-IP`,這又是一個自定義頭部字段。`X-Real-IP`通常被 HTTP 代理用來表示與它產生 TCP 連接的設備 IP,這個設備可能是其他代理,也可能是真正的請求端。需要注意的是,`X-Real-IP`目前并不屬于任何標準,代理和 Web 應用之間可以約定用任何自定義頭來傳遞這個信息。
<br>
現在可以用域名 + 端口號直接訪問這個 Node.js 服務,再配一個 Nginx 反向代理:
~~~
server{
listen 80;
server_name 127.0.0.1;
root /srv/ip-test;
index index.html;
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
proxy_pass http://127.0.0.1:9009/;
proxy_redirect off;
}
}
~~~
Nginx 監聽`80`端口,所以不帶端口就可以訪問 Nginx 轉發過的服務。
<br>
測試直接訪問 Node 服務:
~~~
curl http://192.168.199.217:9009/
remoteAddress: 192.168.199.180
x-forwarded-for: undefined
x-real-ip: undefined
~~~
由于我的電腦直接連接了 Node.js 服務,Remote Address 就是我的 IP。同時我并未指定額外的自定義頭,所以后兩個字段都是 undefined。
<br>
再來訪問 Nginx 轉發過的服務:
~~~
remoteAddress: 127.0.0.1
x-forwarded-for: 192.168.199.180
x-real-ip: 192.168.199.180
~~~
通過 Nginx 訪問 Node.js 服務,得到的 Remote Address 實際上是 Nginx 的本地 IP。而前面 Nginx 配置中的這兩行起作用了,為請求額外增加了兩個自定義頭:
~~~
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
~~~
<br>
HTTP 請求頭可以隨意構造,我們通過 curl 的`-H`參數構造`X-Forwarded-For`和`X-Real-IP`,再來測試一把。
直接訪問 Node.js 服務:
~~~
curl http://192.168.199.217:9009/ -H 'X-Forwarded-For: 1.1.1.1' -H 'X-Real-IP: 2.2.2.2'
remoteAddress: 192.168.199.217
x-forwarded-for: 1.1.1.1
x-real-ip: 2.2.2.2
~~~
對于 Web 應用來說,`X-Forwarded-For`和`X-Real-IP`就是兩個普通的請求頭,自然就不做任何處理原樣輸出了。這說明,對于直連部署方式,除了從 TCP 連接中得到的 Remote Address 之外,請求頭中攜帶的 IP 信息都不能信。
<br>
訪問 Nginx 轉發過的服務:
~~~
curl http://192.168.199.217/ -H 'X-Forwarded-For: 1.1.1.1' -H 'X-Real-IP: 2.2.2.2'
remoteAddress: 127.0.0.1
x-forwarded-for: 1.1.1.1, 192.168.199.217
x-real-ip: 192.168.199.217
~~~
這一次,Nginx 會在`X-Forwarded-For`后追加我的 IP;并用我的 IP 覆蓋`X-Real-IP`請求頭。這說明,有了 Nginx 的加工,`X-Forwarded-For`最后一節以及`X-Real-IP`整個內容無法構造,可以用于獲取用戶 IP。
<br>
用戶 IP 往往會被使用與跟 Web 安全有關的場景上,例如檢查用戶登錄地區,基于 IP 做訪問頻率控制等等。這種場景下,確保 IP 無法構造更重要。經過前面的測試和分析,對于直接面向用戶部署的 Web 應用,必須使用從 TCP 連接中得到的 Remote Address;對于部署了 Nginx 這樣反向代理的 Web 應用,在正確配置了 Set Header 行為后,可以使用 Nginx 傳過來的`X-Real-IP`或`X-Forwarded-For`最后一節(實際上它們一定等價)。
<br>
那么,Web 應用自身如何判斷請求是直接過來,還是由可控的代理轉發來的呢?在代理轉發時增加額外的請求頭是一個辦法,但是不怎么保險,因為請求頭太容易構造了。如果一定要這么用,這個自定義頭要夠長夠罕見,還要保管好不能泄露出去。
<br>
判斷 Remote Address 是不是本地 IP 也是一種辦法,不過也不完善,因為在 Nginx 所處服務器上訪問,無論直連還是走 Nginx 代理,Remote Address 都是 `127.0.0.1`。這個問題還好通常可以忽略,更麻煩的是,反向代理服務器和實際的 Web 應用不一定部署在同一臺服務器上。所以更合理的做法是收集所有代理服務器 IP 列表,Web 應用拿到 Remote Address 后逐一比對來判斷是以何種方式訪問。
<br>
首先,如果用戶真的是通過代理訪問 Nginx,`X-Forwarded-For`最后一節以及`X-Real-IP`得到的是代理的 IP,安全相關的場景只能用這個,但有些場景如根據 IP 顯示所在地天氣,就需要盡可能獲得用戶真實 IP,這時候`X-Forwarded-For`中第一個 IP 就可以排上用場了。這時候需要注意一個問題,還是拿之前的例子做測試:
~~~
curl http://192.168.199.217/ -H 'X-Forwarded-For: unknown, <>"1.1.1.1'
remoteAddress: 127.0.0.1
x-forwarded-for: unknown, <>"1.1.1.1, 192.168.199.217
x-real-ip: 192.168.199.217
~~~
<br>
`X-Forwarded-For`最后一節是 Nginx 追加上去的,但之前部分都來自于 Nginx 收到的請求頭,這部分用戶輸入內容完全不可信。使用時需要格外小心,符合 IP 格式才能使用,不然容易引發 SQL 注入或 XSS 等安全漏洞。
<br>
<br>
# 結論
1. 直接對外提供服務的 Web 應用,在進行與安全有關的操作時,只能通過 Remote Address 獲取 IP,不能相信任何請求頭;
2. 使用 Nginx 等 Web Server 進行反向代理的 Web 應用,在配置正確的前提下,要用`X-Forwarded-For`最后一節 或`X-Real-IP`來獲取 IP(因為 Remote Address 得到的是 Nginx 所在服務器的內網 IP);同時還應該禁止 Web 應用直接對外提供服務;
3. 在與安全無關的場景,例如通過 IP 顯示所在地天氣,可以從`X-Forwarded-For`靠前的位置獲取 IP,但是需要校驗 IP 格式合法性;
<br>
PS:網上有些文章建議這樣配置 Nginx,其實并不合理:
~~~
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
~~~
這樣配置之后,安全性確實提高了,但是也導致請求到達 Nginx 之前的所有代理信息都被抹掉,無法為真正使用代理的用戶提供更好的服務。
# 參考資料
[HTTP 請求頭中的 X-Forwarded-For](https://imququ.com/post/x-forwarded-for-header-in-http.html)
- 第一部分 HTML
- meta
- meta標簽
- HTML5
- 2.1 語義
- 2.2 通信
- 2.3 離線&存儲
- 2.4 多媒體
- 2.5 3D,圖像&效果
- 2.6 性能&集成
- 2.7 設備訪問
- SEO
- Canvas
- 壓縮圖片
- 制作圓角矩形
- 全局屬性
- 第二部分 CSS
- CSS原理
- 層疊上下文(stacking context)
- 外邊距合并
- 塊狀格式化上下文(BFC)
- 盒模型
- important
- 樣式繼承
- 層疊
- 屬性值處理流程
- 分辨率
- 視口
- CSS API
- grid(未完成)
- flex
- 選擇器
- 3D
- Matrix
- AT規則
- line-height 和 vertical-align
- CSS技術
- 居中
- 響應式布局
- 兼容性
- 移動端適配方案
- CSS應用
- CSS Modules(未完成)
- 分層
- 面向對象CSS(未完成)
- 布局
- 三列布局
- 單列等寬,其他多列自適應均勻
- 多列等高
- 圣杯布局
- 雙飛翼布局
- 瀑布流
- 1px問題
- 適配iPhoneX
- 橫屏適配
- 圖片模糊問題
- stylelint
- 第三部分 JavaScript
- JavaScript原理
- 內存空間
- 作用域
- 執行上下文棧
- 變量對象
- 作用域鏈
- this
- 類型轉換
- 閉包(未完成)
- 原型、面向對象
- class和extend
- 繼承
- new
- DOM
- Event Loop
- 垃圾回收機制
- 內存泄漏
- 數值存儲
- 連等賦值
- 基本類型
- 堆棧溢出
- JavaScriptAPI
- document.referrer
- Promise(未完成)
- Object.create
- 遍歷對象屬性
- 寬度、高度
- performance
- 位運算
- tostring( ) 與 valueOf( )方法
- JavaScript技術
- 錯誤
- 異常處理
- 存儲
- Cookie與Session
- ES6(未完成)
- Babel轉碼
- let和const命令
- 變量的解構賦值
- 字符串的擴展
- 正則的擴展
- 數值的擴展
- 數組的擴展
- 函數的擴展
- 對象的擴展
- Symbol
- Set 和 Map 數據結構
- proxy
- Reflect
- module
- AJAX
- ES5
- 嚴格模式
- JSON
- 數組方法
- 對象方法
- 函數方法
- 服務端推送(未完成)
- JavaScript應用
- 復雜判斷
- 3D 全景圖
- 重載
- 上傳(未完成)
- 上傳方式
- 文件格式
- 渲染大量數據
- 圖片裁剪
- 斐波那契數列
- 編碼
- 數組去重
- 淺拷貝、深拷貝
- instanceof
- 模擬 new
- 防抖
- 節流
- 數組扁平化
- sleep函數
- 模擬bind
- 柯里化
- 零碎知識點
- 第四部分 進階
- 計算機原理
- 數據結構(未完成)
- 算法(未完成)
- 排序算法
- 冒泡排序
- 選擇排序
- 插入排序
- 快速排序
- 搜索算法
- 動態規劃
- 二叉樹
- 瀏覽器
- 瀏覽器結構
- 瀏覽器工作原理
- HTML解析
- CSS解析
- 渲染樹構建
- 布局(Layout)
- 渲染
- 瀏覽器輸入 URL 后發生了什么
- 跨域
- 緩存機制
- reflow(回流)和repaint(重繪)
- 渲染層合并
- 編譯(未完成)
- Babel
- 設計模式(未完成)
- 函數式編程(未完成)
- 正則表達式(未完成)
- 性能
- 性能分析
- 性能指標
- 首屏加載
- 優化
- 瀏覽器層面
- HTTP層面
- 代碼層面
- 構建層面
- 移動端首屏優化
- 服務器層面
- bigpipe
- 構建工具
- Gulp
- webpack
- Webpack概念
- Webpack工具
- Webpack優化
- Webpack原理
- 實現loader
- 實現plugin
- tapable
- Webpack打包后代碼
- rollup.js
- parcel
- 模塊化
- ESM
- 安全
- XSS
- CSRF
- 點擊劫持
- 中間人攻擊
- 密碼存儲
- 測試(未完成)
- 單元測試
- E2E測試
- 框架測試
- 樣式回歸測試
- 異步測試
- 自動化測試
- PWA
- PWA官網
- web app manifest
- service worker
- app install banners
- 調試PWA
- PWA教程
- 框架
- MVVM原理
- Vue
- Vue 餓了么整理
- 樣式
- 技巧
- Vue音樂播放器
- Vue源碼
- Virtual Dom
- computed原理
- 數組綁定原理
- 雙向綁定
- nextTick
- keep-alive
- 導航守衛
- 組件通信
- React
- Diff 算法
- Fiber 原理
- batchUpdate
- React 生命周期
- Redux
- 動畫(未完成)
- 異常監控、收集(未完成)
- 數據采集
- Sentry
- 貝塞爾曲線
- 視頻
- 服務端渲染
- 服務端渲染的利與弊
- Vue SSR
- React SSR
- 客戶端
- 離線包
- 第五部分 網絡
- 五層協議
- TCP
- UDP
- HTTP
- 方法
- 首部
- 狀態碼
- 持久連接
- TLS
- content-type
- Redirect
- CSP
- 請求流程
- HTTP/2 及 HTTP/3
- CDN
- DNS
- HTTPDNS
- 第六部分 服務端
- Linux
- Linux命令
- 權限
- XAMPP
- Node.js
- 安裝
- Node模塊化
- 設置環境變量
- Node的event loop
- 進程
- 全局對象
- 異步IO與事件驅動
- 文件系統
- Node錯誤處理
- koa
- koa-compose
- koa-router
- Nginx
- Nginx配置文件
- 代理服務
- 負載均衡
- 獲取用戶IP
- 解決跨域
- 適配PC與移動環境
- 簡單的訪問限制
- 頁面內容修改
- 圖片處理
- 合并請求
- PM2
- MongoDB
- MySQL
- 常用MySql命令
- 自動化(未完成)
- docker
- 創建CLI
- 持續集成
- 持續交付
- 持續部署
- Jenkins
- 部署與發布
- 遠程登錄服務器
- 增強服務器安全等級
- 搭建 Nodejs 生產環境
- 配置 Nginx 實現反向代理
- 管理域名解析
- 配置 PM2 一鍵部署
- 發布上線
- 部署HTTPS
- Node 應用
- 爬蟲(未完成)
- 例子
- 反爬蟲
- 中間件
- body-parser
- connect-redis
- cookie-parser
- cors
- csurf
- express-session
- helmet
- ioredis
- log4js(未完成)
- uuid
- errorhandler
- nodeclub源碼
- app.js
- config.js
- 消息隊列
- RPC
- 性能優化
- 第七部分 總結
- Web服務器
- 目錄結構
- 依賴
- 功能
- 代碼片段
- 整理
- 知識清單、博客
- 項目、組件、庫
- Node代碼
- 面試必考
- 91算法
- 第八部分 工作代碼總結
- 樣式代碼
- 框架代碼
- 組件代碼
- 功能代碼
- 通用代碼