圖解跨域請求、反向代理原理,對前端更友好的反向代理服務器 - Caddy

caddy

寫在開頭

本文采用圖文解析、結合實戰的方式進行網絡原理解析,幫助大家去掌握一些網絡知識,並瞭解 Caddy 的基本使用(見下圖)。

caddy

本人計劃在近幾年將持續輸出深度好文,如果對這類文章感興趣的話,還請您點個 關注 支持一下吧!

引言

大家好呀~

本篇文章主要是安利一個對前端更友好的 web 服務器 Caddy,我們會介紹 Caddy 的安裝使用,並通過圖文來解析其原理。

Caddy 是唯一一個在默認情況下自動使用 HTTPSWeb 服務器,可以用來完成跨域請求、反向代理、靜態文件服務器、部署 History SPA 應用、負載均衡等等功能,在可讀性、可維護性和易用性方面都做的很好,對前端更友好!

如果你還是不太理解 Caddy 到底是用來做什麼的,那你可以把它簡單理解爲對前端更友好的 nginx

反向代理

本文討論的 代理 僅限於 HTTP 代理,不涉及其他協議。

Caddy 是一個簡單好用的 Web 服務器,反向代理 是它的一個核心功能。所以,在介紹 Caddy 之前,我們先介紹一下 反向代理 是什麼,反向代理 可以幫我們做什麼事情。

我們先來了解一下正向代理,正向代理就是在客戶端與服務器之間實現一個代理服務器,客戶端的所有請求先經過代理服務器,由代理服務器再去請求真實服務器,請求成功後再由代理服務器將真實服務器的響應結果發回至客戶端。

正向代理的經典案例就是公司內部的 VPN 代理,企業員工在 遠程開發 時需要先連接 VPN,再由 VPN 連接至公司服務器。這樣做可以防止一些陌生連接,拒絕除 VPN 外的所有外網連接,只有連接 VPN 才能正常訪問公司服務器。

我們來畫一張圖幫助大家理解什麼是 正向代理(見下圖)

caddy

而反向代理正好相反,反向代理一般是在服務器端,客戶端發起的網絡請求首先被反向代理服務器收到,再由反向代理服務器決定轉發到某個具體的服務。換而言之,反向代理服務器將決定客戶端最終訪問到的目標服務器,常見的反向代理案例有負載均衡、CDN 加速。

我們在實際開發中,可以使用反向代理來 解決前端跨域問題部署前端服務 等等,我們本篇教程也是主要介紹這兩個功能的使用。

我們來畫一張圖幫助大家理解什麼是 反向代理(見下圖)

caddy

最後使用一句話概括就是:正向代理隱藏真實客戶端,反向代理隱藏真實服務端。

Caddy 的優勢

我們在實際開發中,可以使用 Caddy 來搭建反向代理服務器,從而完成跨域請求、靜態文件服務器、部署 History SPA 應用、負載均衡等等功能,使用 Caddy 來做這些工作的好處是我們通過幾行配置文件就可以完成這些工作,非常的簡單易用。

在日常開發中我們通常使用 webpack 解決開發環境的跨域和請求轉發問題,webpackproxy 選項可以解決大部分跨域和請求轉發問題,但是對 history 路由的支持性較差,並且組內開發的成員之間的配置可能會導致衝突,造成額外的維護成本。

使用 nginx 可以解決這些問題,但是 nginx 比較複雜,對前端人員並不是特別友好。在學習 nginx 的過程中我們可能會漸行漸遠,忘記了我們的初衷只是爲了解決跨域和請求轉發問題。

Caddy 使用 Go 語言編寫,跨平臺性強,配置文件具有高可讀性,對前端更友好。在可讀性、可維護性和易用性方面的優勢成爲了選擇 Caddy 的理由。

安裝 Caddy

介紹了那麼多,我們差不多可以進入到實戰部分了,先從 安裝 開始吧!

Caddy 目前有 1.02.0 兩個大版本,本文是針對 2.0 版本的教程,如果需要使用 1.0 版本的話建議查看 Caddy 1.0 官方文檔

如果想要先了解 Caddy 好不好用,可以先跳過 安裝 這一節。

Mac 平臺

