【Go Web開發(fā)】跨域請(qǐng)求

在接下來的文章中酌媒,我們將討論一個(gè)全新的主題,并更新我們的應(yīng)用程序贡珊,使其支持來自JavaScript的跨域請(qǐng)求(CORS: Cross-origin resource sharing))摹恨。

你將學(xué)習(xí)到:

  • 什么是跨域請(qǐng)求,為什么瀏覽器默認(rèn)阻止跨域請(qǐng)求茫孔。
  • 普通請(qǐng)求和跨域請(qǐng)求之間的區(qū)別叮喳。
  • 如何使用Access-Control請(qǐng)求頭來允許或拒絕特定的跨域請(qǐng)求。
  • 考慮到安全因素缰贝,你需要注意什么時(shí)候配置你的應(yīng)用程序可以跨域請(qǐng)求馍悟。

CORS概述

在深入代碼前,或者開始具體討論跨域請(qǐng)求之前剩晴,讓我們先來定義一下術(shù)語origin的含義锣咒。

本質(zhì)上,如果兩個(gè)url具有相同的schema赞弥、主機(jī)和端口(如果指定了)毅整,則稱它們具備相同的域。為了說明這一點(diǎn)绽左,讓我們比較以下url:

URL A URL B 同域? 原因
https://foo.com/a http://foo.com/a NO schema不同(http vs https)
https://foo.com/a http://www.foo.com/a NO host不同(foo.com vs www.foo.com)
https://foo.com/a http://foo.com:443/a NO 端口不同(默認(rèn)端口 vs 443)
https://foo.com/a http://foo.com/b NO url路徑不同
https://foo.com/a http://foo.com/a?b=c NO 查詢字符串不同
https://foo.com/a#b http://foo.com/a#c NO 路徑不通
https://foo.com/a#b http://foo.com/a NO schema不同(http vs https)

理解什么是origin是很重要的悼嫉,因?yàn)樗械膚eb瀏覽器都實(shí)現(xiàn)了一種被稱為同域策略的安全機(jī)制。在瀏覽器實(shí)現(xiàn)這個(gè)策略的方式上有一些非常小的區(qū)別拼窥,但廣義上來說:

  • 一個(gè)域的網(wǎng)頁可以在其HTML中嵌入來自另一個(gè)域的特定類型的資源——包括圖像戏蔑、CSS和JavaScript文件。例如鲁纠,在你的網(wǎng)頁中這樣做是可以的:
 <img src="http://anotherorigin.com/example.png" alt="example image">
  • 一個(gè)域上的網(wǎng)頁可以向另一個(gè)域發(fā)送數(shù)據(jù)辛臊。例如,網(wǎng)頁中的HTML表單可以向不同的域提交數(shù)據(jù)房交。
  • 但是一個(gè)域上的網(wǎng)頁不允許接收來自不同域的數(shù)據(jù)。

這里的關(guān)鍵是最后一個(gè)要點(diǎn):同域策略不允許(潛在惡意)其他域網(wǎng)站從本域讀取數(shù)據(jù)伐割。

必須強(qiáng)調(diào)的是候味,同域策略不會(huì)阻止數(shù)據(jù)的跨域發(fā)送,盡管這很危險(xiǎn)隔心。事實(shí)上白群,這就是為什么CSRF攻擊可能發(fā)生,以及為什么我們需要采取額外的步驟來防止它們——比如使用samsite cookie和CSRF令牌硬霍。

作為一名開發(fā)人員帜慢,您最可能遇到同域策略是在瀏覽器中運(yùn)行JavaScript的跨域請(qǐng)求時(shí)。例如:假設(shè)你有一個(gè)http://foo.com網(wǎng)頁包含一些前端JavaScript代碼唯卖。如果這個(gè)JavaScript試圖發(fā)起https://bar.com/data.json請(qǐng)求(不同的域)粱玲,然后請(qǐng)求被發(fā)送到bar.com服務(wù)端處理,但用戶的瀏覽器將阻止接收響應(yīng)拜轨,以至于https://foo.com這邊的JavaScript代碼接收不到返回?cái)?shù)據(jù)抽减。

一般來說,同域策略是一種非常有用的安全保護(hù)措施橄碾。雖然它在一般情況下是好的卵沉,但在某些情況下颠锉,你可能想要將這種約束放開。例如史汗,你有一個(gè)API服務(wù)域名為api.example.com和一個(gè)JavaScript前端應(yīng)用運(yùn)行在www.example.com琼掠,那么你可能想要允許www.example.com前端應(yīng)用跨域訪問API服務(wù)。

或者你有一個(gè)完全開放的公共API服務(wù)停撞,你想允許來自任何地方的跨域請(qǐng)求瓷蛙,這樣其他開發(fā)人員就可以很容易地與他們自己的網(wǎng)站集成。

