CORS 中的预检请求
在 CORS 机制中,客户端将请求分为了两种:简单请求和非简单请求;当请求为非简单请求时,就会触发浏览器发送预检请求,这是浏览器的行为。
预检请求会向服务器确认跨域是否允许,服务返回的响应头里有对应字段Access-Control-Allow-Origin
来给浏览器判断:如果允许,浏览器紧接着发送实际请求;不允许,报错并禁止客户端脚本读取响应相关的任何东西。
所以,一个 POST 请求并且请求头添加了Content-Type: application/json
,浏览器判定为非简单请求,自己先发一个 OPTIONS 给服务器获取做跨域判定,获取响应后浏览器发现可以跨域,接着就发送真实的 POST。
这里就解答两个问题了,接下来就是为什么预检请求选择了 OPTIONS 呢?
来看预检请求的流程,如果是一个跨域请求,浏览器会自动给该请求带上 Origin
头部,标明当前请求的来源域;服务器判断这个请求是否允许跨域,就会在返回时,选择是否带上 Access-Control-Allow-Origin
头部,最后,浏览器判断 Access-Control-Allow-Origin 就知道,后续请求是否发送。
这个流程中,对预检请求方法的要求:
- 不需要带请求体,服务器判断的依据在 Request header 中;
- 服务器返回不需要响应体,浏览器判断的依据在 Response header 中;
- 请求不会去修改服务器资源,要是一个安全的请求;
- 浏览器默认不会缓存,需要每次发送跨域验证;
再看看 OPTIONS 的定义,会有一种量身定做的感觉。
但是,这里还是有问题:
- 既然服务端做了请求限制,而且浏览器判断跨域只和
Access-Control-Allow-Origin
有关,预检请求是否有点多余? - 原生 form 表单可以提交 POST 请求,而且为一个简单请求,很可能修改服务端数据,仅仅依靠 CORS 机制也不安全。
预检请求的意义
浏览器为了安全的数据传输,提出了 CORS 机制,它更像一种授权机制,需要浏览器和服务器共同配合实现,对于没有实现此机制的客户端,比如 curl,是不受限制的:
上图中,服务器实现很简单,仅仅返回预先定义好的数据,curl 的返回头中也没有和Access-Control-*
相关字段,但是返回体中,我们能够看到返回数据,想必解析出来也并不困难。同样的请求在浏览器中就会报错了:
从这里可以看出,在 CORS 机制中,默认服务器为禁止跨域,服务器啥也不做就能禁止浏览器跨域了;但是,实际中能发请求的客户端很多,每个请求的目的很复杂,对于那些真正要禁止跨域传输的服务自然有一套处理逻辑,这些逻辑很可能是复杂和高消耗的。
如果类似浏览器这种,包含 CORS 机制的客户端发送的请求,每次都要经过一个复杂逻辑才能知道自己是否跨域,服务器的压力和用户体验是不理想的,那么预检请求就孕育而生:发送实际请求前,先发送预检请求询问服务器是否允许跨域,不允许就不发送实际请求,服务器只需要对预检请求进行跨域处理。
这样来看,在 CORS 机制中,发送预检请求是一种保护机制,保护资源不被未授权的请求修改。和授权服务很像,预检请求通过了,浏览器后续对同一服务的请求,不需要做跨域询问,服务端不想支持跨域访问,啥也不用做。
表单请求
原生 form 表单请求,和 action,method,enctype 属性有关(回顾这三个重要属性)。form 表单提交后,会自动跳转页面到 action 所指向的 URL 来获取结果,最后变成同域,在没有 AJAX 技术的时候,我们发 POST 一般会提交到当前 URL,后端响应 POST 请求,处理之后,又将当前页面返回浏览器重新渲染,这也是每次提交表单会刷新页面的原因。
而 method 和 enctype 可以的取值,也出现在了简单请求的定义里面,所以提交传统表单请求,是不会引发预检请求,如果服务端对操作数据更改的接口,不做请求来源限制,即便是不允许跨域,变化依然会在服务端发生。
最后,HTTP 服务是无状态的哦!
无状态服务器,是指一种把每个请求,作为与之前任何请求都无关的,独立的服务器。
即便通过了预检请求的检查,浏览器的后续请求,服务端只会响应当前请求;所以,可能出现预检请求没问题,后续的实际请求出现跨域:
所以,对于无状态服务,真想限制跨域资源传输,仅仅依赖 CORS 机制是做不到的, 除了浏览器这种客户端遵守了并实现了 CORS,世界上这么多客户端是不遵守的:curl(可以回顾之前的示例图)、wget、server proxy,还有各种爬虫脚本,postman 这种 API 测试工具,可能都没实现,都能正常发送请求和获取数据。
想一想,这个请求可能是 CRUD 中的任何一种,成功执行服务端逻辑并返回 2xx,这时,CORS 验证机制像是马后炮。假如服务器不想支持跨域访问,还是得在响应处理的时候,去实现过滤机制防止资源被非法获取或修改。