CORS的本质
CORS
(Cross-Origin Resource Sharing跨源资源共享) 是一种认证机制,是 W3C
(万维网联盟) 推荐的一种用于跨域资源访问的安全策略。
源与同源策略
CORS 中的源指的是某个URL中的协议、域名和端口,由这三个元素标识一个唯一的源,如 http://localhost:8080
和 https://localhost:8000
是不同的源,因为它们的协议和端口都不同。
源也可以宽泛地理解为一个 Web站点,通常我们访问一个网站的时候,加载的静态资源通常都是站内的。比如下图的例子,当我们访问必应的首页时,http请求中的图片资源,也是来自必应网站的。
访问站内资源一般不会有限制,这里也可以引申出一个安全机制叫 同源策略 (same-origin policy)
。
当我们访问网页时,网页中的不同静态资源,比如图片、JS脚本等可以通过url的方式加载进来,在同源策略的安全机制下,这些url只能属于同一个源。而CORS允许网页从互联网上的其他Web站点加载静态资源。
CORS的实现机制
CORS根据请求类型可以分为静态和动态两种:
- 静态的CORS的常见例子就是 HTML 中的 img 标签,src字段设定为其他 Web站点的 URL,这就是一次静态的CORS例子
<img src="http://www.other-origin.com" />
- 动态的CORS请求就是在Ajax请求中访问跨域资源,处于安全考虑,浏览器限制从脚本中发起的跨域HTTP请求,接下来也主要讨论这类CORS请求
跟实现CORS相关的几个Header
Origin:
当浏览器检测到某个Ajax请求要访问跨域资源时,会在请求头中加上Origin
,Origin 通常就是从 Referer 字段中截取协议、域名和端口相关的信息,用于表示Ajax请求的来源站点Access-Control-Allow-Origin:
如果服务器支持CORS的话,会在响应头中加上这个字段,表示服务器信任的源,当值为"*"
时,表示服务器的资源信任所有的源;浏览器发起CORS请求后也会检查这个字段,如果响应头中没有这个字段或者不信任源,都会在控制台报错Access-Control-Allow-Methods:
发起跨域请求时允许的HTTP方法,由逗号分隔如GET,OPTIONS
表示只能通过这些方法发起跨域请求,其他 DELETE,PUT,POST等可能改变资源的方法会被拒绝
CORS实现
从以上介绍的几个CORS相关的Header也可以看出,CORS的实现需要客户端(浏览器)和服务端的协同配合:
- 浏览器要支持在发起跨域请求时附加
Origin
头,并在响应返回时检查Access-Control-Allow-Origin
等头部,判断服务端是否支持或允许该跨域请求。现代的浏览器基本都支持CORS。 - 服务端要在响应中设置
Access-Control-Allow-Origin
等响应头,表示自己支持CORS,并声明自己的安全策略
ps:CORS相关的HTTP头还有很多,还根据请求的方法和Content-Type等分为简单请求和预检请求,这些知识点主要看下方的参考文档,本文不再赘述
在Go语言中验证CORS
因为要验证多个源之间的资源访问,所以要启动两个Web服务,这里使用 Gin 框架启动两个Web服务:
WebServer A(服务端支持CORS)
type CORSOptions struct {
Origin string
}
// 参考 https://github.com/gin-gonic/gin/issues/29#issuecomment-89132826
func CORS(options CORSOptions) gin.HandlerFunc {
return func(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin","*")
if options.Origin != "" {
c.Writer.Header().Set("Access-Control-Allow-Origin",options.Origin)
}
c.Writer.Header().Set("Access-Control-Max-Age","86400")
c.Writer.Header().Set("Access-Control-Allow-Methods","POST, GET, OPTIONS, PUT, DELETE, UPDATE")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Origin, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, X-Auth-Token, X-Auth-UUID, X-Auth-Openid, referrer, Authorization, x-client-id, x-client-version, x-client-type")
c.Writer.Header().Set("Access-Control-Expose-Headers", "Content-Length")
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(200)
} else {
c.Next()
}
}
}
func main() {
r := gin.New()
// 应用CORS中间件
r.Use(CORS(CORSOptions{Origin:""}))
// 设置静态资源的访问路由,目录下包含一个图片文件
r.StaticFS("public",http.Dir("template/home"))
// 加载用于验证CORS的网页
r.LoadHTMLGlob("template/home/*.tmpl")
r.GET ("/cors",func(c *gin.Context) {
c.HTML(http.StatusOK,"cors.tmpl",nil)
})
r.Run(":8081")
}
WebServer B(服务端不支持CORS)
func main() {
r := gin.New()
// 设置静态资源的访问路由,目录下包含一个图片文件
r.StaticFS("public",http.Dir("template/home"))
// 加载用于验证CORS的网页
r.LoadHTMLGlob("template/home/*.tmpl")
r.GET ("/cors",func(c *gin.Context) {
c.HTML(http.StatusOK,"cors.tmpl",nil)
})
r.Run(":8082")
}
用于验证的网页模板
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<div>
<!--在WebServer A中端口号设置为8082,在WebServer A中端口号设置为8081,用于访问对方的资源-->
<img src="http://localhost:8082/public/test.jpg" />
<div id="img_Div"></div>
<script type="text/javascript">
//XmlHttpRequest对象
function createXmlHttpRequest(){
if(window.ActiveXObject){ //如果是IE浏览器
return new ActiveXObject("Microsoft.XMLHTTP");
}else if(window.XMLHttpRequest){ //非IE浏览器
return new XMLHttpRequest();
}
}
function getFile() {
var img_Container = document.getElementById("img_Div");
var xhr = createXmlHttpRequest();
//在WebServer A中端口号设置为8082,在WebServer A中端口号设置为8081,用于访问对方的资源
xhr.open('GET', 'http://localhost:8082/public/test.jpg', true);
xhr.setRequestHeader('Content-Type', 'image/jpeg');
xhr.responseType = "blob";
xhr.onload = function() {
if (this.status == 200) {
var blob = this.response;
var img = document.createElement("img");
img.onload = function(e) {
window.URL.revokeObjectURL(img.src);
};
img.src = window.URL.createObjectURL(blob);
img_Container.appendChild(img);
}
}
xhr.send(null);
}
</script>
<div class="row">
<input type="button" onclick="getFile()" value="Get" />
</div>
</div>
</body>
</html>
验证结果
从例子中也可以看到,浏览器对静态CORS请求的资源不会做验证,只对JS脚本中发起的Ajax跨域请求应用CORS验证,这大概是因为跨域资源访问带来的安全风险大多是由恶意脚本发起的跨域请求引起的。
CORS的安全必要性
正如文章开头说的那样,通常Web服务器的静态文件资源只提供给同源的网站加载。当业务系统中由多个Web应用时,可能需要共享某些静态资源,这时我们会把Access-Control-Allow-Origin
设置为"*",方便资源共享,但是这也意味着互联网上其他站点也可以访问你的静态资源,尤其Access-Control-Allow-Methods
包含 PUT, POST, DELETE 等可以操作静态资源的方法时,可能就会被恶意利用和篡改。
比如 CSRF 攻击,就是诱导用户访问Web A,然后窃取用户的cookie信息并执行恶意脚本访问用户之前访问的 Web B,由于黑客已经获取了用户的认证信息(cookie)以及Web B服务端支持CORS,所以就可以在跨域请求中以用户的名义执行恶意代码。
因此,服务端在考虑是否支持CORS和跨域请求的安全策略时,一定要结合业务场景和安全需求,谨慎设置Access-Control-Allow-Origin
和 Access-Control-Allow-Methods
等响应头