Mac 非常適合開發者,歡迎廣大開發者加入 Mac 大家庭。

首先我們需要下載 Caddy,你也可以去 官方地址 下載最新版本。

由於 Caddygo 編寫,go 編譯後的文件可以直接執行,所以我們下載完成後我們直接解壓到自己的目錄,比如 ~/bin/ 目錄。然後我們加上一個映射就可以使用啦,我們使用 vi ~/.bash_profile 命令編輯文件,添加下面這行代碼:

export PATH=~/bin

添加了全局映射後,我們使用下面這行命令使我們的改動生效

source ~/.bash_profile

接下來我們輸入 caddy version 來驗證我們的安裝是否生效,如果可以正確輸出 caddy 的版本說明已經安裝成功啦~(見下圖)

caddy

Windows 平臺

首先我們需要下載 Caddy,你也可以去 官方地址 下載最新版本。

下載完成後,解壓到你的常用目錄(路徑最好別帶中文),然後我們複製 Caddy 所在目錄的路徑(見下圖)

caddy

然後,我們使用 Win + E 喚起文件管理器,然後右鍵點擊我的電腦,點擊 屬性(見下圖)

caddy

然後,我們選擇 高級,點擊 環境變量(見下圖)

caddy

然後我們在彈出的窗口中,選中 Path 這一欄(見下圖)

caddy

然後,我們在彈出的窗口中點擊新建,將我們複製的 Caddy 目錄路徑粘貼進去(見下圖)

caddy

最後,我們點擊 確定,保存設置。我們在命令行中輸入 caddy,安裝成功啦!(見下圖)

caddy

Linux 平臺

首先我們使用 curl 命令下載 Caddy 的安裝包,如下

curl -OL https://github.com/caddyserver/caddy/releases/download/v2.0.0/caddy_2.0.0_linux_amd64.tar.gz

大家根據自己的需要下載對應版本的安裝包。

我們使用 tar zxvf caddy_2.0.0_linux_amd64.tar.gz 解壓文件,解壓後的 caddy 文件是可執行文件,我們再配置相應的映射,將命令映射到全局即可(見下圖)

caddy

Caddy 使用教程

Caddy 安裝完成後,我們來學習如何使用 Caddy 吧。

使用 Caddy 解決跨域問題

我們先使用 Caddy 來解決一個前端最常見的跨域問題,我們以一個簡單 Demo 爲例。在該案例中,我們使用 fetch 發起一個網絡請求,請求一個網絡資源(見下圖)

caddy

從上圖我們可以看出,我們在使用 fetch 發起了一個網絡請求後,將請求的結果打印出來。現在,我們打開瀏覽器,查看請求結果(見下圖)。

caddy

從上圖可以看到,我們的請求失敗了,請求失敗的原因是因爲瀏覽器的 同源策略 導致的跨域問題。

同源策略是一個重要的安全策略,它用於限制一個 origin 的文檔如何能與另一個源的資源進行交互,在使用 XMLHttpRequestfetch 時則會受到同源策略的約束。

我們想要解決這個問題的話,需要服務端返回指定的響應頭(Access-Control-Allow-*),這些響應頭可以通過瀏覽器的 同源策略 檢測。

如果需要在服務端配置響應頭的話,則需要後端人員配合,由前端推動後端的工作在效率上是不高的,還可能有些後端人員難以配合(可能是異地、第三方接口、不知道跨域是啥…)。

我們現在來使用 Caddy 解決這個問題,我們需要通過簡單的兩步來解決這個跨域問題:

  • 配置 CaddyfileCaddy 的配置文件),啓動 Caddy
  • 配置 hosts 文件;

配置 Caddyfile

CaddyfileCaddy 的配置文件,我們在 Demo 的根目錄 下新建文件 Caddyfile,添加下面幾行代碼

http://proxy.dev-api-mall.jt-gmall.com {
  reverse_proxy http://dev-api-mall.jt-gmall.com {
    header_up Host dev-api-mall.jt-gmall.com
    header_down Access-Control-Allow-Origin *
    header_down Access-Control-Allow-Methods *
    header_down Access-Control-Allow-Headers *
  }
}

我們對這幾行配置進行簡單的解析(見下圖):

caddy

