详细讲解跨域问题相关概念及常见的CORS和JSONP解决方案代码

562 阅读4分钟

什么是跨域问题

浏览器从一个域名的网页去请求另一个域名的资源时,域名、端口、协议任一不同,都是跨域

跨域跨域,见名知意,跨出领域的意思。那什么是领域呢?

我们知道,无论是前端还是后端代码想要运行,需要有一个服务支撑,也就是需要有一个ip(主机)地址和对应的这个ip(主机)上的端口。而这里的一个ip加端口,实际上就是指一个服务运行的地方,可以理解为ip加端口就是一个服务的领域(当然也包括~http或https也要保持一致,不然也会跨域)

而目前开发项目主要是前后端分离做法,即前端自己启动一个前端服务领域(前端自己有一个ip端口),后端自己启动一个后端服务领域(后端也自己有自己的ip端口)需要什么数据资源,发个请求给后端,后端收到请求以后,从数据库中查询数据,得到结果再返回给前端。所以,此时就会出现跨出领域的问题~前端跨出自己的领域到后端领域要数据。而,在浏览器中有这样一条神圣法则:同源策略法则,同源策略规则发现请求跨域了,就会立刻阻止这个请求,同时也会报错,提醒开发者。那么,什么是同源策略法则呢?

同源策略法则

所谓同源策源(Same-origin_policy),指的就是来源于同一个地方(域名、端口、协议要保持一致)的意思。这是一种非常重要的安全策略、计谋。目前基本上所有的浏览器都遵循同源策略,同源策略规定:

  1. 非同源的浏览器请求和相应,不能共享Cookie、LocalStorage 和 IndexDB等
  2. 非同源,DOM 和 Js对象无法获得
  3. 非同源,AJAX请求被禁掉了

我们可以看出同源策略还限制了不少操作的,无法获取Cookie、LocalStorage 、DOM 、Js对象连ajax请求也被限制发送了,这样的话,的确是安全多了。特别是对于银行、金融机构。关于网络安全方面的知识(XSS、CSRF、SQL注入攻击、Dos攻击什么的)在这里就不赘述了,大家只需要记住,,越严格的规则、越多条条框框,对于用户来说就越安全。所以给浏览器加入了同源策略限制。

但是同源策略安全是安全了,我们开发项目就略有麻烦啊。出现了跨域,ajax请求被禁用了,这样的话,前后端接口没法联调了啊,所以我们需要解决同源策略下的跨域问题

小历史:1995年,Netscape网景公司将同源策略规则引入浏览器,作为浏览器核心的安全功能,防范XSS、CSFR等攻击。

同源策略官网文档说明: developer.mozilla.org/zh-CN/docs/…

呈现跨域问题

在解决跨域问题之前,我们先来呈现一下跨域问题。看看跨域长什么样子的,这里的话,我们就让前后端服务分别是不同的ip和端口,看一下

后端代码

后端用的电脑的ip是10.9.26.107,端口是4000,所以后端的域就是: http://10.9.26.107:4000/

const http = require('http');// 引入http模块

const port = 4000;// 设置一个端口

const server = http.createServer((req, res) => { // 创建一个服务返回数据
    res.statusCode = 200;
    res.setHeader("Content-type""application/json");
    res.end('你好,跨域');
});

server.listen(port, () => { // 将服务启动在这个4000端口,并且监听
    console.log(`server running at ${port}/`);
});

前端代码

这里我们使用vue框架,加上axios库,快速高效些。至于前端的ip端口地址(域),这里用的是默认的本机端口:http://localhost:8080/

<template>
  <div id="box">
    <el-button @click="crossDomain">跨域浮现</el-button>
  </div>
</template>

<script>
const axios = require('axios'); // 引入axios库
export default {
  methods: {
    crossDomain(){
      axios.get('http://10.9.26.107:4000/') // 向后端的服务发请求
      .then((res)=>{
        console.log('请求结果',res);
      })
      .catch((err)=>{
        console.log('报错',err);
      })
    }
  },
};
</script>

这里我们的请求是:从 http://localhost:8080/ 域 到 http://10.9.26.107:4000/,ip和端口都不一样,当然协议都是http是一样的。所以就会受到同源策略的限制,就会出现跨域问题

跨域问题出现

当我们点击“跨域浮现”这个按钮的时候,出现这样的报错,network和控制台都有

1.png

翻译一下就是:

从本机的localhost:8080位置向http://10.9.26.107:4000/ 发的XMLHttp请求被CORS政策禁止掉了,缺少请求头设置Access-Control-Allow-Origin

这就是工作中常见的跨域问题的呈现,有问题就要解决,那我们如何解决呢?我们继续往下看...

使用CORS解决跨域问题

CORS简介

跨源资源共享(CORS)是W3C提出的解决跨域同源策略的一种方案(因为没有啥比较好的方案,所以就新提出这个解决方案了),上述的案例中控制台也有提示,就是使用cors去解决这个问题。只要设置一下,就可以解决跨域禁止访问的问题了。由于cors方案简单易用,所以使用广泛

官方地址: developer.mozilla.org/zh-CN/docs/…

cors解决跨域问题

若后端的访问没什么限制的话,都允许访问,只需要加上这句话,设置对应的响应头即可res.setHeader("Access-Control-Allow-Origin","*");因为星号*是代表所有的意思;如果想要指定某个网址,将星号换成对应网址即可。

关键代码如下:

