跨域问题的产生

跨域是由浏览器的同源策略引起的。 原来浏览器的同源策略会阻止从不同来源读取资源。此机制会阻止恶意网站读取其他网站的数据,但也可以防止合法使用。这就是跨域问题产生的根本原因。

同源策略

同源策略的特征也是很明显的,它的定义如下:
==如果两个 URL 的协议、端口(如果有指定的话)和主机都相同的话,则这两个 URL 是_同源_的。==

核心点就以下三个条件:

  • 协议相同
  • 域名相同
  • 端口相同

接下来们将用实际例子来加深一下理解:

详解跨域问题--三种解决方案

也就是说协议、域名、端口这三点要同时满足一致性才满足同源策略。

同源策略存在的原因是为了保护用户的安全和隐私,防止恶意网站对其他网站进行攻击或滥用。

要解决这个问题呢我们就要引入一个新的概念-------跨源资源共享 (CORS)

跨源资源共享 (CORS)

现代 Web 应用通常需要从不同来源获取资源。它们可以是公开资源,应可供任何人读取,但同源政策会阻止使用。
跨域资源共享 (CORS) 会以标准化方式解决此问题。 启用 CORS 可让服务器告知浏览器它可以使用其他来源。

详解跨域问题--三种解决方案

这幅图就形象描述了我们请求的数据被浏览器拦截的情况。

CORS 的工作原理是什么?

同源政策会告知浏览器屏蔽跨源请求。当您需要来自其他来源的公开资源时,资源提供服务器会告知浏览器,发送请求的来源可以访问其资源。浏览器会记住这一点,并允许对该资源进行跨源资源共享。

第 1 步:客户端(浏览器)请求

当浏览器发出跨域请求时,浏览器会添加一个 Origin 标头,其中包含当前源站(架构、主机和端口)。

第 2 步:服务器响应

当服务器看到此标头并希望允许访问时,它会在响应中添加一个 Access-Control-Allow-Origin 标头来指定请求来源(或添加 * 以允许任何来源)。

第 3 步:浏览器收到响应

当浏览器看到此响应带有相应的 Access-Control-Allow-Origin 标头时,便会与客户端网站共享响应数据。

详解跨域问题--三种解决方案

上图就是CROS过程的执行图了,所以我们要解决跨域问题,要么就是发生的请求和响应数据的服务器是同源服务器,要么我们就要在响应数据的标头上标记允许访问数据的来源地址,接下来我们将用一个实例来讲解如何解决跨域问题。

在这里我们要补充一个知识-----axios封装,这是为了我们后续同源代理代码部分的讲解所插入的一个知识点~~·

axios封装

首先我们得在src文件夹下面新建一个common文件夹来封装axios请求,我们得在common文件夹下面新建apiRequest.jsindex.js

详解跨域问题--三种解决方案

index.js

这个index.js,我们就封装一个后端的服务器访问路径:

export const baseURL = () => "https://localhost:7260/";

apiRequest.js

这里是我们创建axios请求的部分。

import axios from "axios";
import { baseURL } from "./index";
//service:创建好的 axios封装
const service = axios.create({
    baseURL: baseURL(),
});

这样每次我们要调用axios请求的时候就不用每个网页都去创建一个axios请求实体了,我们直接调用我们封装好的js文件即可~~。比如在我们的log.vue中:

<script setup>
//中文包
import zhCn from "element-plus/es/locale/lang/zh-cn";

import { ref, onMounted } from "vue";
//import axios from "axios";
import { ClickOutside } from "element-plus";
import service from "../../common/apiRequest";//将axios请求实体导入
//列表数据绑定结果
const tableData = ref([]);
const currentPage = ref(0);
const pageSize = ref(10);
const recordCount = ref();
const searchQuery = ref({
  currentPage: 1,
  pageSize: 5,
  recordCount: 1,
  searchDateTime: [],
});
const logQuery = async () => {
  var url = `/api/SystemLog/${searchQuery.value.currentPage}/${searchQuery.value.pageSize}`;
  var searchDatearray = searchQuery.value.searchDateTime;
  if (searchDatearray.length == 2) {
    var timestampStart = Date.parse(searchDatearray[0]);
    var timestampEnd = Date.parse(searchDatearray[1]);
  url=`/api/SystemLog/${searchQuery.value.currentPage}/${searchQuery.value.pageSize}/${timestampStart / 1000}/${timestampEnd / 1000}`;
  }
  //var service = axios.create({ baseURL: "https://localhost:7260/" });
  let response = await service.get(url);//发起请求。
  let result = response.data;
  tableData.value = result.dataList;
  searchQuery.value.recordCount = result.recordCount;
};
</script>