我們來分析一下上面幾行核心配置代碼的含義吧,解析如下:

  • 第 1 行:攔截對 http://proxy.dev-api-mall.jt-gmall.com 這條 url 的訪問請求,進行內部邏輯處理;
  • 第 2 行:將 攔截的請求 轉發(反向代理)到 http://dev-api-mall.jt-gmall.com(我們的目標地址);
  • 第 3 行:在轉發請求時,添加首部字段 Host: dev-api-mall.jt-mall.com,這一步的目的是爲了讓目標服務器能夠識別請求源;
  • 第 4~6 行:在響應結果時,添加 Access-Control-Allow-*: * 等多個首部字段信息,這樣可以通過瀏覽器的 同源策略 檢測;

我們通過嵌套結構的幾行代碼就可以將 Caddyfile 配置完成啦!

配置 hosts 文件

在配置好 Caddyfile 後,我們將我們請求的地址修改爲 http://proxy.dev-api-mall.jt-gmall.com,代碼實現如下:

caddy

我們在命令行工具使用 caddy run --watch 命令運行 caddy(運行 caddy 時請保證 80 端口是空閒的),caddy 運行成功後將會輸出下面的結果(見下圖)

caddy

然後我們打開瀏覽器,打開 http://localhost:3000Demo 的運行地址),查看控制檯輸出的請求結果(見下圖)

caddy

從上圖可以看出,我們的請求失敗了,這是因爲我們在訪問代理地址(http://proxy.dev-api-mall.jt-gmall.com)時,由於這個域名沒有註冊,將會導致 DNS 解析失敗,最終導致請求失敗。

此時我們只需要配置 hosts 文件,將這條 hostnameIP 地址指向本機即可,在 hosts 文件中添加下面這條記錄:

127.0.0.1 proxy.dev-api-mall.jt-gmall.com

hosts 文件是一個操作系統文件,以表的形式存儲了 主機名 和 IP 地址,用於查找主機名稱。

這條記錄代表的是在訪問 proxy.dev-api-mall.jt-gmall.com 時,將 IP 地址解析爲 127.0.0.1(本機)。

不同系統的 hosts 文件配置方法在本文的 最後一節

配置好了 hosts 文件後,我們刷新瀏覽器,可以看到我們的請求結果被打印在控制檯了!(見下圖)

caddy

caddy

我們從上圖可以看出,我們通過 Caddy 的反向代理功能解決了跨域問題,並且更好的模擬了真實環境的網絡請求。

原理解析

我們來簡單梳理一遍流程,分析一下 Caddy 做了什麼,幫助我們解決了跨域問題。

我們從客戶-服務端的視角來進行解析,我們的瀏覽器就是客戶端,Caddy 同時作爲服務端與客戶端,目標服務器屬於服務端。

瀏覽器 - 客戶端

首先,我們在客戶端(瀏覽器)發起了一個請求,請求的地址是 http://proxy.dev-api-mall.jt-gmall.com/vegetable/list?page=1&pageSize=20,瀏覽器首先解析出 hostname 的值爲 proxy.dev-api-mall.jt-gmall.com

在解析出了 hostname 後,瀏覽器讀取主機的 hosts 文件配置,查詢是否匹配,此時將命中我們在 hosts 文件中設置的 127.0.0.1 proxy.dev-api-mall.jt-gmall.com 規則,將域名解析爲 IP 地址 - 127.0.0.1,也就是本機地址。

將域名解析完成後,瀏覽器解析到請求的端口爲空,請求協議爲 http,然後使用 http 的默認端口 80IP 地址創建了網絡套接字 127.0.0.1:80(如下圖)。

caddy

創建好了網絡套接字後,瀏覽器將與目標地址 127.0.0.1:80(我們運行的 Caddy 服務) 創建 TCP 連接,然後按照 http 協議標準封裝好請求信息,以數據分組(segment)的形式發送給服務端。

Caddy - 服務端 + 客戶端

我們的 Caddy 服務(服務端)運行在本地端口 80 上,對應的地址就是 127.0.0.1:80。所以, Caddy 服務收到了這個 TCP 連接請求,CaddyTCP 的數據分組(segment)解析後,解析到了 http 請求(見下圖)。

caddy

從上面可以看出,我們的請求源是 127.0.0.1:57721IP 地址爲我們本機的 IP,端口爲 瀏覽器 發起請求時使用的的隨機端口 - 瀏覽器 客戶端),目的地址是 127.0.0.1:80IP 地址爲我們本機的 IP,端口爲 Caddy 的運行端口 - Caddy 服務端)。我們的 Host 請求頭爲 proxy.dev-api...(代理地址),請求來源(發起方)是 http://localhost:3000(我們的本地服務)。

