什么是预检请求?
浏览器在发送“非简单跨域请求”之前,先用一个 OPTIONS 方法,发送一个 空壳请求 到目标服务器,问:“我可以发这个请求吗?”
目的是为了在“请求尚未发出”前,就防止数据泄露或危险操作。
哪些情况会发起预检请求?
只要请求不是“简单请求”,就会触发预检。
- 同源请求:不需要预检,无论请求多么特殊。
- 跨源请求:可能需要预检,取决于请求的具体特征。
对于同源请求,即使请求很特殊,也不会触发预检请求。这是因为同源策略(Same-Origin Policy)本身就是一种安全机制,浏览器默认信任来自同一源的请求。
举个具体的例子:
假设你有一个网页游戏,需要向另一个网站的服务器发送玩家的高分数据。
fetch('https://other-website.com/api/scores', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Player-Id': '12345',
},
body: JSON.stringify({ score: 1000 }),
});
这个请求会触发预检,因为:
- 它使用了自定义头部(X-Player-Id)
- 它发送的是 JSON 数据
浏览器会先发送一个 OPTIONS 请求(预检请求),询问服务器是否允许这样的请求。如果服务器答应了,真正的 POST 请求才会被发送。
那么,哪些请求会发送预检请求?
浏览器将CORS分为简单请求和非简单请求:
简单请求:
- 请求方法 GET, POST, HEAD
- 请求头 仅限 Accept, Content-Type(值必须是 application/x-www-form-urlencoded、multipart/form-data、text/plain),不能有自定义头
- 无 XMLHttpRequest.withCredentials = true
✅ 满足以上就是简单请求,不预检。
预检请求的问题
- OPTIONS请求次数过多就会损耗页面加载的性能,降低用户体验度。所以尽量要减少OPTIONS请求次数,可以后端在请求的返回头部添加:Access-Control-Max-Age:number。它表示预检请求的返回结果可以被缓存多久,单位是秒。该字段只对完全一样的URL的缓存设置生效,所以设置了缓存时间,在这个时间范围内,再次发送请求就不需要进行预检请求了。
- 浏览器的预检机制并不全面,不会拦住所有危险请求,特别是“简单请求”的 POST / 伪装成简单表单的提交,可以骗过预检,直接操作数据库,造成危险。所以,浏览器的预检机制并不防住所有 POST 请求,尤其是“简单 POST”仍然可以被攻击者利用;真正安全的保障必须依靠后端验证身份、校验来源。
网站开发者需要在服务端做保护,比如:
- 使用 CSRF Token:每个表单都带一个随机值,后台验证;
- SameSite Cookie 属性:设置 cookie 不允许第三方请求携带
- Referer / Origin 验证:服务器检查请求来源是否合法。