彻底读懂前端跨域CORS

7,035 阅读7分钟

  前端小伙伴在使用AJAX的时候,相信对No Access-Control-Allow-Origin header这样的报错提示感到很头疼,怎么请求又跨域了。文章的开始,让我们从一个小故事开始。。。

一个小故事

  在开发中,前端的童鞋们每次看到浏览器下面出现一长串红色的跨域报错就会很恼火,不停的念叨着:那个谁谁谁,又没有给我加跨域头;后端小伙伴又会毫不示弱地反击道:不就是Access-Control-Allow-Origin: *么?已经有了啊!那为什么还会报错?肯定是你没加好!

  于是,一场甩锅大战即将开始...

shuaiguo

谁应该了解跨域

  说实话,每一个前后端开发都应该要了解跨域的用法。

  前端的小伙伴可能会觉得跨域问题应该都是后端接口来处理的,但是如果多了解一些HTTP请求响应头的,能够更快的定位问题,更快的解决接口异常,方便排查调试,所以希望能够耐下心把这篇文章看完。

什么时候会跨域

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

跨域资源共享(CORS) 是一种机制,它使用额外的 HTTP 头来告诉浏览器 让运行在一个 origin (domain) 上的Web应用被准许访问来自不同源服务器上的指定的资源。当一个资源从与该资源本身所在的服务器不同的域、协议或端口请求一个资源时,资源会发起一个跨域 HTTP 请求

  简单来说就是当你向不同“域”的服务器发起网络请求的时候,这个请求就跨域了。这里不同“域”指的是不同的协议、域名、端口,有任何一个不同时,浏览器都视为跨域。我们在使用postman、fiddler等一些工具模拟发起http请求的时候,不会遇到跨域的情况;当我们在浏览器中请求不同域名的时候,虽然请求正常发出了,但是浏览器在请求返回时会进行一系列的校验,判断此次请求是否“合法”;如果不合法,返回结果就被浏览器拦截了。

  我们在进行POST或其他跨域请求时,会发现只有一个OPTIONS请求,并没有我们想要的请求方法。

error.png

神秘的OPTIONS请求

  我们没有发送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请求:

var instance = axios.create({
    baseURL: 'http://192.168.0.100:8081'
})

instance({
    url: '/post',
    method: 'post',
    data:{
        url: 'xieyufei.com'
    }
})

options.png

  可以看到OPTIONS请求头很简单,都没有请求的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
    • Viewport-Width

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

    instance({
        url: '/post',
        method: 'post',
        headers:{
            'Content-Type': 'application/x-www-form-urlencoded'
        },
        data:{
            url: 'xieyufei.com'
        }
    })

非简单请求

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

字段名 位置 用法 备注
Origin 请求头 origin 表明预检请求或实际请求的源站
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-seconds 指定了请求的结果能够被缓存多久

  在上面的OPTIONS请求中我们可以发现表格中的三个请求头部都在该次请求中出现了,Access-Control-Request-MethodAccess-Control-Request-Headers用来询问服务器,下面我要用POST方法和Content-Type头部来请求,你就说你答不答应吧?

request.jpg

  在服务器端,我们可以这么写来允许请求跨域:

const express = require('express')
const app = express()
const PORT = 8081

let allowCrossDomain = function (req, res, next) {
    res.header('Access-Control-Allow-Origin', '*')
    res.header('Access-Control-Allow-Methods', 'POST')
    res.header('Access-Control-Allow-Headers', 'content-type')
    next()
}

app.use(allowCrossDomain)

app.post('/post', (req, res) => {
    res.json({
        msg: 'hi post'
    })
})

app.listen(PORT)

  这里有我们后端小伙伴很熟悉的Access-Control-Allow-Origin: *,用来表明所有的origin都允许跨域,相当于告诉浏览器:

allow.jpg

  这样我们就能看到我们期待已久的POST请求,同时返回的头部信息中带上了CORS的响应头;同时我们可以看到axios默认的Content-Typeapplication/json;charset=UTF-8,不在仅限的三个值中,因此会触发OPTIONS请求。

post.png

其他头部信息

  除了content-type,我们还可以在请求头中添加一些自己定义的信息,比如需要传给后台的token之类的。

//浏览器端
instance({
    url: '/put',
    method: 'put',
    headers:{
        'X-Custom-Header': 'xieyufei-head'
    },
    data:{
        url: 'xieyufei.com'
    }
})

//服务器端
res.header('Access-Control-Allow-Origin', '*')
res.header('Access-Control-Allow-Methods', 'POST,PUT')
res.header('Access-Control-Allow-Headers', 'content-type, X-Custom-Header')

跨域获取Cookie

  默认情况下,Cookie是不包括在CORS的请求中,但有时候我们又需要用到Cookie来传输数据,这时候我们的Access-Control-Allow-Credentials字段就派上用处了,另一方面,需要在AJAX请求中打开withCredentials属性;我们再把代码进行如下改造:

//服务器端
res.header('Access-Control-Allow-Credentials', 'true')

//浏览器端
instance({
    url: '/put',
    method: 'put',
    //新增withCredentials
    withCredentials: true,
    headers:{
        'X-Custom-Header': 'xieyufei-head'
    },
    data:{
        url: 'xieyufei.com'
    }
})

  当我们满怀期待打开浏览器准备接收Cookie时,却发现又报错了:

credential-error.png

  经过对错误信息仔细阅读,发现这次报错跟上面的跨域报错不完全一样,大概的意思是当请求的身份凭证包括的时候,Access-Control-Allow-Origin不能是通配符'*'(wildcard)。因此我们大概了解到了错误的原因是在通配符上面,我们对代码再进行一下改造:

const cookieParser = require('cookie-parser');  
app.use(cookieParser())

res.header('Access-Control-Allow-Origin', '*')
res.header('Access-Control-Allow-Methods', 'POST')
res.header('Access-Control-Allow-Headers', 'content-type, X-Custom-Header')
res.header('Access-Control-Allow-Credentials', 'true')

app.post('/post', (req, res) => {
    console.log(req.cookies, 'cookie')
    res.json({
       msg: 'hi post'
    })
})

  这时候就能看到我们想要的Cookie了。

总结

  CORS内容其实来说不是很多,也比较简单,但是考验动手实践能力,面试时一般也会问到,因此通过express搭建服务器来加深对CORS知识的了解。

  更多文章请关注我的公众号:前端壹读。

wechat_qrcode.jpg