Caddy 收到了這個 http 請求後,解析到協議爲 HTTPHostproxy.dev-api-mall.jt-gmall.com,組合起來後匹配到了下面這條配置規則。(見下圖)

caddy

從上圖可以看出,Caddy 在匹配到內部規則後,開始處理這條請求。根據配置規則,Caddy 將這條請求轉發到 http://dev-api-mall.jt-gmall.com。此時,Caddy 先進行 DNS 查詢和端口查詢,組合了 IP 地址與端口後再與該地址建立 TCP 連接,將客戶端的請求原封不動的轉發到指定地址(見下圖)。

caddy

從上圖可以看出,這條請求由作爲客戶端的 Caddy 發出。我們的請求源是 10.8.71.38:52170IP 地址爲本機的 IP,端口是 Caddy 使用的隨機端口 - Caddy 客戶端),目的地址是 39.98.164.255:80IP 地址爲目標服務器 IP,端口爲 HTTP 協議默認端口號 - 目標服務器)。我們的 Host 請求頭爲 dev-api...(我們在 Caddyfile 中指定的 Host 首部),其餘的首部字段及請求信息都由 Caddy 直接轉發到目標服務器。

遠程服務器接收到請求後,處理請求後返回響應結果。(見下圖)

caddy

我們從上圖可以看出,這條響應結果的源地址是 39.98.164.255:80IP 地址爲請求的服務器 IP,端口爲請求的服務器端口 80 - 遠程服務器),目的地址是 10.8.71.38:52170IP 地址爲我們本機的 IP,端口是 Caddy 使用的隨機端口 - Caddy 客戶端)。服務器將響應結果發送到 Caddy 客戶端,我們的 Caddy 客戶端接收到響應結果後,由 Caddy 服務器進行處理。

我們的 Caddy 服務器在處理響應結果時,根據 Caddyfile 配置在響應結果中添加 Access-Control-Allow-... 三條首部信息,最後將這條響應結果發送給瀏覽器客戶端。(見下圖)

caddy

我們從上圖可以看出,這條響應結果的源地址是 127.0.0.1:80IP 地址爲我們本機的 IP,端口爲 Caddy 的運行端口 - Caddy 服務端),目的地址是 127.0.0.1:57721IP 地址爲我們本機的 IP,端口爲 瀏覽器 發起請求時使用的的隨機端口 - 瀏覽器 客戶端)。

我們可以在響應結果中看到,我們在 Caddyfile 設置的首部信息 Access-Control-Allow-... 被添加在了響應結果中,響應結果中有這三個首部字段就可以通過瀏覽器的 同源策略 限制。我們在響應首部中可以看到兩個 Server 首部,一個是我們本地的 Caddy 服務自動添加,另一個可能是遠程服務器上的 Caddy 服務器所添加的。最後,數據被正常返回,我們在瀏覽器的控制檯也可以看到請求成功啦!(見下圖)

caddy

從上圖看出,我們通過 Caddy 的反向代理功能,解決了跨域問題!

我們最後來通過一張圖幫助大家理解上面的流程吧!(見下圖)

caddy

圖有點大,建議點擊查看原圖,這樣可以看到更多細節。

使用 Caddy 搭建反向代理服務器

在這一節我們將使用 Caddy 搭建反向代理服務器,Caddy 可以輕鬆地完成這項工作。

使用 Caddy 搭建反向代理服務器的思路和解決跨域問題的思路是差不多的,都是使用 reverse_proxy 屬性。

我們想要實現的效果是,在訪問 http://www.caddy-test.com 域名時,將其反向代理到我們的本地服務 http://localhost:3000 上。

我們先在 http://localhost:3000 服務加上一些樣式,修改後效果如下圖