幸運(yùn)的是怜森,大多數(shù)現(xiàn)代web瀏覽器允許你通過設(shè)置API響應(yīng)的訪問控制頭來允許或禁止特定的跨域請(qǐng)求速挑。在接下來的幾節(jié)中,我們將詳細(xì)解釋如何做到這一點(diǎn)以及這些響應(yīng)頭是如何工作的副硅。

演示同源(Same-Origin)策略

為了演示同源策略是如何工作的姥宝,以及如何在對(duì)API的請(qǐng)求中放開同源策略,我們需要模擬一個(gè)來自不同源的對(duì)API的請(qǐng)求恐疲。

可以快速地創(chuàng)建一個(gè)簡(jiǎn)單的Go應(yīng)用程序來模擬這個(gè)跨域請(qǐng)求腊满。從本質(zhì)上說,我們希望第二個(gè)應(yīng)用程序包含一些JavaScript的網(wǎng)頁培己,然后向我們的 GET / v1 / healthcheck接口發(fā)起請(qǐng)求碳蛋。

如果你跟隨本系列文章的操作,創(chuàng)建文件cmd/example/cors/simple/main.go來寫我們的第二個(gè)應(yīng)用程序省咨。

$ 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網(wǎng)頁字符串常量肃弟。
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()  {
    //允許服務(wù)地址在運(yùn)行時(shí)可根據(jù)命令行參數(shù)配置
    addr := flag.String("addr", ":9000", "Server address")
    flag.Parse()

    log.Printf("starting server on %s", *addr)
    //啟動(dòng)HTTP服務(wù),并監(jiān)聽給定的地址零蓉。并對(duì)上面的HTML中所有請(qǐng)求進(jìn)行應(yīng)答笤受。
    err := http.ListenAndServe(*addr, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte(html))
    }))
    log.Fatal(err)
}

這里Go代碼我們應(yīng)該很熟悉了,下面一起來看看<script>標(biāo)簽中的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>

在這個(gè)代碼中:

  • 我們使用fetch()函數(shù)向API服務(wù)的健康檢查接口發(fā)起請(qǐng)求敌蜂。默認(rèn)發(fā)送的是GET請(qǐng)求箩兽,但是也可以配置不同的HTTP方法,并添加自定義請(qǐng)求頭章喉。稍后我們?cè)俳榻B如何實(shí)現(xiàn)汗贫。
  • fetch()方法是異步工作的,并返回響應(yīng)秸脱。然后使用then()方法在響應(yīng)中設(shè)置回調(diào)函數(shù):如果接口調(diào)用成功就執(zhí)行第一個(gè)回調(diào)函數(shù)落包,否則就執(zhí)行第二個(gè)回調(diào)。
  • 在“成功”回調(diào)中使用response摊唇。text()讀取響應(yīng)body妥色,然后使用document.getElementById("output").innerHTML將響應(yīng)body替換<div id="output"><div>元素。
  • 在“失敗”回調(diào)中將返回的錯(cuò)誤消息替換<div id="output"><div>元素遏片。
  • 整個(gè)邏輯放在document.addEventListener(''DOMContentLoaded', function(){...})嘹害,表示fetch()函數(shù)只有在用戶瀏覽器把HTML內(nèi)容加載完之后才會(huì)執(zhí)行撮竿。

提示:這里不是關(guān)于JavaScript的,不用擔(dān)心其中的細(xì)節(jié)笔呀。你需要知道的就是JavaScript向API服務(wù)的健康檢查接口發(fā)起請(qǐng)求幢踏,然后將響應(yīng)內(nèi)容填入到<div id="output"><div>元素中。

演示

下面開始測(cè)試下许师,請(qǐng)啟動(dòng)第二個(gè)應(yīng)用程序:

$ go run ./cmd/example/cors/simple 
2022/01/09 09:57:20 starting server on :9000

然后打開一個(gè)新的終端房蝉,把我們的API應(yīng)用服務(wù)啟動(dòng):

$ 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"}}

此時(shí)你的API服務(wù)啟動(dòng)的域?yàn)?a target="_blank">http://localhost:4000,JavaScript所在網(wǎng)頁運(yùn)行在域?yàn)椋?a target="_blank">http://localhost:9000微渠。因?yàn)槎丝诓灰粯哟罨茫鼈兲幵诓煌颉R虼顺雅瑁瑸g覽器訪問http://localhost:9000時(shí)檀蹋,fetch()向http://localhost:4000/v1/healthcheck發(fā)起請(qǐng)求時(shí)會(huì)被同域策略阻止。具體來說云芦,API服務(wù)會(huì)接收和處理請(qǐng)求的俯逾,但是瀏覽器會(huì)阻塞以至于JavaScript無法讀取到響應(yīng)。

