彻底搞定浏览器跨域问题

彻底搞定浏览器跨域问题

本文主要讲解关于浏览器跨域请求现象原因并详细介绍一些目前的最佳解决方案

目录

认识跨域请求

CORS跨域解决方案

1.简单跨域请求

2.复杂跨域请求

3.跨域请求携带Cookie

反向代理服务器解决方案

总结

认识跨域请求

在提出解决方案之前我们先来认识一下跨域请求

浏览器发送一个HTTP请求有很多种方式,不同的方式发送的HTTP请求浏览器会将其分为不同的类型,比如请求一个html页面资源的类型为 document,使用 <script> 请求一个js脚本资源的类型是 script ,使用 XMLHttpRequest 请求一个接口的类型为 xhr,而我们今天的主角就是 xhr ,它是唯一能触发浏览器跨域策略的请求类型。

一个 xhr 请求包含请求行、消息报头(Header)、请求正文(Body)三个部分。其中请求行中包含了本次请求的URL,是否跨域主要是根据请求URL与当前发送请求的html页面地址是否是同源来决定。

当一个URL的协议、域名、端口号全部一致则为同源

例如

# 协议、域名、端口号全部一致:8080/:8080/api/user1

当一个URL的协议、域名、端口号有任何一处不一致则为跨域

例如

# 协议不一致:8080/:8080/api/user1# 域名不一致:8080/:8080/api/user1# 端口号不一致:8080/:3000/api/user1

所以当在页面 :8080 中使用 xhr 请求接口 :3000/api/user1 时,则浏览器抛出跨域错误

Index.html

<!DOCTYPE html><html lang="en"><head>  <meta charset="UTF-8">  <meta http-equiv="X-UA-Compatible" content="IE=edge">  <meta name="viewport" content="width=device-width, initial-scale=1.0">  <title>Document</title></head><body>  <script src=""></script>  <script>    const Axios = axios    ;(async () => {      const axios = Axios.create({ baseURL: :3000 })      const res = await axios.get(/api/user1)    })()  </script></body></html>

node-server.js

const http = require(http)const app = http.createServer((req, res) => {  const { method, url, headers: { cookie } } = req  console.log(method: , method, | url: , url, | cookie: , cookie)  if (method === GET && url === /api/user1) {    res.end(JSON.stringify([{ name: hagan, age: 22 }]))  }})app.listen(3000, () => {  console.log(listen 3000)})

CORS跨域解决方案

CORS英文全称Cross-origin resource sharing,中文翻译为跨域资源共享,是W3C标准,也是目前最推荐的跨域解决方案,该解决方案主要包含以下三种情况

1.简单跨域请求

在cors里,请求被分为两种类型,一种是简单跨域请求,另一种则是复杂跨域请求,当浏览器判断当前请求为简单跨域请求时,浏览器会启用较为宽松的安全策略,反之则会触发浏览器较高的安全防护策略

浏览器会通过以下标准来判断跨域请求是否为简单请求

为GET、POST、HEAD之一没有人为设置以下集合之外的其他请求头字段AcceptAccept-LanguageContent-LanguageContent-TypeContent-Type 的值是否为下列三者之一text/plainmultipart/form-dataapplication/x-www-form-urlencoded请求中的任意XMLHttpRequestUpload 对象均没有注册任何事件监听器。XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问请求中没有使用 ReadableStream 对象

以下为一个简单请求的示例

index.html

<!DOCTYPE html><html lang="en"><head>  <meta charset="UTF-8">  <meta http-equiv="X-UA-Compatible" content="IE=edge">  <meta name="viewport" content="width=device-width, initial-scale=1.0">  <title>Document</title></head><body>  <script src=""></script>  <script>    const Axios = axios    ;(async () => {      /**       * 简单跨域请求       * 请求流程:浏览器发送简单跨域请求,服务端正常响应请求,浏览器接受请求时发现响应头中没有Access-Control-Allow-Origin,浏览器拒绝接受请求       * 解决方法:服务端只需要加一个Access-Control-Allow-Origin就可以跨域访问       */      const axios = Axios.create({ baseURL: :3000 })      const res = await axios.get(/api/user1)      console.log(简单跨域请求: , res)    })()  </script></body></html>

这条请求能够正确被服务端接收处理和返回,但浏览器收到结果时会去响应头中判断 Access-Control-Allow-Origin 字段是否为 true,不符合则拒绝接收该请求并抛出跨域错误

解决这个错误只需要服务端在响应头中添加 Access-Control-Allow-Origin 为 true 便可解决。

node-server.js

