深入剖析.NETCORE中CORS(跨站资源共享)

前言

由于现代互联网的飞速发展,我们在开发现代 Web 应用程序中,经常需要考虑多种类型的客户端访问服务的情况;而这种情况放在15年前几乎是不可想象的,在那个时代,我们更多的是考虑怎么把网页快速友好的嵌套到服务代码中,经过服务器渲染后输出HTML到客户端,没有 iOS,没有 Android,没有 UWP。更多的考虑是 防止 XSS,在当时的环境下,XSS一度成为各个站长的噩梦,甚至网站开发的基本要求都要加上:必须懂防 XSS 攻击。

CORS 定义

言归正传,CORS(Cross-Origin Resource Sharing)是由 W3C 指定的标准,其目的是帮助在各个站点间的资源共享。CORS 不是一项安全标准,启用 CORS 实际上是让站点放宽了安全标准;通过配置 CORS,可以允许配置中的请求源执行允许/拒绝的动作。

在 .NETCore 中启用 CORS

在 .NETCore 中,已经为我们集成好 CORS 组件 Microsoft.AspNetCore.Cors,在需要的时候引入该组件即可,Microsoft.AspNetCore.Cors 的设计非常的简洁,包括两大部分的内容,看图:

深入剖析.NETCORE中CORS(跨站资源共享)

从上图中我们可以看出,左边是入口,是我们常见的 AddCors/UseCors,右边是 CORS 的核心配置和验证,配置对象是 CorsPolicyBuilder 和 CorsPolicy,验证入口为 CorsService,中间件 CorsMiddleware 提供了拦截验证入口。

CorsService 是整个 CORS 的核心实现,客户端的请求流经中间件或者AOP组件后,他们在内部调用 CorsService 的相关验证方法,在 CorsService 内部使用配置好的 PolicyName 拉去相关策略进行请求验证,最终返回验证结果到客户端。

Microsoft.AspNetCore.Mvc.Cors

通常情况下,我们会在 Startup 类中的 ConfigureServices(IServiceCollection services) 方法内部调用 AddCors() 来启用 CROS 策略,但是,该 AddCors() 并不是上图中 CorsServiceCollectionExrensions 中的 AddCors 扩展方法。

实际上,在 ConfigureServices 中调用的 AddCors 是处于程序集 Microsoft.AspNetCore.Mvc.Cors ;在 Microsoft.AspNetCore.Mvc.Cors 内部的扩展方法 AddCors() 中,以 AOP 方式定义了对 EnableCorsAttribute/DisableCorsAttributeAttribute 的拦截检查。

具体做法是在程序集 Microsoft.AspNetCore.Mvc.Cors 内部,定义了类 CorsApplicationModelProvider ,当我们调用 AddCors 扩展方法的时候,将进一步调用 CorsApplicationModelProvider.OnProvidersExecuting(ApplicationModelProviderContext context) 方法,从而执行检查 EnableCorsAttribute/DisableCorsAttributeAttribute 策略。

所以,我们在 ConfigureServices 中调用的 AddCore,其实是在该程序集内部定义的类: MvcCorsMvcCoreBuilderExtensions 的扩展方法,我们看 MvcCorsMvcCoreBuilderExtensions 的定义

public static class MvcCorsMvcCoreBuilderExtensions
{
    public static IMvcCoreBuilder AddCors(this IMvcCoreBuilder builder)
    {
       ...
       AddCorsServices(builder.Services);
       ...
    }

    public static IMvcCoreBuilder AddCors(this IMvcCoreBuilder builder,Action<CorsOptions> setupAction)
    {
      ...
      AddCorsServices(builder.Services);
      ...
    }

    public static IMvcCoreBuilder ConfigureCors(this IMvcCoreBuilder builder,Action<CorsOptions> setupAction)
    {
      ...
    }

    // Internal for testing.
    internal static void AddCorsServices(IServiceCollection services)
    {
        services.AddCors();

        services.TryAddEnumerable(
            ServiceDescriptor.Transient<IApplicationModelProvider, CorsApplicationModelProvider>());
        services.TryAddTransient<CorsAuthorizationFilter, CorsAuthorizationFilter>();
    }
}

重点就在上面的 AddCorsServices(IServiceCollection services) 方法中, 在方法中调用了 CORS 的扩展方法 AddCors()。

那么我们就要问, CorsApplicationModelProvider 是在什么时候被初始化的呢?
答案是在 startup 中 ConfigureServices(IServiceCollection services) 方法内调用 services.AddControllers() 的时候。在AddControllers() 方法内部,调用了 AddControllersCore 方法

