深入剖析.NETCORE中CORS(跨站資源共享)
阿新 • • 發佈: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 的設計非常的簡潔,包括兩大部分的內容,看圖:
![](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