跳至主要內容

跨域请求

wzCoding大约 9 分钟JavaScript跨域请求

跨域请求

在实际开发当中,当我们在向后端服务器请求数据时,难免会遇到跨域问题从而导致请求失败。跨域问题可以说是在前后端联调时经常会遇到的问题,那么跨域问题是如何产生的呢?又要如何解决跨域问题呢?

跨域产生的原因

跨域(也叫跨源)问题产生的根本原因是浏览器的 同源策略。同源策略是浏览器的一种安全策略,它限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制

同源 指的是:

  • 协议相同:使用的都是同一种网络通信协议,例如HTTP、HTTPS等
  • 域名相同:使用的是同一种网络域名,例如 www.baidu.combaidu.com
  • 端口相同:使用的是同一种网络端口,例如80和8080

只要互相通信的双方中不满足以上任意一项,那么就存在跨域问题

同源策略限制以下几种行为:

  • Cookie、LocalStorage 和 IndexDB 无法读取
  • DOM 和 JS 对象无法获得
  • AJAX 请求不能发送

跨域解决方案

JSONP

JSONP(JSON with Padding)是 JSON 的一种“使用模式”,可以让网页从别的域名(如:http://example.com)那获取资料,即跨域读取数据

JSONP 使用 <script> 标签的 src 属性,通过 HTTP 请求加载其他网站的脚本,并通过函数调用的方式返回数据 JSONP 由两部分组成:回调函数和数据

JSONP 优缺点

优点

  • 兼容性好,在 older browser 中也能运行,不需要 XMLHttpRequest 或 ActiveX 的支持
  • 跨域数据访问是 JSONP 的核心功能,它为跨域数据访问提供了一种新的解决方案

缺点

  • JSONP 只支持 GET 请求而不支持 POST 等其他类型的 HTTP 请求。
  • JSONP 安全性问题。因为 JSONP 请求的返回数据,都是直接作为代码来执行的。因此,如果返回的数据被恶意篡改,就可能会造成 XSS 攻击

使用场景

  • 跨域读取数据
  • 跨域提交数据

JSONP 实现

JSONP 的实现原理非常简单,就是利用 <script> 标签没有跨域限制的漏洞。通过 <script> 标签指向一个需要访问的地址并提供一个回调函数来接收数据当需要提交数据时,也通过 <script> 标签来实现,将数据提交到服务器的地址,然后由服务器提供返回的脚本,并执行这个脚本

实现步骤

  1. 创建一个 <script> 标签,设置其 src 属性为目标 URL,并设置一个回调函数来接收数据
  2. 服务器接收到请求后,将数据作为参数传递给回调函数,并将该函数返回给客户端
  3. 客户端接收到数据后,立即执行返回的函数,该函数将数据作为参数传递给事先定义好的回调函数,从而完成数据的接收

实现代码

<script>
    function jsonpCallback(data) {
        console.log(data);
    }
</script>
<script src="http://example.com/data?callback=jsonpCallback"></script>

CORS

CORS(Cross-Origin Resource Sharing)跨域资源共享,是一种机制,它使用额外的 HTTP 头来告诉浏览器让运行在一个 origin (domain) 上的 Web 应用被准许访问来自不同源服务器上的指定的资源。当一个资源从一个域或协议访问另一个域或协议的资源时,会出现跨域访问

CORS 实现原理

CORS 需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于 IE10

整个 CORS 通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS 通信与同源的AJAX 通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉

因此,实现 CORS 通信的关键是服务器。只要服务器实现了 CORS 接口,就可以跨源通信

CORS 请求分类

浏览器将 CORS 请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)

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

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

凡是不同时满足上面两个条件,就属于非简单请求

简单请求

对于简单请求,浏览器直接发出 CORS 请求。具体来说,就是在头信息之中,增加一个 Origin 字段

下面是一个例子,浏览器发现这次跨源AJAX请求是简单请求,就自动在头信息之中,添加一个 Origin 字段

GET /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

上面的头信息中,Origin 字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求

如果 Origin 指定的源,不在许可范围内,服务器会返回一个正常的 HTTP 回应。浏览器发现,这个回应的头信息没有包含 Access-Control-Allow-Origin 字段(详见下文),就知道出错了,从而抛出一个错误,被 XMLHttpRequest 的 onerror 回调函数捕获。注意,这种错误无法通过状态码识别,因为 HTTP 回应的状态码有可能是 200

如果 Origin 指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段

Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8

