【Go Web 開發】跨域請求
在接下來的文章中,我們將討論一個全新的主題,並更新我們的應用程序,使其支持來自 JavaScript 的跨域請求 (CORS: Cross-origin resource sharing))。
你將學習到:
-
什麼是跨域請求,爲什麼瀏覽器默認阻止跨域請求。
-
普通請求和跨域請求之間的區別。
-
如何使用 Access-Control 請求頭來允許或拒絕特定的跨域請求。
-
考慮到安全因素,你需要注意什麼時候配置你的應用程序可以跨域請求。
CORS 概述
在深入代碼前,或者開始具體討論跨域請求之前,讓我們先來定義一下術語 origin 的含義。
本質上,如果兩個 url 具有相同的 schema、主機和端口 (如果指定了),則稱它們具備相同的域。爲了說明這一點,讓我們比較以下 url:
理解什麼是 origin 是很重要的,因爲所有的 web 瀏覽器都實現了一種被稱爲同域策略的安全機制。在瀏覽器實現這個策略的方式上有一些非常小的區別,但廣義上來說:
- 一個域的網頁可以在其 HTML 中嵌入來自另一個域的特定類型的資源——包括圖像、CSS 和 JavaScript 文件。例如,在你的網頁中這樣做是可以的:
<img src="http://anotherorigin.com/example.png" alt="example image">
-
一個域上的網頁可以向另一個域發送數據。例如,網頁中的 HTML 表單可以向不同的域提交數據。
-
但是一個域上的網頁不允許接收來自不同域的數據。
這裏的關鍵是最後一個要點:同域策略不允許 (潛在惡意) 其他域網站從本域讀取數據。
必須強調的是,同域策略不會阻止數據的跨域發送,儘管這很危險。事實上,這就是爲什麼 CSRF 攻擊可能發生,以及爲什麼我們需要採取額外的步驟來防止它們——比如使用 samsite cookie 和 CSRF 令牌。
作爲一名開發人員,您最可能遇到同域策略是在瀏覽器中運行 JavaScript 的跨域請求時。例如:假設你有一個 http://foo.com 網頁包含一些前端 JavaScript 代碼。如果這個 JavaScript 試圖發起 https://bar.com/data.json 請求 (不同的域),然後請求被髮送到 bar.com 服務端處理,但用戶的瀏覽器將阻止接收響應,以至於 https://foo.com 這邊的 JavaScript 代碼接收不到返回數據。
一般來說,同域策略是一種非常有用的安全保護措施。雖然它在一般情況下是好的,但在某些情況下,你可能想要將這種約束放開。例如,你有一個 API 服務域名爲 api.example.com 和一個 JavaScript 前端應用運行在 www.example.com,那麼你可能想要允許 www.example.com 前端應用跨域訪問 API 服務。
或者你有一個完全開放的公共 API 服務,你想允許來自任何地方的跨域請求,這樣其他開發人員就可以很容易地與他們自己的網站集成。
幸運的是,大多數現代 web 瀏覽器允許你通過設置 API 響應的訪問控制頭來允許或禁止特定的跨域請求。在接下來的幾節中,我們將詳細解釋如何做到這一點以及這些響應頭是如何工作的。
演示同源 (Same-Origin) 策略
爲了演示同源策略是如何工作的,以及如何在對 API 的請求中放開同源策略,我們需要模擬一個來自不同源的對 API 的請求。
可以快速地創建一個簡單的 Go 應用程序來模擬這個跨域請求。從本質上說,我們希望第二個應用程序包含一些 JavaScript 的網頁,然後向我們的 GET / v1 / healthcheck 接口發起請求。
如果你跟隨本系列文章的操作,創建文件 cmd/example/cors/simple/main.go 來寫我們的第二個應用程序。
$ mkdir -p cmd/example/cors/simple
$ touch cmd/example/cors/simple/main.go
添加以下代碼:
File:cmd/example/cors/simple/main.go
package main
import (
"flag"
"log"
"net/http"
)
//定義HTML網頁字符串常量。
const html = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
<h1>Simple CORS</h1>
<div id="output"></div>
<script>
document.addEventListener('DOMContentLoaded', function() {
fetch("http://localhost:4000/v1/healthcheck").then(
function (response) {
response.text().then(function (text) {
document.getElementById("output").innerHTML = text;
});
},
function(err) {
document.getElementById("output").innerHTML = err;
}
);
});
</script>
</body>
</html>`
func main() {
//允許服務地址在運行時可根據命令行參數配置
addr := flag.String("addr", ":9000", "Server address")
flag.Parse()
log.Printf("starting server on %s", *addr)
//啓動HTTP服務,並監聽給定的地址。並對上面的HTML中所有請求進行應答。
err := http.ListenAndServe(*addr, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(html))
}))
log.Fatal(err)
}
這裏 Go 代碼我們應該很熟悉了,下面一起來看看 標籤中的 JavaScript 代碼:
<script>
document.addEventListener('DOMContentLoaded', function() {
fetch("http://localhost:4000/v1/healthcheck").then(
function (response) {
response.text().then(function (text) {
document.getElementById("output").innerHTML = text;
});
},
function(err) {
document.getElementById("output").innerHTML = err;
}
);
});
</script>
在這個代碼中:
-
我們使用 fetch() 函數向 API 服務的健康檢查接口發起請求。默認發送的是 GET 請求,但是也可以配置不同的 HTTP 方法,並添加自定義請求頭。稍後我們再介紹如何實現。
-
fetch() 方法是異步工作的,並返回響應。然後使用 then() 方法在響應中設置回調函數:如果接口調用成功就執行第一個回調函數,否則就執行第二個回調。
-
在 “成功” 回調中使用 response。text()讀取響應 body,然後使用 document.getElementById("output").innerHTML 將響應 body 替換 < div> 元素。
-
在 “失敗” 回調中將返回的錯誤消息替換 < div> 元素。
-
整個邏輯放在 document.addEventListener(''DOMContentLoaded', function(){...}),表示 fetch() 函數只有在用戶瀏覽器把 HTML 內容加載完之後纔會執行。
提示:這裏不是關於 JavaScript 的,不用擔心其中的細節。你需要知道的就是 JavaScript 向 API 服務的健康檢查接口發起請求,然後將響應內容填入到 元素中。
演示
下面開始測試下,請啓動第二個應用程序:
$ go run ./cmd/example/cors/simple
2022/01/09 09:57:20 starting server on :9000
然後打開一個新的終端,把我們的 API 應用服務啓動:
$go run ./cmd/api
{"level":"INFO","time":"2022-01-09T01:58:42Z","message":"database connection pool established"}
{"level":"INFO","time":"2022-01-09T01:58:42Z","message":"starting server","properties":{"addr":":4000","env":"development"}}
此時你的 API 服務啓動的域爲 http://localhost:4000,JavaScript 所在網頁運行在域爲:http://localhost:9000。因爲端口不一樣,它們處在不同域。因此,瀏覽器訪問 http://localhost:9000 時,fetch() 向 http://localhost:4000/v1/healthcheck 發起請求時會被同域策略阻止。具體來說,API 服務會接收和處理請求的,但是瀏覽器會阻塞以至於 JavaScript 無法讀取到響應。
讓我們來測試下。打開瀏覽器然後訪問 http://localhost:9000,你會看到 CORS 報頭後面跟着類似這樣的錯誤消息:
提示:這裏的錯誤消息是瀏覽器定義的,我用的是 chrome 瀏覽器,因此如果你使用其他瀏覽器可能錯誤不太一樣。
這裏你可以打開瀏覽器開發者工具,並刷新頁面,然後看看控制檯日誌。你應該能看到一條消息表示 GET /v1/healthcheck 接口的響應因同域策略被阻止。如下所示:
Access to fetch at 'http://localhost:4000/v1/healthcheck' from origin 'http://localhost:9000' has been blocked by CORS policy
您可能還希望打開開發者工具中的網絡活動選項,並檢查與被阻止的請求相關的 HTTP 頭。
這裏有些重要的信息需要指出。首先請求的 url 說明是把請求發送到我們的 API 服務的,並且 API 服務處理完請求後將 200 OK 返回給瀏覽器。請求本身並沒有被同域策略阻止,而是瀏覽器沒有將響應內容傳給 JavaScript。
第二個需要指出的是:web 瀏覽器自動設置請求的 Origin 頭,以顯示請求的來源,如下所示:
Origin: http://localhost:9000
我們將在下一節中使用這個頭信息來幫助我們有選擇地放開同域策略,這取決於我們是否信任請求的來源。最後需要強調的是同域策略只在瀏覽器中使用。在瀏覽器之外,任何應用都可以向 API 服務發起請求,使用 curl、wget 等工具都可以讀取到返回內容。這完全不受同源策略的影響。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/EcB1AR7hvwDRKltrHXQM3g