前言

前端小伙伴在使用 XMLHttpRequest 或者 Fetch 的时候,相信对 No Access-Control-Allow-Origin header 这样的报错是最不陌生的问题了,无论是开发过程中还是在面试过程中都是一个经常遇到的一个问题,在开发过程中遇到这个问题的话一般都是找后端同学去解决,以至于很多人都忽略了对跨域的认识。为什么会导致跨域?遇到跨域又怎么去解决呢?本文会对这些问题一一的介绍。

什么时候会跨域?

早期为了防止CSRF(跨域请求伪造)的攻击,浏览器引入了同源策略(Same origin policy)来提高安全性。

CSRF(Cross-site request forgery),跨站请求伪造,也被称为:one click attack/session riding,缩写为:CSRF/XSRF。 —— 跨站请求伪造

MDN中,对跨域是这么解释的:

跨源资源共享(CORS,或通俗地译为跨域资源共享)是一种基于 HTTP 头的机制,该机制通过允许服务器标示除了它自己以外的其他源(域、协议或端口),使得浏览器允许这些源访问加载自己的资源。跨源资源共享还通过一种机制来检查服务器是否会允许要发送的真实请求,该机制通过浏览器发起一个到服务器托管的跨源资源的“预检”请求。在预检中,浏览器发送的头中标示有 HTTP 方法和真实请求中会用到的头。

而所谓同源策略,即同协议(protocol)、同域名(domain或ip)、同端口(port)的才能互相获取资源,而不能访问其他域的资源。在同源策略影响下,一个域名A的网页可以获取域名B下的脚本,css,图片等,但是不能发送Ajax请求,也不能操作Cookie、LocalStorage等数据。同源策略的存在,一方面提高了网站的安全性,但同时在面对前后端分离、模拟测试等场景时,也带来了一些麻烦,从而不得不寻求一些方法来突破限制,获取资源。

OPTIONS请求

我们在进行POST或其他跨域请求时,会发现只有一个OPTIONS请求,它的名称叫CORS请求预检,首先来看一下官方对它的定义是:

HTTP的OPTIONS方法用于获取目的资源所支持的通信选项。客户端可以对特定的URL使用OPTIONS方法,也可以对整站(通过将 URL 设置为“*”)使用该方法。

选项 是否允许 备注
Request has body No 没有请求体
Successful response has body No 成功的响应有响应体
Safe Yes 安全
Idempotent Yes 密等性,不变性,同一个接口请求多少次都一样
Cacheable No 不能缓存
Allowed in HTML forms No 不能在表单里使用

根据官网的文档,我们发现它没有请求体,不能设置data,也不能直接发起OPTIONS请求。简言之,OPTIONS请求是用于请求服务器对于某些接口等资源的支持情况的,包括各种请求方法、头部的支持情况,仅作查询使用。

  让我们详细地看一下OPTIONS请求的真实面目吧,我们首先构造一个POST请求:

1
2
3
4
5
6
fetch('http://192.168.0.100:8081', {
method: 'POST'
})
.then(data => console.log(data))
.catch(error => console.error(error))

可以看到OPTIONS请求头(Request Headers)很简单,都没有请求的body,有两个字段Access-Control-Request-HeadersAccess-Control-Request-Method是新出现的,下面会说到这两个字段的用法;那么什么时候会触发OPTIONS请求呢,这里涉及到两种CORS请求。

浏览器将CORS请求分成两类:简单请求(simple request)和 非简单请求(not-so-simple request),简单请求不会触发CORS预检请求。

  • 简单请求
    只要同时满足以下两大条件,就属于简单请求:

    1. 请求方法是以下三种方法之一
      HEAD、GET、POST
    2. HTTP的头信息不超出以下几种字段
      Accept、
      Accept-Language
      Content-Language
      Content-Type 只限于三个值 application/x-www-form-urlencoded、multipart/form-data、text/plain
      DPR
      Downlink
      Save-Data
      Viewport-Width

    因此我们只要把上面的请求加一个请求头Content-Type,就能不触发OPTIONS请求。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    fetch('http://192.168.0.100:8081', {
    method: 'POST',
    headers: {
    'Content-Type': 'application/x-www-form-urlencoded', // +++
    },
    })
    .then(data => console.log(data))
    .catch(error => console.error(error))

  • 非简单请求
    下面,我们的重点来了,我们在进行ajax请求时,一般都会在请求头加一下自定义的数据,因此大多数请求都是非简单请求。非简单请求涉及以下几个请求和响应的头部的字段:

    字段名 位置 用法 备注
    Access-Control-Request-Method 请求头 method 将实际请求所使用的 HTTP 方法告诉服务器
    Access-Control-Request-Headers 请求头 field-name[, field-name]* 将实际请求所携带的头部字段告诉服务器
    Access-Control-Allow-Origin 响应头 origin or* 对于不需要携带身份凭证的请求,服务器可以指定该字段的值为通配符,表示允许来自所有域的请求
    Access-Control-Allow-Methods 响应头 method[, method]* 指明了实际请求所允许使用的 HTTP 方法
    Access-Control-Allow-Headers 响应头 field-name[, field-name]* 指明了实际请求中允许携带的头部字段。
    Access-Control-Allow-Credentials 响应头 true 指定了当浏览器的credentials设置为true时是否允许浏览器读取response的内容
    Access-Control-Max-Age 响应头 delta-second 指定了请求的结果能够被缓存多久

    在上面的OPTIONS请求中我们可以发现表格中的三个请求头部都在该次请求中出现了,Access-Control-Request-Method和Access-Control-Request-Headers用来询问服务器.