caddy

我們從上圖可以看出,我們的服務允許在本地的 3000 端口上,我們使用 /list 路徑訪問了一個列表頁。

此時我們打開 http://www.caddy-test.com/list(見下圖)

caddy

從上圖可以看出,由於這個域名尚未註冊,所以導致我們的 DNS 查詢失敗啦!

配置 hosts 文件

我們在本地開發時,只需要配置 hosts 文件,將這條 hostnameIP 地址指向本機即可,我們在 hosts 文件中添加這條記錄:

127.0.0.1 www.caddy-test.com

不同系統的 hosts 文件配置方法在本文的 最後一節

這條記錄表示,當匹配到 www.caddy-test.com 域名時,返回 IP 地址 127.0.0.1(本機 IP)。我們在配置好了 hosts 文件後,我們再次打開 http://www.caddy-test.com/list(如下圖)

caddy

從上圖可以看出,我們此時的頁面是一片空白。這是因爲在解析了域名和端口後,瀏覽器最終訪問到了 127.0.0.1:80 上的 Caddy 服務(我們在第一節的時候運行了 Caddy),而 Caddy 服務對這條域名的訪問並沒有做配置,無法做出正確響應。接下來,我們將會進行 Caddyfile 的配置。

擴展閱讀:

如果此時訪問 http://www.caddy-test.com:3000/list(指定端口)會發現頁面可訪問,也可能返回了 Invalid Host header 字符串(這是因爲被 webpack 自帶的一些安全策略攔截了正確的響應結果,但是我們已經成功訪問到了服務)。

這是因爲在指定了端口後,我們訪問的地址就被解析成了 127.0.0.1:3000,直接訪問指定端口的服務。

這樣的方式既不安全(需要暴露可訪問端口),也不優雅(帶個端口號太難記啦)。

配置 Caddyfile

我們現在需要配置我們的 Caddyfile,配置如下:

http://www.caddy-test.com {
  reverse_proxy localhost:3000 {
    header_up Host localhost
  }
}

由於我們啓動 Caddy 的命令加上了 --watch,所以此時我們的 Caddy 將會檢測到 Caddyfile 的變化後自動重啓。

我們現在再打開 http://www.caddy-test.com/list 就可以看到,我們的頁面可以正常訪問啦(見下圖)!

caddy

從上圖可以看出,在我們訪問我們配置的 測試域名 時,展示了我們在本地 3000 端口運行的服務所返回的頁面,我們的反向代理配置成功啦!

擴展閱讀:

如果我們的域名不是配置在 hosts 文件中,而是註冊在真實的 域名註冊機構,那我們的 Caddy 服務就是 “真正的” 反向代理服務器啦!

接下來我們對 Caddyfile 配置文件進行逐行解析(見下圖)。

caddy

我們來逐行解析一下:

  • 第 10 行:攔截對 http://www.caddy-test.com 這條 url 的訪問請求,進行內部邏輯處理;
  • 第 11 行:將 攔截的請求 轉發(反向代理)到 localhost:3000(我們的本地服務);
  • 第 12 行:轉發請求時,帶上首部字段 Host: localhost,這一步的目的是爲了通過 webpack 自帶的 Host 首部安全檢查;

原理解析

其實反向代理的原理和解決跨域問題的原理是一樣的,只是把遠程服務器地址換成了內網地址,所以我們直接用一張長圖來進行解釋吧(見下圖)。

caddy

圖有點大,建議點擊查看原圖,這樣可以看到更多細節。

使用 Caddy 部署 SPA - History 路由模式項目

在介紹完了反向代理後,我們來介紹一下如何使用 Caddy 部署 history 路由模式的單頁應用吧。

目前前端的兩種路由模式主要分爲 hashhistory 模式兩種。hash 模式是指通過地址欄 URL 中的 # 符號區分路由,而 history 模式就是通過路徑 /xxx 來區分路由。

在單頁(SPA)應用中使用 history 路由模式需要服務器配置支持,我們在開發過程中可以通過 webpack 來配置 history 路由模式。在我們將應用打包後,我們可以通過 Caddy 配置,使我們的 Caddy 服務器支持 history 路由模式的 SPA 應用。

