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

Ron.Liang發表於2020-07-24

前言

由於現代網際網路的飛速發展,我們在開發現代 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 的設計非常的簡潔,包括兩大部分的內容,看圖:

從上圖中我們可以看出,左邊是入口,是我們常見的 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

相關文章