1. 程式人生 > >深入剖析.NETCORE中CORS(跨站資源共享)

深入剖析.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 的設計非常的簡潔,包括兩大部分的內容,看圖: ![](https://img2020.cnblogs.com/blog/26882/202007/26882-20200723174045903-461772115.png) 從上圖中我們可以看出,左邊是入口,是我們常見的 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 setupAction) { ... AddCorsServices(builder.Services); ... } public static IMvcCoreBuilder ConfigureCors(this IMvcCoreBuilder builder,Action setupAction) { ... } // Internal for testing. internal static void AddCorsServices(IServiceCollection services) { services.AddCors(); services.TryAddEnumerable( ServiceDescriptor.Transient()); services.TryAddTransient(); } } ``` 重點就在上面的 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(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](https://docs.microsoft.com/zh-cn/aspnet/core/security/cors?view=aspnetcore-3.1#same-origin) 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