乐趣区

关于前端:理解跨域和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 代码比较简单,并没有任何业务逻辑 / 平安相干,请勿放到正式环境中应用

退出移动版