跨域资源共享(CORS)安全性
背景
提起浏览器的同源策略,大家都很熟悉。不同域的客户端脚本不能读写对方的资源。但是实践中有一些场景需要跨域的读写,所以出现了一些hack的方式来跨域。比如在同域内做一个代理,JSON-P等。但这些方式都存在缺陷,无法完美的实现跨域读写。所以在XMLHttpRequest v2标准下,提出了CORS(Cross Origin Resourse-Sharing)的模型,试图提供安全方便的跨域读写资源。目前主流浏览器均支持CORS。
技术原理
CORS定义了两种跨域请求,简单跨域请求和非简单跨域请求。当一个跨域请求发送简单跨域请求包括:请求方法为HEAD,GET,POST;请求头只有4个字段,Accept,Accept-Language,Content-Language,Last-Event-ID;如果设置了Content-Type,则其值只能是application/x-www-form-urlencoded,multipart/form-data,text/plain。说起来比较别扭,简单的意思就是设置了一个白名单,符合这个条件的才是简单请求。其他不符合的都是非简单请求。
之所以有这个分类是因为浏览器对简单请求和非简单请求的处理机制是不一样的。当我们需要发送一个跨域请求的时候,浏览器会首先检查这个请求,如果它符合上面所述的简单跨域请求,浏览器就会立刻发送这个请求。如果浏览器检查之后发现这是一个非简单请求,比如请求头含有X-Forwarded-For字段。这时候浏览器不会马上发送这个请求,而是有一个preflight,跟服务器验证的过程。浏览器先发送一个options方法的预检请求。如果预检通过,则发送这个请求,否则就不拒绝发送这个跨域请求。
下面详细分析一下实现安全跨域请求的控制方式。先看一下非简单请求的预检过程。
非简单请求
浏览器先发送一个options方法的请求。带有如下字段:
- Origin: 普通的HTTP请求也会带有,在CORS中专门作为Origin信息供后端比对,表明来源域。
- Access-Control-Request-Method: 接下来请求的方法,例如PUT, DELETE等等
- Access-Control-Request-Headers: 自定义的头部,所有用setRequestHeader方法设置的头部都将会以逗号隔开的形式包含在这个头中
服务器收到"预检"请求以后,检查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段以后,确认允许跨源请求,就可以做出回应。
会返回对应对的字段
- Access-Control-Allow-Origin:
- Access-Control-Allow-Methods:该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次“预检”请求。
- Access-Control-Allow-Headers:
如果服务器否定了"预检"请求,会返回一个正常的HTTP回应,但是没有任何CORS相关的头信息字段。这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误,被XMLHttpRequest对象的onerror回调函数捕获
一旦服务器通过了"预检"请求,以后每次浏览器正常的CORS请求,就都跟简单请求一样,会有一个Origin头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin头信息字段。
为什么要预检?
这是为了防止这些新增的请求,对传统的没有 CORS 支持的服务器形成压力,给服务器一个提前拒绝的机会,这样可以防止服务器大量收到DELETE和PUT请求,这些传统的表单不可能跨域发出的请求。
简单请求
简单请求前面讲过是直接发送,只是多加一个origin字段表明跨域请求的来源。
如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段(详见下文),就知道出错了,从而抛出一个错误,被XMLHttpRequest的onerror回调函数捕获。注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。
如果Origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。
- Access-Control-Allow-Origin: 允许跨域访问的域,可以是一个域的列表,也可以是通配符"*"。这里要注意Origin规则只对域名有效,并不会对子目录有效。即http://foo.example/subdir/ 是无效的。但是不同子域名需要分开设置,这里的规则可以参照同源策略
- Access-Control-Allow-Credentials: 是否允许请求带有验证信息,这部分将会在下面详细解释
- Access-Control-Expose-Headers: 允许脚本访问的返回头,请求成功后,脚本可以在XMLHttpRequest中访问这些头的信息(貌似webkit没有实现这个)
- Access-Control-Max-Age: 缓存此次请求的秒数。在这个时间范围内,所有同类型的请求都将不再发送预检请求而是直接使用此次返回的头作为判断依据,非常有用,大幅优化请求次数
- Access-Control-Allow-Methods: 允许使用的请求方法,以逗号隔开
- Access-Control-Allow-Headers: 允许自定义的头部,以逗号隔开,大小写不敏感
然后浏览器通过返回结果的这些控制字段来决定是将结果开放给客户端脚本读取还是屏蔽掉。如果服务器没有配置cors,返回结果没有控制字段,浏览器会屏蔽脚本对返回信息的读取。
withCredentials
withCredentials是什么?
withCredentials是XMLHttpRequest的一个属性,表示跨域请求是否提供凭据信息(cookie、HTTP认证及客户端SSL证明等)
实际中用途就是跨域请求是要不要携带cookie
在需要跨域携带cookie时,要把withCredentials设置为true,比如
var xhr = new XMLHttpRequest()
xhr.withCredentials = true
xhr.open('GET', 'http://localhost:8888/', true)
xhr.send(null)
服务端的设置
只有客户端设置当然不够了,服务端还需要设置两点
比如你页面所在的域名为http://www.abc.com,服务端的Access-Control-Allow-Origin,必须是http://www.abc.com
- Access-Control-Allow-Credentials
在响应头中,Access-Control-Allow-Credentials这个值也要设置为true,根据mdn上的说法,只有设置为true的时候,浏览器才会把响应结果暴露给你的js代码
- Access-Control-Allow-Origin
既然是跨域请求,服务端要设置Access-Control-Allow-Origin,告诉浏览器允许跨域,而且这个值必须指定域名,不能设置为*
尽管浏览器可以支持通配符,但是不能同时将凭证标志设置成true。
就像下面这种头部配置:
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
这样配置浏览器将会报错,因为在响应具有凭据的请求时,服务器必须指定单个域,所不能使用通配符。简单的使用通配符将有效的禁用“Access-Control-Allow-Credentials”这个字段。这些限制和行为的结果就是许多CORS的实现方式是根据“Origin”这个头部字段的值来生成“AccessControl-Allow-Origin”的值
为什么不能两者共存?
默认情况下,如果没有设置“Access-Control-Allow-Credentials”这个头的话,浏览器发送的请求就不会带有用户的身份数据(cookie或者HTTP身份数据),所以就不会泄露用户隐私信息。下面这个图展示一个简单的CORS请求流:
其实图片所展示的就是经典的CSRF攻击。而Origin的限制,是为了明确是哪个站点发送的请求,根据Origin就可以发现是钓鱼网站发起的请求。从而避免cookie的泄露。
参考文献:
CORS通信
跨域资源共享(CORS)安全性浅析
【web前端】withCredentials有什么作用
cors安全完全指南