其中我们发起请求的部分就在:let response = await service.get(url); 是不是很方便??我们封装完之后一句话就搞定了发起请求的指令~~

同源代理解决跨域

同源代理思路介绍

我们要理清楚问题的根本产生者------浏览器,也就是说我们服务器发送请求和响应请求数据是正常执行的,只不过响应的数据是被浏览器给拦截了,拦截了之后就导致我们前端代码无法获取对应的数据信息。

综上我们发现服务器和服务器直接的请求推送是没问题的,那解决的第一种思路就出来了,我们前端的代码发生请求的时候就不直接发送到异源的后端服务器,我们把请求发送到前端代码所在的服务器中去,让前端服务器转发请求给后端服务器,返回给的数据交给前端服务器转手一下再给前端代码,这样我们就完美绕过了浏览器同源策略的限制,接下来我将用两幅图来阐述同源代理前后的请求情况。

详解跨域问题--三种解决方案


详解跨域问题--三种解决方案


乍一看是不是完全一样呀??注意仔细我用蓝色字迹加粗部分,那部分才是暗藏玄机,通俗来说就是我同源代理前浏览器在检查同源策略的时候会发现你请求的 URL 的协议、端口(如果有指定的话)和后端服务器的url不一致,所以它将响应数据拦截了不给前端代码获取。而我们的同源代理则是我们前端代码发起的请求是给前端服务器a的url地址,响应的数据也是前端服务器a的地址,这样我们的浏览器一看,发起请求和相应请求的是同源服务器,果断就将数据放行了。

下面我们来看一下同源代理前后的请求url地址的变化将更能理解以上的概念:

详解跨域问题--三种解决方案

我们可以看到当我们不做同源代理处理之前,我们请求是直接显示后端服务器地址7260端口的。

详解跨域问题--三种解决方案

这个是我们的前端服务器地址,端口号为4000

接下来我们来看一下经过同源代理之后的请求url:

详解跨域问题--三种解决方案

看到了不,我们的请求url也变成了我们的前端服务器端口4000啦~~这不就是变成同源了嘛~哈哈哈完美地骗过了浏览器的同源检测~~接下来我将来讲解代码实现,我们的示例是基于我们上篇文章讲的浪浪山管理系统系列的代码来完成的,相关代码有时间我会打包好统一放在文章底部。

同源代理代码实现

浪浪山管理系统前端是基于vite实现的,所以首先要在vite.config.js中配置代理转发

  server: {
    proxy: {
      '/api': {
        // 本地环境
        target: "https://localhost:7260/", //指定我们要转发的后端服务器的url
        // 启用跨域访问
        changeOrigin: true,  //
        // 忽略自签名证书错误
        secure: false,
        }
      }
    }

这个里为啥设置'/api'呢?这个得看我们请求的时候发生的URL,我们上面log.vue的时候我们写了var url = /api/SystemLog/${searchQuery.value.currentPage}/${searchQuery.value.pageSize};,所以我们就用/api去匹配这个对应的请求URL。

  1. server 配置
    • 这是 Vite 配置文件中的一部分,用于配置开发服务器的行为。
  2. proxy 配置
    • 该配置项用于设置代理规则。代理可以将特定的请求转发到另一个服务器,通常用于解决跨域问题。
  3. '/api' 路由
    • 该配置项表示所有以 /api 开头的请求都会被代理到指定的目标服务器。
  4. target
    • target: "https://localhost:7260/":表示将 /api 开头的请求代理到 https://localhost:7260/。这是目标服务器的地址。
  5. changeOrigin
    代理服务器将会:
    重写 Host 字段
    • 将请求头中的Host字段替换为目标服务器的主机名。这意味着对于前端的每一个请求,代理服务器会将Host: localhost:4000替换为Host: localhost:7260(或者目标服务器的实际主机名)。
      伪装请求来源
    • 使得目标服务器认为请求是直接发送给它的,而不是通过代理服务器转发的。这可以绕过某些服务器的安全检查,并确保请求被正确处理
  6. secure
    • secure: false:表示在代理到目标服务器时忽略 HTTPS 证书错误。这通常用于开发环境中,目标服务器使用自签名证书的情况。在生产环境中,应该设置为 true 以确保安全。