private static IMvcCoreBuilder AddControllersCore(IServiceCollection services)
{
    // This method excludes all of the view-related services by default.
    return services
        .AddMvcCore()
        .AddApiExplorer()
        .AddAuthorization()
        .AddCors()
        .AddDataAnnotations()
        .AddFormatterMappings();
}

理解了 CORS 的执行过程,下面我们就可以开始了解应该怎么在 .NETCore 中使用 CORS 的策略了

CORS 启用的三种方式

在 .NETCore 中,可以通过以下三种方式启用 CORS

1、使用默认策略/命名策略的中间件的方式
2、终结点路由 + 命名策略
3、命名策略 + EnableCorsAttribute

通过上面的三种方式,可以灵活在程序中控制请求源的走向,但是,残酷的事实告诉我们,一般情况下,我们都是会对全站进行 CORS。所以,现实情况就是在大部分的 Web 应用程序中, CORS 已然成为皇帝的新装,甚至有点累赘。

CorsPolicyBuilder(CORS策略)

通过上面的 CORS 思维导图,我们已经大概了解了 CORS 的整个结构。由上图我们知道,CorsPolicyBuilder 位于命名空间 Microsoft.AspNetCore.Cors.Infrastructure 中。
在内部提供了两种基础控制策略:全开/半开。这两种策略都提供了基本的方法供开发者直接调用,非常的贴心。

全开

public CorsPolicyBuilder AllowAnyHeader();
public CorsPolicyBuilder AllowAnyMethod();
public CorsPolicyBuilder AllowAnyOrigin();
public CorsPolicyBuilder AllowCredentials();

半开

public CorsPolicyBuilder DisallowCredentials();
public CorsPolicyBuilder WithHeaders(params string[] headers);
public CorsPolicyBuilder WithMethods(params string[] methods);
public CorsPolicyBuilder WithOrigins(params string[] origins);

上面的策略定义从字面理解就可以知道其用途,实际上呢,他们的实现原理也是非常的简单。在 CorsPolicyBuilder 内部维护着一个 CorsPolicy 对象,当你使用全开/半开方式配置策略的时候,builder 会将配置写入内部 CorsPolicy 中存储备用。

比如半开 WithOrigins(params string[] origins);,通过迭代器将配置的源写入 _policy.Origins 中。

    public CorsPolicyBuilder WithOrigins(params string[] origins)
    {
        foreach (var origin in origins)
        {
            var normalizedOrigin = GetNormalizedOrigin(origin);
            _policy.Origins.Add(normalizedOrigin);
        }

        return this;
    }

开始使用

在理解了配置的过程后,我们就可以进入真正的使用环节了,通过上面的学习我们知道,启用 CORS 有三种方式,咱们一步一步来。

使用默认策略/命名策略的中间件的方式

所谓的命名策略就是给你的策略起个名字,默认策略就是没有名字,所有的入口都使用同一个策略,下面的代码演示了命名策略

private readonly string CORS_ALLOW_ORGINS = "cors_allow_orgins";

public void ConfigureServices(IServiceCollection services)
{
    services.AddCors(options =>
    {
        options.AddPolicy(CORS_ALLOW_ORGINS, policy =>
        {
            policy.WithOrigins("http://localhost:5500", "http://localhost:8099");
        });
    });
    services.AddControllers().AddJsonOptions(options =>
    {
        options.JsonSerializerOptions.Converters.Add(new StringJsonConverter());
    });
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    app.UseRouting();
    app.UseCors(CORS_ALLOW_ORGINS);
    app.UseAuthorization();
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}

上面的代码演示了如何在站点中全局终结点启用 CORS,首先声明了命名策略 cors_allow_orgins ,然后将其用 AddCors() 添加到 CORS 中,最后使用 UseCors() 启用该命名策略,需要注意的是,AddCors() 和 UseCors() 必须成对出现,并且要使用同一个命名策略。

终结点路由 + 命名策略

.NETCore 支持通过对单个路由设置 CORS 命名策略,从而可以实现在一个系统中,对不同的业务提供个性化的支持。终结点路由 + 命名策略的配置和上面的命名策略基本相同,仅仅是在配置路由的时候,只需要对某个路由增加 RequireCors 的配置即可

private readonly string CORS_ALLOW_ORGINS = "cors_allow_orgins";
public void ConfigureServices(IServiceCollection services)
{
    services.AddCors(options =>
    {
        options.AddPolicy(CORS_ALLOW_ORGINS, policy =>
        {
            policy.WithOrigins("http://localhost:5500", "http://localhost:8099");
        });
    });
    services.AddControllers().AddJsonOptions(options =>
    {
        options.JsonSerializerOptions.Converters.Add(new StringJsonConverter());
    });
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    app.UseRouting();
    app.UseCors();
    app.UseAuthorization();
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllerRoute("weatherforecast", "{controller=WeatherForecast}/{action=Get}").RequireCors(CORS_ALLOW_ORGINS);
        // endpoints.MapControllers();
    });
}