const http = require(http)const app = http.createServer((req, res) => {  const { method, url, headers: { cookie } } = req  console.log(method: , method, | url: , url, | cookie: , cookie)  if (method === GET && url === /api/user1) { // 简单跨域请求    res.setHeader(Access-Control-Allow-Origin, :8080)    res.end(JSON.stringify([{ name: hagan, age: 22 }]))  }})app.listen(3000, () => {  console.log(listen 3000)})

2.复杂跨域请求

以下为复杂跨域请求示例

index.html

<!DOCTYPE html><html lang="en"><head>  <meta charset="UTF-8">  <meta http-equiv="X-UA-Compatible" content="IE=edge">  <meta name="viewport" content="width=device-width, initial-scale=1.0">  <title>Document</title></head><body>  <script src=""></script>  <script>    const Axios = axios    ;(async () => {      /**       * 复杂跨域请求       * 请求流程:浏览器发送复杂跨域请求时会提高安全等级,先发送一个OPTION类型的预检请求,请求没任何返回,预检请求失败,接口请求失败       * 解决方法:服务端处理OPTIONS请求并返回正确的响应头,浏览器校验响应头发现符合复杂跨域标准,预检请求成功,后续和简单跨域请求流程一致       */      const axios = Axios.create({ baseURL: :3000 })      const res = await axios.get(/api/user2, {        headers: { Hagan-Token: 123 }      })      console.log(复杂跨域请求: , res)    })()  </script></body></html>

当浏览器判断当前请求为复杂跨域请求时,浏览器会提高安全等级,先发送一个OPTION类型的预检请求,如果OPTION请求正确返回并且符合复杂跨域标准,才会发送真正的跨域请求去请求数据。如果OPTION请求没返回或不符合复杂跨域标准,则预检请求失败,接口请求失败

解决这个错误需要先在服务端处理 OPTION 类型的请求,然后返回符合复杂跨域标准的响应头,服务端代码如下

node-server.js

const http = require(http)const app = http.createServer((req, res) => {  const { method, url, headers: { cookie } } = req  console.log(method: , method, | url: , url, | cookie: , cookie)  if (method === OPTIONS && url === /api/user2) { // 复杂跨域请求    res.statusCode = 200    res.setHeader(Access-Control-Allow-Origin, :8080) // 可通过预检请求的域名    res.setHeader(Access-Control-Allow-Headers, Hagan-Token,Content-Type) // 可通过预检请求的Headers    res.setHeader(Access-Control-Allow-Methods, GET,POST) // 可通过预检请求的Method    res.end()  }  if (method === GET && url === /api/user2) { // 复杂跨域请求    res.setHeader(Content-Type, application/json)    res.setHeader(Access-Control-Allow-Origin, :8080)    res.setHeader(Set-Cookie, name=hagan)    res.end(JSON.stringify([{ name: hagan, age: 22 }]))  }})app.listen(3000, () => {  console.log(listen 3000)})

3.跨域请求携带Cookie

跨域请求默认不携带cookie,但有时在做业务开发时服务端还是需要根据cookie来做一些逻辑判断。

CORS也提供了携带Cookie的功能,主要有两步,1.客户端发送请求时指定 withCredentials 为 true,2.服务端在响应头设置 Access-Control-Allow-Credentials 为 true,代码如下

index.html

<!DOCTYPE html><html lang="en"><head>  <meta charset="UTF-8">  <meta http-equiv="X-UA-Compatible" content="IE=edge">  <meta name="viewport" content="width=device-width, initial-scale=1.0">  <title>Document</title></head><body>  <script src=""></script>  <script>    const Axios = axios    ;(async () => {      /**       * 跨域请求携带Cookie       * 请求流程:浏览器发送跨域请求时默认不会携带cookie,服务端无法接受到cookie导致服务端异常       * 解决方法:浏览器请求时添加withCredentials,此时服务端能接收到cookie,但返回响应时需要在响应头中添加Access-Control-Allow-Credentials,浏览器判断响应头中是否有Access-Control-Allow-Credentials,如果有则接受请求       */      const axios = Axios.create({ baseURL: :3000, withCredentials: true })      const res = await axios.get(/api/user3)      console.log(跨域请求携带Cookie: , res)    })()  </script></body></html>

node-server.js

const http = require(http)const app = http.createServer((req, res) => {  const { method, url, headers: { cookie } } = req  console.log(method: , method, | url: , url, | cookie: , cookie)  if (method === GET && url === /api/user3) { // 跨域请求携带Cookie    res.setHeader(Access-Control-Allow-Origin, :8080)    res.setHeader(Access-Control-Allow-Credentials, true)    res.end(JSON.stringify([{ name: hagan, age: 22 }]))  }})app.listen(3000, () => {  console.log(listen 3000)})

反向代理服务器解决方案

如果服务端资源可控,我们也可以通过服务端做反向代理的方式来解决跨域问题

:8080/:3000/api/user1

我们在页面 :8080/index.html 请求接口 :3000/api/user1 时跨域,如果我们通过反向代理把 :3000/api/user1 接口代理到 :8080/api/user1 下,那么在页面 :8080/index.html  请求接口 :8080/api/user1 就不会产生跨域问题了

实现反向代理有很多方式,比如nginx、webpack dev server等,这里提供一个node.js的示例

代码如下

node-server.js

const http = require(http)const app = http.createServer((req, res) => {  const { method, url } = req  if (method === GET && url === /api/user1) {    res.end(JSON.stringify([{ name: hagan, age: 22 }]))  }})app.listen(3000, () => {  console.log(listen 3000)})

proxy-server.js

const express = require(express)const { createProxyMiddleware } = require(http-proxy-middleware)const app = express()app.use(express.static(__dirname + /))app.use(/api, createProxyMiddleware({ target: :3000 }))app.listen(8080)

index.html

<!DOCTYPE html><html lang="en"><head>  <meta charset="UTF-8">  <meta http-equiv="X-UA-Compatible" content="IE=edge">  <meta name="viewport" content="width=device-width, initial-scale=1.0">  <title>Document</title></head><body>  <script src=""></script>  <script>    ;(async () => {      const res = await axios.get(/api/user1, {        headers: { Hagan-Token: 123 }      })      console.log(代理服务器解决方案: , res)    })()  </script></body></html>

总结

以上两种跨域解决方案已经能够覆盖大部分业务场景了,其他的一些跨域解决方案,比如 JSONP ,本文不太推荐使用,也不再展开介绍了,本文会在我的博客 www.hagan.zone 里持续更新,感兴趣的小伙伴可以通过查看原文关注我的博客。

关注我的,一起学习吧~