这里我们要讲一下为啥我们要设一个secure: false,这个其实和我们的同源代理没关系,这个问题在于我们后端https://localhost:7260/https://localhost:7260/https签名是我们自定义的而不是通过机构的发行的签证导致我们请求的时候被拦截,所以我们要把这个拦截我们自签证书的功能暂时关闭,所以我们就写了secure:false。接下来让我们重回“案发现场”给大家加深一下印象吧~~

详解跨域问题--三种解决方案

返回状态显示了500 内部服务器错误,其本质的问题就是我们访问的时候此时我们的https是我们自定义的,导致被拦截了~~

我们在vite.config.js中配置代理转发之后我们还有一个细节要注意,那就是我们在封装axios请求的时候:

import axios from "axios";
import { baseURL } from "./index";
//service:创建好的 axios封装
const service = axios.create({
  //  baseURL: baseURL(),
});

此时我们得把baseURL: baseURL()注释掉,因为这个是直接指向了我们的后端url:https://localhost:7260/,但在同源代理中我们得让其先指向前端服务器的地址:http://localhost:4000/,然后转发给后端url,所以这里我们注释掉baseURL之后axios请求将默认指向当前前端服务器地址http://localhost:4000/.这样我们就完美地从前端代码请求=》前端URL=》代理转发=》后端URL=》前端URL=》前端代码的流程啦~~。

大家其实有可能还疑惑,我不用前端代理,我们的请求发送路径也不是同样的这个路径吗??是的,虽然他们看起来很像,但是还是有区别的,我们使用前端代理的时候,我们前端代码请求的是我们的前端服务器,返回也是要前端服务器去返回,而我们不使用前端代理的时候,我们前端代码是直接请求后端服务器的,这时候浏览器一看,你发送请求是前端服务器的URL,而请求的是目标后端URL,这俩URL是异源的,所以直接屏蔽数据。用了前端代理之后浏览器发现,前端代码请求的是前端服务器URL,返回的时候也是前端服务器的URL,满足同源策略,果断放行,这样看有没有一点障眼法的 感觉呢~~嘿嘿。

至此我们的同源代理的思路解决跨域问题就讲解完毕了~~~


服务器自封装解决跨域问题

我们的同源代理解决跨域问题呢是通过伪装成同源服务器来绕过浏览器的同源策略,那我们的另外一个思路就是告诉浏览器:"我不装了,其实对方是自己人"。我们直接在响应数据的时候告诉浏览器,对方服务器的URL是我自己设定的,你无需阻拦,快快放行!!!。

详解跨域问题--三种解决方案

要实现这个思路,我们先回到"案发现场",上图就是我们跨域问题产生的时候浏览器控制台的报错信息,报错信息提到请求的资源在响应标头没有Access-Control-Allow-Origin标记。这个标记就是我们上面提到的让浏览器放行的””自己人标记“。

接下来我们进入后端代码去实现它!!!

代码实现

我们在后端代码(还是我们之前文章写的浪浪山管理系统的后端代码)新建一个Utility文件夹,在其内部我们再新建一个名叫Filters的文件夹,在Filters文件夹中我们新建一个名叫CustomAlwaysOnResultFilterAttribute.cs的类,在这个类中我们封装一个Access-Control-Allow-Origin特性以供控制器(Controllers)调用。

using Microsoft.AspNetCore.Mvc.Filters;
namespace langlangshanWebAPI.Utility.Filters
{   /// <summary>
    /// 这个Filter的特点:请求只要是到达了服务器,就会执行进来
    /// </summary>
    public class CustomAlwaysOnResultFilterAttribute : Attribute, IAsyncAlwaysRunResultFilter
    {
        public async Task OnResultExecutionAsync(ResultExecutingContext context,
            ResultExecutionDelegate next)
        {
            context.HttpContext.Response.Headers.Add("Access-Control-Allow-Origin", "*");
            await next.Invoke();
        }
    }
}

这里我来解析一下这段代码,首先我们继承的父类是Attribute,Attribute.NET框架自带的特性类,这个特性类呢你可以把它理解成要实现功能自身的特性,也就功能其与生具有的能力,然后我们把这个特性赋予给我们想要的对于功能代码,这样我们就可以一劳永逸地批量去实现代码功能。否则我们每调用Access-Control-Allow-Origin一次这个标记就得在对应控制器上写一次,非常麻烦,代码的耦合性高。