const server = http.createServer((req, res) => {
    res.statusCode = 200;
    res.setHeader("Access-Control-Allow-Origin","*"); // 这句话表示允许所有的跨域请求, * 星号 指得是 所有的
    res.setHeader("Content-type""application/json");
    res.end('你好,跨域');
});

当然cors写法多样,作用也很大,在实际开发中我们可以有如下的写法:

 //允许的header类型
 res.header("Access-Control-Allow-Headers","content-type");
 //跨域允许的请求方式 
 res.header("Access-Control-Allow-Methods","DELETE,PUT,POST,GET,OPTIONS");
// 等等...

附上详细点的讲解 www.w3cschool.cn/javascript_…

CORS方案略为简洁方便,所以篇幅不多,接下来我们看看比较古老的跨域解决方案:JSONP

使用JSONP解决跨域

JSONP是什么

JSONP(JSON with Padding),是一个非官方的跨域解决方案,纯粹凭借程序员的聪明才智做出来的,但是很局限,只支持get请求。post、put、delete请求什么的,都不支持。平常工作中这种方式基本上不用,但是我们还是要知道的

JSONP工作原理

我们知道在html的标签中,有一些标签在创造的时候,就允许跨域,就允许在某个地址上获取资源(比如图片资源、文件资源等 )。比如img标签、link标签、iframe标签、script标签

而JSONP就是使用script标签的跨域能力来发送请求的。

JSONP写法举例

服务端代码

const express = require('express')
const app = express()
// 我们使用express框架,指定一个端口,返回给前端数据
app.get('/myServer', (req, res) => {
    res.end("hello JSONP")
})
app.listen(6789, () => {
    console.log(`服务已经启动`)
})

所以服务端代码的地址为:http://localhost:6789/myServer

前端代码

而前端的html代码中,我们使用的是vscode编辑器,并且安装了Live Server这个插件,所以我们右击通过启动浏览器,最终前端代码的域名端口是:http://127.0.0.1:5500/JSONP.html Live Server插件默认是5500端口。对比一下前后端服务的域名端口,我们发现,的确是跨域了,所以接下来我们就看看JSONP是否能够解决跨域问题

<!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>
    <!-- 使用src属性... -->
    <script src="http://localhost:6789/myServer"></script>
</body>
</html>

我们用script标签的src属性指定服务端的地址,那么浏览器加载渲染页面的时候,就会向src中的地址发请求,获取数据。这个时候我们打开F12中的network面板,我们会发现请求的确是发送成功了,也得到了数据,但是控制台却报错了。我们看一下,下面两张图,图示如下:

跨域成功请求图示

1.png

貌似很成功的解决了跨域问题,但是实际上,跨域问题才解决一半,因为控制台报错了,如图:

跨域成功控制台报错图示

2.png

JSONP报错原因

JSONP不能直接返回数据,直接返回数据浏览器识别不了,就会报语法错,报错信息上图。因为浏览器的script标签需要是js代码,既然要js代码,我们就想到给浏览器一个函数js代码,把需要返回给前端的数据,作为函数中的参数传递给前端,这样的话, 浏览器就能识别了。而前端只需要声明一个对应的函数接收就行了

换而言之,JSOP需要将数据作为函数的参数返回,而不是直接返回数据,对应的,前端需要声明一个相同名字的函数,用于接收这个数据

所以正确的写法是这样的

JSONP正确写法

服务端

app.get('/myServer', (req, res) => {
    let data = {
        name:"孙悟空",
        age:"500",
        home:"花果山水帘洞"
    }
    let resData = JSON.stringify(data)
    res.end(`fn(${resData})`)
})

客户端

<body>
    <script>
        // 这个函数名字fn必须和后端定义的名字保持一致,这样在解析的时候才能
        // 对应上,才能找到相应的函数,才能得到相应的数据
        function fn(data) {
            console.log(data);
        }
    </script>
    <script src="http://localhost:6789/myServer"></script>
</body>

JSONP前端完整写法

假设我们是点击按钮JSONP发请求,我们看一下原生方式代码如下:

<!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>
    <button>发请求</button>
    <script>
        // 第一步,声明一个函数用于接收后端返回的
        function fn(data) {
            console.log('后端返回的数据', data);
        }
        document.querySelector('button').onclick = function () {
            // 第二步,创建一个script标签
            let script = document.createElement("script")
            // 第三步,指定发请求的地址
            script.src = "http://localhost:6789/myServer"
            // 第四步,将标签插入到文档中
            document.body.appendChild(script)
        }
    </script>
</body>
</html>

所以上述就是一个简单的使用JSONP的小案例,但是,JSONP只支持GET请求,纵然JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。实际上我们想想,现在已经是2021年了,基本上老式浏览器都被淘汰了。所以JSONP这个基本上不用,但是我们也要知道这个东西

文章总结

  • CORS支持所有类型的HTTP请求,是跨域的最好的解决方案(需后端配置)
  • JSONP仅支持GET请求,JSONP的优势在于作用在一些古老的浏览器,毕竟古老的浏览器是没有CORS规则的(前端书写、后端配合定义函数名)说实话,基本不用,知道即可

还可以通过Node中转代理的方式,比如vue框架中的vue.config.js文件里面的devServer中的proxy对象做配置

当然也可以通过nginx反向代理解决跨域问题(后期写nginx文章的时候,会专门介绍相关nginx反向代理的配置)

不管是Node中间件代理还是nginx反向代理,主要是通过同源策略不作用于服务器来的控制的,毕竟同源策略是浏览器里面的规则,和服务器没有关系。日常工作中,主流跨域解决方案是cors和nginx反向代理