理解跨域和CORS

Posted fengxh

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了理解跨域和CORS相关的知识,希望对你有一定的参考价值。

简言

跨域是前端开发者不可避免的概念之一,也是面试中会经常出现的话题。在平常的开发工作中我也会遇到跨域的问题,如果你不能很好的理解并掌握它,会极大的影响你的工作效率,也会耗费你大量的时间来翻阅资料,一步步解决对应的问题点。同时,这篇文章也是我在多次处理同样问题后,意识到需要总结一下并且留下文档,以便后面再遇到问题可以快速解决。如果你也遇到跨域的问题,那么就请继续往下阅读,我会带你阅读整篇文章,来一步步地了解跨域并且掌握处理跨域问题的方法。好了话不多说,让我们开始吧。
本篇文章相关代码

理解跨域

在开发项目的时候,我们肯定会需要访问后台服务的资源。如果用XMLHttpRequest/axios/fetch直接访问后台资源(假设后台服务没有配置cors的情况下),浏览器会出于安全原因拦截掉该响应,那么此时我们的接口访问就失败了,对应的数据也拿不到,浏览器控制台会打印出类似跨域的错误(Access to XMLHttpRequest at \'http://localhost:3000/cors\' from origin \'http://localhost:3001\' has been blocked by CORS policy)。

  1. 那什么是跨域呢?简单来讲,如果两个服务的协议不同(http/https),或者域名不同(www.a.com/www.b.com),或者端口不同(www.a.com:80/www.a.com:81),此时想要访问另一个服务,就会发生跨域。
  2. 那为什么要产生跨域?跨域本身是浏览器实施的,基于安全原因,浏览器会限制来自脚本初始化的跨域http请求。比如浏览器中常见的XMLHttpRequest和fetch,他们都遵从同源原则。

解决跨域

大概讲述完跨域,可能你会想,既然浏览器会拦截跨域的响应,那么岂不是我们都拿不到跨域服务的数据?但是现实中很多项目都是跨域,好像都正常运行的,这是怎么回事?那么这里就需要了解跨域资源共享技术了。

跨域资源共享,是一种基于http头信息的机制。它允许服务器指示,浏览器应该允许加载该服务器的资源。即通过该机制,服务器告诉浏览器,你应该加载我发送的响应,而不是拦截掉,即使我们现在是跨域的。

了解了跨域资源共享,我们就可以针对性的处理各种涉及跨域的问题。以下我列举了几种跨域并且通过代码来复现跨域并且处理该跨域问题。

简单请求

简单请求,即不会出发预检的请求。它需要满足下面的条件:

  1. http方法为GET/HEAD/POST之一
  2. 除了被用户代理自动设定的头信息(比如ConnectionUser-Agent等),仅可再包含AcceptAccept-LanguageContent-LanguageContent-Type。请注意下面Content-Type额外的限制
  3. Content-Type只允许三种值:Application/x-www-form-urlencodedmultipart/form-datatext/plain

    // // backend/index.html
    const xhr = new XMLHttpRequest()
    xhr.open("get", "http://localhost:3000/cors", true)
    xhr.onreadystatechange = function() {
      if (xhr.readyState === XMLHttpRequest.DONE) {
     const status = xhr.status;
     if (status === 0 ||  (status >= 200 && status < 400)) {
       console.log(xhr.responseText)
       return
     }
    
     // an error 
      }
    }
    xhr.send()
    // backend/app.js
    if (req.url === "/cors") {
      res.end("hello cors");
      return;
    }

    那么此时,我们就会遇到第一个跨域问题。控制台告诉我们(Access to XMLHttpRequest at \'http://localhost:3000/cors\' from origin \'http://localhost:3001\' has been blocked by CORS policy: No \'Access-Control-Allow-Origin\' header is present on the requested resource.)所以我们在后台服务增加响应的头信息Access-Control-Allow-Origin来解决。

    // backend/app.js
    res.setHeader("Access-Control-Allow-Origin", "*")
    if (req.url === "/cors") {
      res.end("hello cors");
      return;
    }

    问题迎刃而解,这是一个解决简单请求的跨域问题。

预检请求

我们把上述的前端请求的头信息改动,不符合简单请求的范围,比如http方法为put/delete,或者Content-Type的值为application/json等等,我们发现浏览器又报错了(Access to XMLHttpRequest at \'http://localhost:3000/cors\' from origin \'http://localhost:3001\' has been blocked by CORS policy: Request header field content-type is not allowed by Access-Control-Allow-Headers in preflight response.)

// frontend/index.html
const xhr = new XMLHttpRequest()
xhr.open("get", "http://localhost:3000/cors", true)
xhr.setRequestHeader("Content-Type", "application/json") // ++
xhr.onreadystatechange = function() {
  if (xhr.readyState === XMLHttpRequest.DONE) {
    const status = xhr.status;
    if (status === 0 ||  (status >= 200 && status < 400)) {
      console.log(xhr.responseText)
      return
    }

    // an error 
  }
}
xhr.send()

那什么是预检请求?不像我们上述的简单请求,对预检请求来说,浏览器首先需要用OPTIONS方法来发送一个http请求到另一个域,来决定真实请求是否可以安全发送。上述代码就造成了预检未通过,所以浏览器在OPTIONS请求后,发出了真实的请求,但是拦截掉了响应,所以发出了上述的错误。那么我们要怎么解决呢?

首先,我们发现OPTIONS请求有两个之前没有的头信息

  • Access-Control-Request-Headers: content-type
  • Access-Control-Request-Method: GET

那么我们可以在后台服务对应设置响应头来处理该跨域问题

// backend/app.js
res.setHeader("Access-Control-Allow-Origin", "*")
res.setHeader("Access-Control-Allow-Methods", "GET")
res.setHeader("Access-Control-Allow-Headers", "Content-Type")

if (req.url === "/cors") {
  res.end("hello cors");
  return;
}

此时我们再看控制台,报错消息了,请求也成功了。

有授权的请求

很多时候我们的后台服务是有权限相关的,比如不同用户/租户等等,对相同的后台接口请求会返回不同的数据,那么我们会用到HTTP身份验证信息。在跨域XMLHttpRequest/feth请求中,浏览器默认不会发送身份验证信息。我们可以设定该标志

// frontend/index.html
const xhr = new XMLHttpRequest()
xhr.open("get", "http://localhost:3000/cors", true)
xhr.withCredentials = true // ++
xhr.setRequestHeader("Content-Type", "application/json")
xhr.onreadystatechange = function() {
  if (xhr.readyState === XMLHttpRequest.DONE) {
    const status = xhr.status;
    if (status === 0 ||  (status >= 200 && status < 400)) {
      console.log(xhr.responseText)
      return
    }

    // an error 
  }
}
xhr.send()

我们会发现,此时浏览器又报错了(Access to XMLHttpRequest at \'http://localhost:3000/cors\' from origin \'http://localhost:3001\' has been blocked by CORS policy: Response to preflight request doesn\'t pass access control check: The value of the \'Access-Control-Allow-Origin\' header in the response must not be the wildcard \'*\' when the request\'s credentials mode is \'include\'. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.)
那么此时我们把错误中提出的Access-Control-Allow-Origin,值改为我们对应的前端服务地址试下

// backend/app.js
res.setHeader("Access-Control-Allow-Origin", "http://localhost:3001") // changed
res.setHeader("Access-Control-Allow-Methods", "GET")
res.setHeader("Access-Control-Allow-Headers", "Content-Type")

此时又报错了(Access to XMLHttpRequest at \'http://localhost:3000/cors\' from origin \'http://localhost:3001\' has been blocked by CORS policy: Response to preflight request doesn\'t pass access control check: The value of the \'Access-Control-Allow-Credentials\' header in the response is \'\' which must be \'true\' when the request\'s credentials mode is \'include\'. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.)
那我们接着改Access-Control-Allow-Credentials的值来看下

// backend/app.js
res.setHeader("Access-Control-Allow-Origin", "http://localhost:3001")
res.setHeader("Access-Control-Allow-Credentials", "true") // ++
res.setHeader("Access-Control-Allow-Methods", "GET")
res.setHeader("Access-Control-Allow-Headers", "Content-Type")

大功告成,我们又如愿获取到了跨域服务的响应。

总结

读完文章,我们了解了很多跨域的知识,并且掌握了常见的跨域错误的解决方法。本文是基于MDN-cors,通过简单代码来复现跨域错误并且逐步解决问题。如果有什么意见或者发现本文的错误,请联系TWITTER、<shangfxh@gmail.com>、QQ(1010454733)。
注意:本文的nodejs代码比较简单,并没有任何业务逻辑/安全相关,请勿放到正式环境中使用

以上是关于理解跨域和CORS的主要内容,如果未能解决你的问题,请参考以下文章

JSONP跨域和CORS跨域的区别

21.跨域和CORS

rest_framework 跨域和CORS

百万年薪python之路 -- 请求跨域和CORS协议详解

SpringBoot整合Shiro 涉及跨域和@Cacheable缓存/@Transactional事务注解失效问题

关于CORS跨域问题的理解