讓我們來測(cè)試下舅逸。打開瀏覽器然后訪問http://localhost:9000桌肴,你會(huì)看到CORS報(bào)頭后面跟著類似這樣的錯(cuò)誤消息:

提示:這里的錯(cuò)誤消息是瀏覽器定義的,我用的是chrome瀏覽器琉历,因此如果你使用其他瀏覽器可能錯(cuò)誤不太一樣坠七。

這里你可以打開瀏覽器開發(fā)者工具,并刷新頁面旗笔,然后看看控制臺(tái)日志灼捂。你應(yīng)該能看到一條消息表示GET /v1/healthcheck接口的響應(yīng)因同域策略被阻止。如下所示:

Access to fetch at 'http://localhost:4000/v1/healthcheck' from origin 'http://localhost:9000' has been blocked by CORS policy

您可能還希望打開開發(fā)者工具中的網(wǎng)絡(luò)活動(dòng)選項(xiàng)换团,并檢查與被阻止的請(qǐng)求相關(guān)的HTTP頭。


這里有些重要的信息需要指出宫蛆。首先請(qǐng)求的url說明是把請(qǐng)求發(fā)送到我們的API服務(wù)的艘包,并且API服務(wù)處理完請(qǐng)求后將200 OK返回給瀏覽器。請(qǐng)求本身并沒有被同域策略阻止耀盗,而是瀏覽器沒有將響應(yīng)內(nèi)容傳給JavaScript想虎。

第二個(gè)需要指出的是:web瀏覽器自動(dòng)設(shè)置請(qǐng)求的Origin頭,以顯示請(qǐng)求的來源叛拷,如下所示:

Origin: http://localhost:9000

我們將在下一節(jié)中使用這個(gè)頭信息來幫助我們有選擇地放開同域策略舌厨,這取決于我們是否信任請(qǐng)求的來源。最后需要強(qiáng)調(diào)的是同域策略只在瀏覽器中使用忿薇。在瀏覽器之外裙椭,任何應(yīng)用都可以向API服務(wù)發(fā)起請(qǐng)求躏哩,使用curl、wget等工具都可以讀取到返回內(nèi)容揉燃。這完全不受同源策略的影響扫尺。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市炊汤,隨后出現(xiàn)的幾起案子正驻,更是在濱河造成了極大的恐慌,老刑警劉巖抢腐,帶你破解...
    沈念sama閱讀 222,252評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件姑曙,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡迈倍,警方通過查閱死者的電腦和手機(jī)伤靠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,886評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來授瘦,“玉大人醋界,你說我怎么就攤上這事√嵬辏” “怎么了形纺?”我有些...
    開封第一講書人閱讀 168,814評(píng)論 0 361
  • 文/不壞的土叔 我叫張陵,是天一觀的道長徒欣。 經(jīng)常有香客問我逐样,道長,這世上最難降的妖魔是什么打肝? 我笑而不...
    開封第一講書人閱讀 59,869評(píng)論 1 299
  • 正文 為了忘掉前任脂新,我火速辦了婚禮,結(jié)果婚禮上粗梭,老公的妹妹穿的比我還像新娘争便。我一直安慰自己,他們只是感情好断医,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,888評(píng)論 6 398
  • 文/花漫 我一把揭開白布滞乙。 她就那樣靜靜地躺著,像睡著了一般鉴嗤。 火紅的嫁衣襯著肌膚如雪斩启。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,475評(píng)論 1 312
  • 那天醉锅,我揣著相機(jī)與錄音兔簇,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛垄琐,可吹牛的內(nèi)容都是我干的边酒。 我是一名探鬼主播,決...
    沈念sama閱讀 41,010評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼此虑,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼甚纲!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起朦前,我...
    開封第一講書人閱讀 39,924評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤介杆,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后韭寸,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體春哨,經(jīng)...
    沈念sama閱讀 46,469評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,552評(píng)論 3 342
  • 正文 我和宋清朗相戀三年恩伺,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了赴背。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,680評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡晶渠,死狀恐怖凰荚,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情褒脯,我是刑警寧澤便瑟,帶...
    沈念sama閱讀 36,362評(píng)論 5 351
  • 正文 年R本政府宣布,位于F島的核電站番川,受9級(jí)特大地震影響到涂,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜颁督,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,037評(píng)論 3 335
  • 文/蒙蒙 一践啄、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧沉御,春花似錦屿讽、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,519評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至硫痰,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間窜护,已是汗流浹背效斑。 一陣腳步聲響...
    開封第一講書人閱讀 33,621評(píng)論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留柱徙,地道東北人缓屠。 一個(gè)月前我還...
    沈念sama閱讀 49,099評(píng)論 3 378
  • 正文 我出身青樓奇昙,卻偏偏與公主長得像,于是被迫代替她去往敵國和親敌完。 傳聞我的和親對(duì)象是個(gè)殘疾皇子储耐,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,691評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容