上面的头信息之中,有三个与 CORS 请求相关的字段,都以 Access-Control-开头

(1)Access-Control-Allow-Origin

该字段是必须的。它的值要么是请求时 Origin 字段的值,要么是一个 *,表示接受任意域名的请求

(2)Access-Control-Allow-Credentials

该字段可选。它的值是一个布尔值,表示是否允许发送 Cookie。默认情况下,Cookie 不包括在 CORS 请求之中。设为 true,即表示服务器明确许可,Cookie 可以包含在请求中,一起发给服务器。这个值也只能设为 true,如果服务器不要浏览器发送 Cookie,删除该字段即可

(3)Access-Control-Expose-Headers

该字段可选。CORS 请求时,XMLHttpRequest 对象的 getResponseHeader() 方法只能拿到 6 个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在 Access-Control-Expose-Headers 里面指定。上面的例子指定,getResponseHeader('FooBar') 可以返回 FooBar 字段的值

非简单请求

非简单请求是那种对服务器有特殊要求的请求,比如请求方法是 PUT 或 DELETE,或者 Content-Type 字段的类型是 application/json

非简单请求的 CORS 请求,会在正式通信之前,增加一次 HTTP 查询请求,称为"预检"请求(preflight)

浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些 HTTP 动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的 XMLHttpRequest 请求,否则就报错

下面是一段浏览器的 JavaScript 脚本

var url = 'http://api.alex.com/resource/foo';
var xhr = new XMLHttpRequest();
xhr.open('PUT', url, true);
xhr.setRequestHeader('X-Custom-Header', 'value');
xhr.send();

上面代码中,HTTP 请求的方法是 PUT,并且发送一个自定义头字段 X-Custom-Header 浏览器发现,这是一个非简单请求,就自动发出一个"预检"请求,要求服务器确认可以这样请求。下面是这个"预检"请求的 HTTP 头信息

OPTIONS /resource/foo HTTP/1.1
Host: api.alex.com
User-Agent: Mozilla/5.0...
Origin: http://api.alex.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header

"预检"请求用的请求方法是 OPTIONS,表示这个请求是用来询问的。头信息里面,关键字段是 Origin,表示请求的源站是 http://api.alex.com

除了 Origin 字段,"预检"请求的头信息包括两个特殊字段

(1)Access-Control-Request-Method

该字段是必须的,用来列出浏览器的 CORS 请求会用到哪些 HTTP 方法,上例是 PUT

(2)Access-Control-Request-Headers

该字段是一个逗号分隔的字符串,指定浏览器 CORS 请求会额外发送的头信息字段,上例是 X-Custom-Header

服务器收到"预检"请求以后,检查了 Origin、Access-Control-Request-Method 和 Access-Control-Request-Headers 字段之后,确认允许跨源请求,就可以做出回应

CORS简单使用

// 前端代码
fetch('https://api.example.com/data')
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error(error))

// 后端代码
const express = require('express')
const app = express()

app.get('/data', (req, res) => {
  res.setHeader('Content-Type', 'application/json')
  res.setHeader('Access-Control-Allow-Origin', '*')
  res.json({ message: 'Hello, World!' })
})

app.listen(3000, () => {
  console.log('Server started on port 3000')
})

使用代理

我们也可以使用代理来解决跨域问题。在Vue项目中,可以使用Vue CLI的配置文件来设置代理。

首先,在项目的根目录下创建一个名为vue.config.js的文件,并在其中进行配置。

module.exports = {
  devServer: {
    proxy: {
      '/api': {
        target: 'http://example.com', // 目标服务器地址
        changeOrigin: true, // 是否改变请求源
        pathRewrite: {
          '^/api': '' // 重写路径,将/api替换为空字符串
        }
      }
    }
  }
}

在上面的配置中,我们设置了代理规则,将所有以/api开头的请求转发到目标服务器 http://example.com。同时,我们设置了changeOrigin为true,表示改变请求源,以便目标服务器正确处理请求。最后,我们设置了pathRewrite,将/api替换为空字符串,以便去掉请求中的/api前缀

现在,当我们在Vue项目中发送请求时,例如 this.$http.get('/api/data'),请求将被转发到目标服务器 http://example.com/data,并且请求源也会被正确设置为 http://example.com

需要注意的是,在使用代理时,需要确保目标服务器能够正确处理跨域请求。如果目标服务器无法处理跨域请求,则需要在目标服务器上进行相应的配置