Golang Gin 實戰(九)| JSONP跨域和劫持
- 2020 年 2 月 10 日
- 筆記
瀏覽器都遵循同源策略,也就是說位於www.flysnow.org
下的網頁是無法訪問非www.flysnow.org
下的數據的,比如我們常見的AJAX跨域問題。
要解決跨域問題的辦法有CORS、代理和JSONP,這裡結合Gin,主要介紹JSONP模式
JSONP原理
JSONP可以跨域,主要是利用了<script>
跨域的能力,因為這個標籤我們可以引用任何域名下的JS文件。既然是這樣,我們就可以利用這個能力,在服務端生成相應的JS程式碼,並且把返回的Content-type
設置為application/javascript
即可。
在生成這個這個對應的JS程式碼的時候,就比較有講究了,一般是調用客戶端網頁已經存在的JS函數。
<script type="text/javascript"> function sayHello(data){ alert(JSON.stringify(data)); } </script>
以上我們定義了一個JS函數sayHello
,可以通過alert
的方式,顯示對應的data
數據。
假設我們通過http://localhost:8080/jsonp?callback=sayHello
來調用sayHello
函數,那麼我們這個URL輸出的內容要是這樣:
sayHello({"wechat":"flysnow_org"});
並且對應的Content-type
設置為application/javascript
,這樣才能達到調用sayHello
函數的目的,也就是通過JSONP的方式解決了數據跨域讀取的問題。
最終,我們的網頁的源程式碼是這樣的:
<script type="text/javascript"> function sayHello(data){ alert(JSON.stringify(data)); } </script> <script type="text/javascript" src="http://localhost:8080/jsonp?callback=sayHello"></script>
注意前後順序,必須要先定義sayHello
函數。
Gin JSONP 實現
要通過Gin來實現服務端對JSONP的支援非常簡單,只需要使用JSONP
函數即可。
func main() { r := gin.Default() r.GET("/jsonp", func(c *gin.Context) { c.JSONP(200, gin.H{"wechat": "flysnow_org"}) }) r.Run(":8080") }
它的使用方法和c.JSON
一模一樣,第一個參數是HTTP Status Code,第二個是返回的數據。
func (c *Context) JSONP(code int, obj interface{}) { callback := c.DefaultQuery("callback", "") if callback == "" { c.Render(code, render.JSON{Data: obj}) return } c.Render(code, render.JsonpJSON{Callback: callback, Data: obj}) }
通過上面的源程式碼,我們發現Gin指定callback
參數名作為接收回調函數的參數名,所以我們上面的例子,是通過http://localhost:8080/jsonp?callback=sayHello"
把sayHello
這個回調JS函數傳遞給服務端,這樣服務端才會返回對應的JS類型的字元串,才能被瀏覽器執行,達到跨域的目的。
sayHello({"wechat":"flysnow_org"});
通過上面的源程式碼,我們也可以發現,如果我們沒有傳遞callback
對應的回調函數,它就會調用c.Render(code, render.JSON{Data: obj})
,和我們直接使用c.JSON
方法是一樣的,直接輸出JSON字元串。
JSONP劫持
JSONP的方法其實是不推薦的,因為它的安全性是個問題,也就是JSONP劫持。JSONP劫持其實是JSON劫持的一部分,JSON劫持是包含JSONP劫持的。
JSON劫持還會稍微麻煩一些,因為需要數據是JSON數組,並且要重寫JS Array的構建函數;而JSONP,因為有現成的JS回調函數,直接重寫JS回調函數就可以了,JSONP讓JSON劫持更簡單了。
JSON劫持
上面我們介紹了JSON劫持,那麼我們如何避免它呢?其實現在的瀏覽器基本上修復了這個問題,但是我們這裡介紹下Gin防止JSON劫持的策略。
JSON劫持,其實就是惡意網站,通過<script>
標籤獲取你的JSON數據,因為JSON數組默認為是可執行的JS,所以通過這種方式,可以獲得你的敏感數據,當然條件還有很多,可以Google JSON劫持詳細了解。
對於Gin,解決JSON劫持的方法很簡單:
r := gin.Default() a := []string{"1", "2", "3"} r.GET("/secureJson", func(c *gin.Context) { c.SecureJSON(200, a) })
運行訪問http://localhost:8080/secureJson
會發現如下資訊:
while(1);["1","2","3"]
最前面有個while(1);
前綴,這就可以在<script>
標籤執行我們返回的數據時,就可以無限循環,阻止後面數組數據的執行,防止數據被劫持。
Gin默認的防JSON劫持的前綴是while(1);
,我們可以改變,通過r.SecureJsonPrefix
方法設置即可,如:
r := gin.Default() r.SecureJsonPrefix("for(;;);")
據說Google採用的是while
的方法,facebook採用的是for
的方法。
小結
雖然Gin對JSONP提供了很好的支援,但是我們並不推薦使用,因為JSONP劫持問題,如果要跨域還是使用代理或者CORS比較好。更多關於Gin的討論可以加入我的星球Golang Gin 實戰,有更深入的討論,一對一的答疑,公眾號和部落格沒有的源程式碼分析。