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 實戰,有更深入的討論,一對一的答疑,公眾號和部落格沒有的源程式碼分析。