上面的代码,指定了路由 weatherforecast 需要执行 CORS 策略 CORS_ALLOW_ORGINS。通过调用 RequireCors() 方法,传入策略名称,完成 CORS 的配置。RequireCors 方法是在程序集 Microsoft.AspNetCore.Cors 内部的扩展方法,具体是怎么启用策略的呢,其实就是在内部给指定的终结点路由增加了 EnableCorsAttribute ,这就是下面要说到的第三种启用 CORS 的方式。

来看看 RequireCors() 内部的代码

public static TBuilder RequireCors<TBuilder>(this TBuilder builder, string policyName) where TBuilder : IEndpointConventionBuilder
{
    if (builder == null)
    {
        throw new ArgumentNullException(nameof(builder));
    }
    builder.Add(endpointBuilder =>
    {
        endpointBuilder.Metadata.Add(new EnableCorsAttribute(policyName));
    });
    return builder;
}

命名策略 + EnableCorsAttribute

最后一种启用 CORS 的方式是使用 EnableCorsAttribute 特性标记,和 RequireCors 方法内部的实现不同的是,这里说的 EnableCorsAttribute 是显式的指定到控制器上,在应用 EnableCorsAttribute 的时候,你可以应用到根控制器或者子控制器上,如果是对根控制器进行标记,被标记的根控制器和他的所有子控制器都将受指定 CORS 策略的影响;反之,如果只是对子控制器进行标记,CORS 策略也只对当前控制器产生影响。

CORS 的初始化

public void ConfigureServices(IServiceCollection services)
{
    services.AddCors(options =>
    {
        options.AddPolicy("controller_cors", policy =>
        {
            policy.WithOrigins("http://localhost:5500", "http://localhost:8099");
        });
        options.AddPolicy("action_cors", policy =>
        {
            policy.WithOrigins("http://localhost:5500", "http://localhost:8099");
        });
    });
    services.AddControllers().AddJsonOptions(options =>
    {
        options.JsonSerializerOptions.Converters.Add(new StringJsonConverter());
    });
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    app.UseRouting();
    app.UseCors();
    app.UseAuthorization();
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}

在上面的代码中,因为 EnableCorsAttribute 可以应用到类和属性上,所以我们定义了两个 CORS 策略,分别是 controller_cors 和 action_cors。接下来将这两种策略应用到 WeatherForecastController 上。

应用 EnableCorsAttribute 特性标记

[ApiController]
[Route("[controller]")]
[EnableCors("controller_cors")]
public class WeatherForecastController : ControllerBase
{
    [EnableCors("action_cors")]
    [HttpPost]
    public string Users()
    {
        return "Users";
    }

    [DisableCors]
    [HttpGet]
    public string List()
    {
        return "List";
    }

    [HttpGet]
    public string Index()
    {
        return "Index";
    }
}

在上面的 WeatherForecastController 控制器中,我们将 controller_cors 标记到控制器上,将 action_cors 标记到 Action 名称为 Users 上面,同时,还对 List 应用了 DisableCors ,表示对 List 禁用 CORS 的策略,所以我们知道,在 CORS 中,有 AddCors/UseCors,也有 EnableCors/DisableCors ,都是成对出现的。

其它策略

我们还记得,在 .NETCore 中,一共有 4 种策略,分别是:Header、Method、Origin、Credentials,但是本文仅演示了 WithOrigins 这一种方式,相信通过这一种方式的演示,对大家在启用其它策略的时候,其思想也是一致的,所谓的标头、请求方式、凭据 等等,其基本法是不变的。

通过对 Microsoft.AspNetCore.Cors 的内部实现的剖析,我们了解到,其实现 CORS 的原理非常简单,结构清晰,就算不用系统自带的 CORS 组件,自行实现一个 CORS 策略,也是非常容易的。

参考资料:
(CORS) 启用跨域请求 ASP.NET Core

GitHub:
https://github.com/dotnet/aspnetcore/tree/master/src/Mvc/Mvc/src
https://github.com/dotnet/aspnetcore/tree/master/src/Mvc/Mvc.Cors/src
https://github.com/dotnet/aspnetcore/tree/master/src/Middleware/CORS/src

上一篇:NetCore +EF+Mysql 从数据库生成实体类到项目


下一篇:NetCore使用NPOI导出Word中的图片信息