CORS(cross-origin resource sharing),跨源资源共享(一般俗称『跨域请求』),想必大家都已经有基本的了解。如果你还不了解的话,可以阅读MDN 上的介绍 (https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) ,这里就不赘述了。
不过在学习CORS时,有些朋友会有疑惑,为什么CORS要把请求分成两类:简单请求和预检请求(preflighted requests)呢?
如果我们看简单请求和预检请求的区分,会看到有很多的条件:
简单请求的 HTTP 方法只能是 GET、HEAD 或 POST
简单请求的 HTTP 头只能是 Accept/Accept-Language/Conent-Language/Content-Type 等
简单请求的 Content-Type 头只能是 text/plain、multipart/form-data 或 application/x-www-form-urlencoded
看上去很是复杂。
那么怎么理解这些限制呢?
其实,简单请求就是普通 HTML Form 在不依赖脚本的情况下可以发出的请求,比如表单的 method 如果指定为 POST ,可以用 enctype 属性指定用什么方式对表单内容进行编码,合法的值就是前述这三种。
非简单请求就是普通 HTML Form 无法实现的请求。比如 PUT 方法、需要其他的内容编码方式、自定义头之类的。
对于服务器来说,第一,许多服务器压根没打算给跨源用。当然你不给 CORS 响应头,浏览器也不会使用响应结果,但是请求本身可能已经造成了后果。所以最好是默认禁止跨源请求。
第二,要回答某个请求是否接受跨源,可能涉及额外的计算逻辑。这个逻辑可能很简单,比如一律放行。也可能比较复杂,结果可能取决于哪个资源哪种操作来自哪个 origin。对浏览器来说,就是某个资源是否允许跨源这么简单;对服务器来说,计算成本却可大可小。所以我们希望最好不用每次请求都让服务器劳神计算。
CORS-preflight 就是这样一种机制,浏览器先单独请求一次,询问服务器某个资源是否可以跨源,如果不允许的话就不发实际的请求。注意先许可再请求等于默认禁止了跨源请求。如果允许的话,浏览器会记住,然后发实际请求,且之后每次就都直接请求而不用再询问服务器否可以跨源了。于是,服务器想支持跨源,就只要针对 preflight 进行跨源许可计算。本身真正的响应代码则完全不管这个事情。并且因为 preflight 是许可式的,也就是说如果服务器不打算接受跨源,什么事情都不用做。
但是这机制只能限于非简单请求。在处理简单请求的时候,如果服务器不打算接受跨源请求,不能依赖 CORS-preflight 机制。因为不通过 CORS,普通表单也能发起简单请求,所以默认禁止跨源是做不到的。
既然如此,简单请求发 preflight 就没有意义了,就算发了服务器也省不了后续每次的计算,反而在一开始多了一次 preflight。
有些人把简单请求不需要 preflight 理解为『向下兼容』。这也不能说错。但严格来说,并不是『为了向下兼容』而不能发。理论上浏览器可以区别对待表单请求和非表单请求 —— 对传统的跨源表单提交不发 preflight,从而保持兼容,只对非表单跨源请求发 preflight。
但这样做并没有什么好处,反而把事情搞复杂了。比如本来你可以直接用脚本发跨源普通请求,尽管(在服务器默认没有跨源处理的情况下)你无法得到响应结果,但是你的需求可能只是发送无需返回,比如打个日志。但现在如果服务器不理解 preflight 你就干不了这个事情了。
而且如果真的这样做,服务器就变成了默认允许跨源表单,如果想控制跨源,还是得(跟原本一样)直接在响应处理中执行跨源计算逻辑;另一方面服务器又需要增加对 preflight 请求的响应支持,执行类似的跨源计算逻辑以控制来自非表单的相同跨源请求。服务器通常没有区分表单/非表单差异的需求,这样搞纯粹是折腾服务器端工程师。
所以简单请求不发 preflight 不是因为不能兼容,而是因为兼容的前提下发 preflight 对绝大多数服务器应用来说没有意义,反而把问题搞复杂了。
- 补充1
贺师俊 (作者) :
绝对意义上的后端安全是另一个层面的事情,不要混在一起理解。简单请求不存在『绕过』的问题。从有网站开始,简单请求就一直可以跨源提交。所以服务器如果要禁止简单请求的跨源提交,从来就是要自己处理的。而节省跨源计算只能针对新的需求,也就是原本浏览器不可能发送的跨源非简单请求。你确实可以把简单请求不需要preflight理解为『为了向下兼容』。但严格来说,不是。理论上浏览器可以对传统的跨源表单提交不发preflight,从而保持兼容,只对脚本发起的跨源表单提交发preflight。这样服务器这里默认允许跨源表单提交,但通过响应preflight来控制脚本的相同跨源请求。但是服务器通常没有区分这种微秒差异的需求。所以不发preflight不是因为不能兼容,而是因为兼容的前提下发preflight对绝大多数服务器应用来说没有意义。
*补充2
Ivony:
如果我们现在重新设计整个HTTP协议,我们可以要求浏览器在发送任何数据到另外一个域的服务器之前,都必须先发送preflight request。但是大部分现存网站并未针对preflight request做出实现,所以这意味着现有的互联网中,如果一个域的表单向另一个域提交的时候会跨域失败,直到目标网站更新处理perflight request为止。所以在我们制定这一新的标准的时候,应当考虑到目前互联网已经存在这样的请求,他们虽然看起来可能不安全,但为了向下兼容,我们不能强制对这些请求做preflight request。既然不能强制做preflight request验证,那发这个东西就没有什么意义了。当然,我认为在时机成熟的时候,我们可以引入一种强制CORS的机制,就像现在的强制HTTPS机制一样。我们可以约定浏览器预先发一个请求到目标域名确定目标域的服务器是否支持强制CORS。如果目标域支持强制CORS,则浏览器对引用目标域的任何资源请求都发出Origin头,任何数据的发送都先发送preflight request。至于你的迷惑,简单来说:CORS是允许受限的跨域访问,不是限制现有的跨域访问。没有CORS之前我们不是不可以跨域访问,而是要很弯弯绕(譬如说JSONP和万能的服务器代发),而CORS则是提出一个方案可以让我们直接了当的描述跨域访问的需求并且加以控制。
*补充3
失礼:
简单请求的情况不需求发起preflight,也就不用先发起options请求然后再发起真实的请求,也就是说如果刚好允许跨域或者压根没跨域,它消耗资源少。简单请求的那些情况就是我们常用的操作,如果也需要preflight,就浪费资源了。因为对我们自己而言的大量非跨域请求就不公平了。而对于非简单请求而言,浏览器实行跨域预检机制可以节约资源,也做了一道防线。因为如果后端不允许跨域,就不需要发送正式的请求啦。退一步讲,正常后端都是会对跨域请求做过滤限制的。不管你是简单还是复杂请求。