浏览器都遵循同源策略,也就是说位于www.flysnow.org下的网页是无法访问非www.flysnow.org下的数据的,比如我们常见的AJAX跨域问题。

要解决跨域问题的办法有CORS、代理和JSONP,这里结合Gin,主要介绍JSONP模式

JSONP原理

JSONP可以跨域,主要是利用了<script>跨域的能力,因为这个标签我们可以引用任何域名下的JS文件。既然是这样,我们就可以利用这个能力,在服务端生成相应的JS代码,并且把返回的Content-type设置为application/javascript即可。

在生成这个这个对应的JS代码的时候,就比较有讲究了,一般是调用客户端网页已经存在的JS函数。

1
2
3
4
5
<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的方式解决了数据跨域读取的问题。

最终,我们的网页的源代码是这样的:

1
2
3
4
5
6
<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函数即可。

1
2
3
4
5
6
7
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,第二个是返回的数据。

1
2
3
4
5
6
7
8
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劫持的方法很简单:

1
2
3
4
5
    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方法设置即可,如:

1
2
r := gin.Default()
r.SecureJsonPrefix("for(;;);")

据说Google采用的是while的方法,facebook采用的是for的方法。

小结

虽然Gin对JSONP提供了很好的支持,但是我们并不推荐使用,因为JSONP劫持问题,如果要跨域还是使用代理或者CORS比较好。更多关于Gin的讨论可以加入我的星球Golang Gin 实战,有更深入的讨论,一对一的答疑,公众号和博客没有的源代码分析。

扫码加入Golang Gin 实战星球

精彩文章推荐

Golang Gin 实战(八)| JSON渲染输出

Golang Gin 实战(七)| 分组路由源代码分析

Golang Gin 实战(六)| 获取Form表单参数和原理分析

Golang Gin 实战(五)| 接收数组和map

Golang Gin 实战(四)| URL查询参数的获取和原理分析

Golang Gin 实战(三)| 路由参数

Golang Gin 实战(二)| 简便的Restful API 实现

Golang Gin 实战(一)| 快速安装入门

我有几个的Go语言交流微信群,可以扫码关注公众号flysnow_org或者网站 https://www.flysnow.org/,加我好友,我拉你进来。

扫码关注