浏览器的同源安全策略

浏览器只允许请求当前域的资源,拒绝其它域的资源请求。下面这几个地方任何不一样就算是跨域:

  1. 请求协议http,https不同
  2. domain不同
  3. 端口port不同

解决浏览器跨域请求的问题,常见的做法是JSONP(json with padding 填充式json)和CROS(Cross-origin resource sharing 跨域资源共享),JSONP更简单浏览器兼容性也好,CROS控制更精准更安全。JSONP为非标准解决方法,CROS是W3C推荐的标准解决方案。

JSONP演示

这里用Go语言实现一个很简单的http server,返回一段 JS 函数调用形式的字符串。

1
2
3
4
5
6
7
8
9
func HttpJsonpServer() {
	http.HandleFunc("/jsonp", jsonpRoutHandler)
	_ = http.ListenAndServe("0.0.0.0:8003", nil)
}

func jsonpRoutHandler(w http.ResponseWriter, r *http.Request) {
	w.WriteHeader(http.StatusOK)
	_, _ = w.Write([]byte(`jpc({"name": "chende"})`))
}

客户端 jsonp.html 页面也很简单,加入下面几行代码就可以,启动服务器端,然后打开客户端 jsonp.html 页面,不出意外你将在浏览器中看到alert窗口,里面写着 chende 几个字符。就这样客户端页面跨域获得了服务器端的数据。

1
2
3
4
5
6
<script>
    function jpc(data) {
        alert(data.name);
    }
</script>
<script src="http://127.0.0.1:8003/jsonp?cbfunc=jpc"></script>

为什么JSONP只能是GET请求?

JSONP是非官方的跨域请求解决方案,它利用浏览器中的标签加载脚本文件,执行里面的JS代码的方式来达到目的的;

<script src="url"></script>

这里的src只能是发GET请求,指向服务器的一个地址,服务器返回一段JS代码。

CROS演示

我们先用Go写一个简单的Web Server:

1
2
3
4
5
6
7
8
func HttpCrossServer() {
	http.HandleFunc("/cros", crossRoutHandler)
	_ = http.ListenAndServe("0.0.0.0:8003", nil)
}
func crossRoutHandler(w http.ResponseWriter, r *http.Request) {
	w.WriteHeader(http.StatusOK)
	_, _ = w.Write([]byte("data"))
}

浏览器中Ajax的测试代码:

1
2
3
let xhr = new XMLHttpRequest();
xhr.open('GET', 'http://127.0.0.1:8003/');
xhr.send();

谷歌浏览器中查看 Request Headers

GET / HTTP/1.1
Host: 127.0.0.1:8003
Connection: keep-alive
User-Agent: Mozilla/5.0 ...
Accept: */*
Origin: http://localhost:63342
Sec-Fetch-Site: cross-site
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://localhost:63342/
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9

浏览器报错,console控制台看到的错误信息如下:

Access to XMLHttpRequest at 'http://127.0.0.1:8003/' from origin 'http://localhost:63342'has been
blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

接下来把服务器端的代码修改一下,加入相应的允许所有客户端(*)跨域协议头:

1
2
3
4
5
header := w.Header()
header["Access-Control-Allow-Origin"] = []string{"*"}
header["Access-Control-Allow-Credentials"] = []string{"true"}
w.WriteHeader(http.StatusOK)
...

会发现这次请求没有报错,而且有返回结果,就这样跨域请求就实现了。查看Response Headers:

1
2
3
4
5
6
HTTP/1.1 200 OK
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: *
Date: Mon, 23 Nov 2020 06:57:20 GMT
Content-Length: 4
Content-Type: text/plain; charset=utf-8

控制台打印xhr对象console.log(xhr),结果如下:

readyState: 4
response: "data"
responseText: "data"
responseURL: "http://127.0.0.1:8003/cros"
status: 200
statusText: "OK"
timeout: 0
withCredentials: false

另外CROS可以支持客户端发送过来的非GET请求,这种属于非简单模式,这时候服务器端需要做改造:(思考:服务器端是需要判断请求Method,分别对询问和真正的请求返回不同结果的)

1
2
3
4
5
6
7
8
header := w.Header()
// 假设支持特定的域名访问,这里测试直接写访问域
header["Access-Control-Allow-Origin"] = r.Header["Origin"]
header["Access-Control-Allow-Credentials"] = []string{"true"}

// 非简单的GET请求,客户端首先会询问服务器是否允许来自我的跨域请求,服务器需要返回对应的头标识
header["Access-Control-Allow-Methods"] = []string{"GET, POST, PUT"}
header["Access-Control-Allow-Headers"] = []string{"X-Custom-Header"}

非简单模式客户端会发起两次请求,第一次为询问服务器是否支持我跨域请求你,如果允许,才会有第二次请求再次发送。

# 第一次是OPTIONS请求,和服务器来一次协议的确认,浏览器将缓存确认状态,后续新的请求不会每次都询问。
OPTIONS /cros HTTP/1.1
Host: 127.0.0.1:8003
Access-Control-Request-Method: POST
Access-Control-Request-Headers: x-custom-header

# 如果支持,马上发起真正的跨域请求
POST /cros HTTP/1.1
Host: 127.0.0.1:8003
X-Custom-Header: value

参考阮一峰老师的博客:http://www.ruanyifeng.com/blog/2016/04/cors.html

JSONP和CROS如何选择

  1. JSONP只支持GET请求,CORS支持所有类型的HTTP请求。
  2. JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。
  3. JSONP有引发XSS(跨站脚本攻击)的风险,可以利用返回结果前加入死循环代码解决:while(1);

CROS是个跨域规范,在资源访问授权方面进行了限制(Access-Control-Allow-Origin),而且标准浏览器都做了安全限制,比如拒绝手动设置origin字段,相对来说是安全了一点。安全性是相对的,没有绝对的安全。CROS同样可以在服务端设置出现漏洞或者不在浏览器的跨域限制环境下进行攻击,而且它不仅可以读(GET),还可以写(POST等)。

根据实际使用场景来选择技术方案,都需要考虑到安全性的问题。

其它跨域方式

还有一种常用的方案,就是服务器代理。利用Nginx这样的反向代理工具,在网页本域下面代理目标服务器的地址。这样网页中的请求都是同源请求了。不过这种方式略显麻烦,需要维护代理服务器,而且中间经过一次代理,消耗了更多服务器和带宽资源,性能也受到影响;大范围的不建议采用。

(完)