跨域的时候,post为什么会发送两次请求?

Daotin 2024-08-20 编辑

什么是预检请求?

想象你要去一个从未去过的国家旅行。在真正踏上旅程之前,你可能会先打电话给那个国家的海关,问问你是否可以入境,需要带什么证件,是否需要签证等。这个打电话询问的过程,就像是预检请求。

在网络世界中,当你的网页想要访问另一个网站的数据(这就是所谓的跨源请求)时,浏览器有时会先发送一个”预检请求”,就是在问那个网站:”嘿,我可以用这种方式访问你的数据吗?”

预检请求是浏览器在发送实际的跨源请求之前,先向服务器发送的一个特殊的 OPTIONS 请求,用于检查实际请求是否安全可接受。

为什么会有预检请求?

  • 安全考虑:就像国家要控制谁可以入境一样,网站也需要控制谁可以访问它们的数据。
  • 保护隐私:预检请求不会包含实际的数据,就像你打电话询问入境要求时,不需要提供你的全部个人信息。
  • 避免麻烦:如果直接发送可能不被允许的请求,就像直接飞到一个国家却被拒绝入境,这样会很麻烦。

预检请求的存在是为了增加浏览器和服务器之间交互的安全性,防止恶意的跨域请求滥用用户身份信息或对服务器资源造成威胁。这是一种保护机制,确保只有经过服务器许可的跨域请求才会被执行。

哪些情况会发起预检请求?

  • 同源请求:不需要预检,无论请求多么特殊。
  • 跨源请求:可能需要预检,取决于请求的具体特征。

对于同源请求,即使请求很特殊,也不会触发预检请求。这是因为同源策略(Same-Origin Policy)本身就是一种安全机制,浏览器默认信任来自同一源的请求。

比如,

  • 使用特殊的”旅行方式”:如果你想骑大象入境(比喻使用 PUT, DELETE 等特殊的 HTTP 方法),而不是普通的步行(GET, POST),海关可能会要求你提前通知。
  • 携带特殊物品:如果你想带一些特殊的物品入境(比如使用自定义的 HTTP 头部,例如,自定义的 X-Custom-Header 或 Authorization ),也需要提前询问。
  • 特殊的行李包装:如果你的行李包装方式很特别(比如,如果 Content-Type 请求头的值不是以下三种之一,application/x-www-form-urlencodedmultipart/form-datatext/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 请求会发送预检请求:

  1. 自定义请求头:如果你在 POST 请求中使用了非简单的自定义请求头(例如,X-Custom-Header),浏览器会发送一个预检请求。

  2. Content-Type 为非简单类型:如果 POST 请求的 Content-Typeapplication/jsonmultipart/form-data 或其他复杂类型(而不是 application/x-www-form-urlencodedtext/plainmultipart/form-data),预检请求会被触发。

  3. 使用了某些 HTTP 方法:如果 POST 请求使用了不常见的方法(如 PUT、DELETE 等),也会触发预检请求。

简单跨域请求:

如果 POST 请求满足以下条件,则浏览器不会发送预检请求:

  • HTTP 方法POSTGETHEAD
  • 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'  // 这里可能会删除用户
})

你说得对的部分是:

  1. 跨域确实是浏览器的行为,服务器会收到请求
  2. 如果没有预检,复杂请求确实可能在被浏览器拦截前就修改了数据

但简单请求和复杂请求的区分主要是基于:

  1. 历史兼容性 - 简单请求对应了传统的表单提交行为,这些在互联网早期就存在
  2. 可预测性 - 简单请求的格式和行为是可预测的,服务器知道要如何处理
  3. 开发者意图 - 使用复杂请求通常意味着开发者在做一些需要特殊处理的事情

所以更准确的说法是:服务器需要通过合适的CORS配置来保护自己免受未授权的跨域请求,无论是简单请求还是复杂请求。预检请求机制则是为了避免不必要的复杂请求发送到服务器。

那如果是黑客发起的简单的跨域post请求,且后端未做任何安全防护的话,即便是浏览器拦截了跨域请求,数据库的数据依然可能被篡改?

是的,您的理解是正确的。即使浏览器显示跨域请求被拦截,服务器可能已经处理了请求并修改了数据库中的数据。这是因为CORS是一个浏览器端的安全机制,它只能阻止浏览器接收响应,而不能阻止服务器处理请求。因此,服务器端的安全验证机制是必不可少的。