什么是预检请求?
想象你要去一个从未去过的国家旅行。在真正踏上旅程之前,你可能会先打电话给那个国家的海关,问问你是否可以入境,需要带什么证件,是否需要签证等。这个打电话询问的过程,就像是预检请求。
在网络世界中,当你的网页想要访问另一个网站的数据(这就是所谓的跨源请求)时,浏览器有时会先发送一个”预检请求”,就是在问那个网站:”嘿,我可以用这种方式访问你的数据吗?”
预检请求是浏览器在发送实际的跨源请求之前,先向服务器发送的一个特殊的 OPTIONS 请求,用于检查实际请求是否安全可接受。
为什么会有预检请求?
- 安全考虑:就像国家要控制谁可以入境一样,网站也需要控制谁可以访问它们的数据。
- 保护隐私:预检请求不会包含实际的数据,就像你打电话询问入境要求时,不需要提供你的全部个人信息。
- 避免麻烦:如果直接发送可能不被允许的请求,就像直接飞到一个国家却被拒绝入境,这样会很麻烦。
预检请求的存在是为了增加浏览器和服务器之间交互的安全性,防止恶意的跨域请求滥用用户身份信息或对服务器资源造成威胁。这是一种保护机制,确保只有经过服务器许可的跨域请求才会被执行。
哪些情况会发起预检请求?
- 同源请求:不需要预检,无论请求多么特殊。
- 跨源请求:可能需要预检,取决于请求的具体特征。
对于同源请求,即使请求很特殊,也不会触发预检请求。这是因为同源策略(Same-Origin Policy)本身就是一种安全机制,浏览器默认信任来自同一源的请求。
比如,
- 使用特殊的”旅行方式”:如果你想骑大象入境(比喻使用 PUT, DELETE 等特殊的 HTTP 方法),而不是普通的步行(GET, POST),海关可能会要求你提前通知。
- 携带特殊物品:如果你想带一些特殊的物品入境(比如使用自定义的 HTTP 头部,例如,自定义的 X-Custom-Header 或 Authorization ),也需要提前询问。
- 特殊的行李包装:如果你的行李包装方式很特别(比如,如果 Content-Type 请求头的值不是以下三种之一,
application/x-www-form-urlencoded
,multipart/form-data
,text/plain
,比如发送 application/json 数据),也可能需要预先检查。
举个具体的例子:
假设你有一个网页游戏,需要向另一个网站的服务器发送玩家的高分数据。
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分为简单请求和非简单请求:
简单请求不会触发CORS预检请求。若该请求满足以下两个条件,就可以看作是简单请求: 1)请求方法是以下三种方法之一: ● HEAD ● GET ● POST 2)HTTP的头信息不超出以下几种字段: ● Accept ● Accept-Language ● Content-Language ● Last-Event-ID ● Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain 若不满足以上条件,就属于非简单请求了。
跨域的时候,POST会发送预检请求吗?
在跨域请求中,POST 请求是否会发送预检请求(preflight request)取决于请求的具体情况。
以下几种情况下,POST 请求会发送预检请求:
-
自定义请求头:如果你在 POST 请求中使用了非简单的自定义请求头(例如,
X-Custom-Header
),浏览器会发送一个预检请求。 -
Content-Type 为非简单类型:如果 POST 请求的
Content-Type
是application/json
、multipart/form-data
或其他复杂类型(而不是application/x-www-form-urlencoded
、text/plain
或multipart/form-data
),预检请求会被触发。 -
使用了某些 HTTP 方法:如果 POST 请求使用了不常见的方法(如 PUT、DELETE 等),也会触发预检请求。
简单跨域请求:
如果 POST 请求满足以下条件,则浏览器不会发送预检请求:
- HTTP 方法:
POST
,GET
或HEAD
。 - Content-Type:为以下三者之一:
application/x-www-form-urlencoded
multipart/form-data
text/plain
如果这些条件都满足,浏览器会直接发送实际的 POST 请求,而无需进行预检。
总结:
- 如果 POST 请求中有自定义头部,或者
Content-Type
为非简单类型,那么会触发预检请求。 - 如果满足简单跨域请求的条件,则不需要预检请求。
也就是说,跨域其实只是浏览器的行为,真实的请求其实已经到达服务器,如果没有预检请求,后端也没有进行设置的话,可能会修改数据库的数据(虽然浏览器提示被拦截),但是实际上数据已经被修改,所以加上预检请求。但是简单请求,因为只是获取数据,不会对数据库的数据有破坏,所以不需要预检请求。可以这样理解吗?
回答:有一点需要纠正 - 关于简单请求的理解不完全准确。
简单请求中的POST方法实际上也是可以修改服务器数据的。比如:
// 这是一个简单请求,但可以修改数据
fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: 'name=John&action=delete' // 这里可能会删除用户
})
你说得对的部分是:
- 跨域确实是浏览器的行为,服务器会收到请求
- 如果没有预检,复杂请求确实可能在被浏览器拦截前就修改了数据
但简单请求和复杂请求的区分主要是基于:
- 历史兼容性 - 简单请求对应了传统的表单提交行为,这些在互联网早期就存在
- 可预测性 - 简单请求的格式和行为是可预测的,服务器知道要如何处理
- 开发者意图 - 使用复杂请求通常意味着开发者在做一些需要特殊处理的事情
所以更准确的说法是:服务器需要通过合适的CORS配置来保护自己免受未授权的跨域请求,无论是简单请求还是复杂请求。预检请求机制则是为了避免不必要的复杂请求发送到服务器。
那如果是黑客发起的简单的跨域post请求,且后端未做任何安全防护的话,即便是浏览器拦截了跨域请求,数据库的数据依然可能被篡改?
是的,您的理解是正确的。即使浏览器显示跨域请求被拦截,服务器可能已经处理了请求并修改了数据库中的数据。这是因为CORS是一个浏览器端的安全机制,它只能阻止浏览器接收响应,而不能阻止服务器处理请求。因此,服务器端的安全验证机制是必不可少的。