介绍完继承Attribute之后我们讲解一下为啥还要实现一个IAsyncAlwaysRunResultFilter接口,继承这个接口前记得引入其命名空间using Microsoft.AspNetCore.Mvc.Filters;~~ ,因为们这个类继承的是特性,特性是本身没有执行代码的能力,所以.NET框架就有这样一个机制:

  • 在ASP.NET Core中,框架会识别在控制器或操作方法上应用的特性,并检查这些特性是否实现了某些过滤器接口。如果实现了,框架会在适当的阶段调用这些接口的方法。

也就是说我们特性Attribute必须要和过滤器(Filter)合用才能完整执行对应功能,而我们这里的IAsyncAlwaysRunResultFilter就是其中一个特殊的过滤器,其特性就是无论发生什么情况(包括错误情况),其都会执行。其它的过滤器可能报错一类的会中断,这个过滤器就不会。

public async Task OnResultExecutionAsync(ResultExecutingContext context,
            ResultExecutionDelegate next)
        {
            context.HttpContext.Response.Headers.Add("Access-Control-Allow-Origin", "*");
            await next.Invoke();
        }

这段代码呢就是我们IAsyncAlwaysRunResultFilter的实现,其中:

  • ResultExecutingContext提供了当前请求和响应的上下文信息,并允许你在结果执行之前进行操作或取消结果的执行。
  • ResultExecutionDelegate 是一个委托,通过调用它可以继续执行请求处理管道中的下一个过滤器或操作结果。

所以这里是将"Access-Control-Allow-Origin", "*")添加进响应标头,其中*代码意思是是允许任意url通过。

最后我们通过过滤器await next.Invoke();去异步执行即可。接下来我们就可以去控制器类应用这个特性了~~~

局部应用

我们打开SystemLogController.cs,在SystemLogController类的上方应用特性[CustomAlwaysOnResultFilterAttribute]即可,这样我们就在当前控制器解决了跨域问题。

详解跨域问题--三种解决方案

一键应用

我们在程序的初始进入入口Program.cs进行全局控制器应用的配置:

builder.Services.AddControllers(options =>
{
    options.Filters.Add<CustomAlwaysOnResultFilterAttribute>();
});

这里我们在控制器全局配置注入阶段利用Lambda 表达式将特性应用于全部的控制器,以实现一键应用的效果。


ASP.NET Core 的 CORS(跨域资源共享)中间件

ASP.NET Core 中有专门用于配置跨域资源共享(CORS)策略的中间件,我们只需给其指定我们所需的跨域策略即可。

我们同样在Program.cs进行配置

builder.Services.AddCors(options =>
{
    options.AddPolicy("allcore", corsBuilder =>
    {
        corsBuilder.AllowAnyHeader()
        .AllowAnyMethod()
        .AllowAnyOrigin();
    });
});

app.UseCors("allcore");//使用策略

接下来我们讲解一下以上的代码部分:

• AddCors 方法:向依赖注入容器中添加 CORS 服务。
• AddPolicy 方法:添加一个命名的 CORS 策略。
• CorsPolicyBuilder 配置:允许任何 HTTP 头、方法和来源。
• UseCors 中间件:在请求管道中应用 CORS 策略。

这里我们自定义一个策略名称"allcore,这个是我们自定义的变量名称,可以随便你换成你喜欢的变量名称。

  1. AllowAnyHeader:
    • 允许请求包含任何 HTTP 头。
    • 这意味着客户端可以发送包含任何自定义头的请求,而不会被 CORS 策略阻止。
    • 例如,客户端可以发送包含 Authorization、Content-Type 或任何其他自定义头的请求。
  2. AllowAnyMethod:
    • 允许任何 HTTP 方法。
    • 这意味着客户端可以使用任何 HTTP 方法(如 GET、POST、PUT、DELETE 等)发送请求,而不会被 CORS 策略阻止。
    • 例如,客户端可以发送 GET 请求来获取数据,发送 POST 请求来提交数据,发送 PUT 请求来更新数据,或发送 DELETE 请求来删除数据。
  3. AllowAnyOrigin:
    • 允许任何来源的请求。
    • 这意味着任何域名的客户端都可以向你的服务器发送请求,而不会被 CORS 策略阻止。

通过以上三个策略方法设定之后我们跨域问题就解决了,其内容与我们自己自定义策略名称allcore绑定。接下来我们只要在应用这个自定义策略策略就好了:

app.UseCors("allcore");//使用策略

这里的allcore就是我们定义的自定义策略,必须要和我们定义的时候的策略名称一致哦~~否则可不会生效!!!

至此我们的跨域问题就解决了


以上就是我们解决跨域问题的三种方法,如果你有更多想法想与我交流,欢迎联系我~~