前言
一个请求从发出到返回,需要浏览器和服务端的协调配合。浏览器要把自己的请求参数带给服务端,服务端校验参数之后,除了返回数据,也可能会顺便把请求是否缓存,cookie等信息告诉浏览器。当请求是跨域请求的时候,这个过程还要复杂一些。接下来咱们就看看跨域会有什么问题,又需要前后端进行怎样的配合。
普通跨域
我有一个朋友,叫小王。前端小王和后端同事小马准备联调一个登录的api。假设是/login;小王在把登录账号和密码都准备好之后,愉快的发起了post提交。结果很意外,请求的响应被浏览器拦截了,浏览器还贴心的在console上抛出了一个错误。
小王翻译了一下,原来是被CORS策略拦截掉了。这个策略大概意思是说,服务端如果允许不同origin的请求,那就需要在返回的response header里面带上Access-Control-Allow-Origin这个header。否则浏览器在拿到响应并发现响应头里没有这个header时,就会把响应给吞掉,而不会交给js进行下一步处理。
小王把这个事情告诉了小马,然后小马在返回的header中加上了
Access-Control-Allow-Origin: *
现在小王终于可以拿到返回的结果了。
这里要注意,浏览器不是在请求阶段就对请求进行拦截,而是正常发出请求,拿到服务端的响应之后,开始查看响应header里面有没有Access-Control-Allow-Origin这个header,如果没有,响应的结果就不会到js那里去。
非简单请求的跨域
后来小王觉得在post中发送表单格式的body太麻烦,希望使用JSON格式的请求体提交。小马觉得就是几行代码的事,就同意了。但是小王改成JSON的消息体之后发现又被CORS拦截了,并抛出了下面的错误:
在上面的报错中,我们看到了 preflight 的单词。那这又是怎么回事呢?原来,修改请求体之后,这个跨域请求不再是简单请求了,需要在发起请求之前先进行 preflight 请求。那么什么是简单请求呢?
- 请求方法包括GET, HEAD, POST
- response header里面不能包含cors安全header以外的header。
- Content-Type 只限于text/plain, multipart/form-data, application/x-www-form-urlencoded
由于json数据的content-type导致这个post请求不再是简单请求,而对于非简单请求,之前允许所有域名跨域访问是被禁止的。所以还是要修改Access
Control-Allow-Origin为特定的请求域名。在开发模式下,可能是-http://localhost:3000之类的。
小马在重新修改Access-Control-Allow-Origin,小王又拿到了登录成功的结果。可以联调下一个api了。
带cookie的跨域
登录是基于session的,也就是说,登录成功后,server会通过set-cookie,将cookie设置到浏览器中,这样,下次访问同源下的api时,cookie就会被带上。
然而,奇怪的是,小王发现登录成功后,调用别的接口,cookie并没有被带上,导致server无法识别出用户信息,最终返回错误(状态码为401)。
withCredentials
原来,浏览器发起跨域请求的时候,是不会主动带上cookie的,如果一个请求需要cookie,需要开发者设置一个选项,以fetch api为例:
fetch('http://baidu.com:3000', {
// ...
credentials: 'include'
})
如果使用xhr api来请求,则需要这样写:
var invocation = new XMLHttpRequest();
var url = 'http://bar.other/resources/credentialed-content/';
function callOtherDomain(){
if(invocation) {
invocation.open('GET', url, true);
invocation.withCredentials = true; // 带上cookie
invocation.onreadystatechange = handler;
invocation.send();
}
}
小王在设置请求之后又发起了一次请求。却发现cookie还是没有带上去。小王只好在MDN继续查看资料,发现在set-cookie时需要带一个sameSite的属性。
sameSite
sameSite是为了防止csrf攻击而产生的属性,如果不知道啥是CSRF攻击,可以自己先去查一下。
由于我们需要在请求中带上cookie,所以需要在set-cookie时将cookie的sameSite设置为none;又由于将sameSite设置为none时,也需要将Secure设置上,所以请求需要基于https;
小王最后一次请求小马对api进行了上诉更改,服务器终于认出请求来自谁,并返回了正确的结果,跨域的踩坑之旅算是告一段落。
总结
很多时候,我们可能只会关注请求体是什么,响应有没有正确返回,而忽略了header部分。殊不知,header在缓存,web安全,浏览器正确解析结果中发挥了重要的作用,比如本文中的一系列Access-Control-Allow-*的header。
为了让web更安全,CORS还在不断地更新,比如这个提案,规定从公网到私网,或者从私网访问local network时,需要设置跨域头,Access-Control-Allow-Private-Network。