首先,我們在 SPA 應用中配置 history 路由模式,然後使用打包命令 npm run build (不同技術棧的打包大同小異)將我們的應用打包,最後項目的目錄層級看起來像是這樣的(見下圖)

caddy

我們構建好的代碼在 dist 目錄下,Caddyfiledist 同級,接下來我們配置一下 Caddyfile,配置如下:

http://localhost:3000 {
  file_server
  root * ./dist
  try_files {path} /index.html
}

配置完成後,我們打開瀏覽器,輸入 http://localhost:3000/list,會發現我們的頁面成功渲染啦(見下圖)!

caddy

我們簡單剖析一下這幾行配置(見下圖)

caddy

我們來進行逐行解析一下:

  • 第 16 行:攔截對 http://localhost:3000 這條 url 的訪問請求,進行內部邏輯處理(在測試或生產環境時,這裏應該配置一個真實域名);
  • 第 17 行:啓用靜態文件服務器;
  • 第 18 行:靜態文件服務器訪問的根目錄在 ./dist - 在 dist 文件夾外的內容無法訪問;
  • 第 19 行:這行代碼是處理 history 路由模式的關鍵 - 如果 URL 匹配不到任何靜態資源,將會返回 index.html(解決 404 問題);

從上面可以看出,使用 Caddy 部署 history 路由模式的單頁應用還是比較簡單的。這裏還涉及了一些服務器運維的知識,先不作展開啦,有興趣的童鞋可以自己去了解一下。

使用 Caddy 進行負載均衡

使用 Caddy 進行負載均衡也是建立在反向代理的基礎之上,我們將 Demo 分別在三個端口運行(模擬多個服務器運行的多個實例),最後運行效果如下:

caddy

從上圖可以看出,我們啓動了三個同樣的 Demo 服務,使用網站的 title 來進行區分。

使用 Caddy 做負載均衡,只需要將多個服務掛在同一個 reverse_proxy 屬性下即可(見下圖)

caddy

在配置完成後,我們打開瀏覽器,輸入 http://www.caddy-test.com,然後多刷新幾次,看看效果(見下圖):

caddy

caddy

caddy

從上面三張圖可以看出,在不斷刷新的過程中,Caddy 自動將我們的請求隨機分流分配到某個服務上,從而達到負載均衡的效果。

注意,實際生產環境的負載均衡要比文中描述的複雜的多,有需要的童鞋最好自己去了解一下。負載均衡並不是本教程的重點,就不作展開討論了。

不同平臺的 hosts 文件配置

如果你知道 hosts 文件如何配置,那麼你可以跳過本節內容~

Mac

Mac 修改 hosts 文件很簡單,使用 vi 命令即可,如下:

# 可能需要 root 權限
sudo vi /etc/hosts

在命令行輸入命令行,鍵盤 i 可進入編輯模式,編輯完成後使用 Esc 鍵退出編輯模式。

最後,同時按下 shift + : 鍵,輸入 wq! 即可保存更改。

Linux

Mac 的方法類似,不做複述。

Windows

首先使用 Win + R 鍵喚起 運行 輸入框(如下圖)

caddy

然後我們輸入 C:\Windows\System32\drivers\etc\hosts 後按下 確定 按鈕(見下圖)

caddy

點擊 確定 按鈕後,選擇使用 記事本 打開,然後進行修改、保存就可以啦(可能需要管理員權限)。

小結

最後,我們使用 Caddy 完成了跨域請求、反向代理、靜態文件服務器、部署 History SPA 應用、負載均衡多種功能。

從上面的案例中我們可以看出,Caddy 在可讀性、可維護性和易用性方面確實做的不錯,通過簡單的學習就可以上手使用。

如果只是用於本地開發、中小型應用,那麼強烈推薦你使用 Caddy

如果想要用於複雜的大型項目,那麼建議你可以先參考下面這些資料,再決定是否使用:

最後一件事

如果您已經看到這裏了,希望您還是點個贊再走吧~

您的點贊是對作者的最大鼓勵,也可以讓更多人看到本篇文章!

如果覺得本文對您有幫助,請幫忙在 github 上點亮 star 鼓勵一下吧!

personal

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章