6

源代码探案系列之 .NET Core 跨域中间件 CORS

 3 years ago
source link: https://blog.yuanpei.me/posts/1276287490/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

源代码探案系列之 .NET Core 跨域中间件 CORS

2021-03-1632 25 min.

本文是 #源代码探案系列# 第三篇,今天这篇博客,我们来一起解读下 ASP.NET Core 中的 CORS 中间件,熟悉这个中间件的的小伙伴们,想必都已经猜出本文的主题:跨域。这确实是一个老生常谈的话题,可我并不认为,大家愿意去深入探究这个问题,因为博主曾经发现,每当工作中遇到跨域问题的时候,更多的是直接重写跨域相关的 HTTP 头。博主曾经写过一篇关于跨域的博客:《聊聊前端跨域的爱恨情仇》,当时是完全以前端的视角来看待跨域。所以,在今天这篇博客里,博主想带领大家从一种新的视角来看待跨域,也许,可以从中发现不一样的东西。

关于 ASP.NET Core 中的 CORS,大家都知道的是,可以通过UseCors()方法在整个 HTTP 请求管道中启用跨域中间件,或者是通过AddCors()方法来定义跨域策略,亦或者通过[EnableCors]来显式地指定跨域策略,更多的细节大家可以参考微软的官方文档,而在这里,我想聊一点大家可能不知道的东西,譬如:服务器端如何处理来自浏览器端的跨域请求?而这一切在 ASP.NET Core 中又如何实现?带着这些问题来解读 CORS 中间件的源代码,我们能更快的找到我们想得到的答案。一图胜千言,请允许博主使用这张流程图来“开宗明义”,我们这就开始今天的“探案”:

一张图览尽 CORS 中间件
一张图览尽 CORS 中间件

对于整个 CORS 中间件而言,核心部件主要有:CorsPolicyCorsService 以及 CorsMiddleware

CorsPolicy

整个 CORS 中间件中,首当其冲的是ICorsPolicy。这个接口的作用是定义跨域的策略,我们知道CORS中引入了Access-Control系列的 HTTP 头,所以,CorsPolicy 本质上是在定义允许哪些 HTTP 头、HTTP 方法、源(Origin) 可以访问受限的资源,以及当跨域请求是一个复杂请求的时候,预检请求的超时时间、是否支持凭据等等:

public class CorsPolicy
{
public bool AllowAnyHeader { get; }
public bool AllowAnyMethod { get; }
public bool AllowAnyOrigin { get; }
public Func<string, bool> IsOriginAllowed { get; private set; }
public IList<string> ExposedHeaders { get; } = new List<string>();
public IList<string> Headers { get; } = new List<string>();
public IList<string> Methods { get; } = new List<string>();
public IList<string> Origins { get; } = new List<string>();
public TimeSpan? PreflightMaxAge { get; set; }
public bool SupportsCredentials { get; set; }

在整个中间件的设计中,与CorsPolicy接口产生直接联系的,是CorsPolicyBuilderICorsPolicyProvider。相信大家从命名上就可以了解到,前者是一个基于建造者模式的、针对 CorsPolicy进行“加工”的工具类,可以快速地对 跨域策略中允许的 HTTP 方法、HTTP 头、源(Origin)等信息进行修改。关于这一点,我们可以从CorsPolicyBuilder提供的方法签名中得到印证,而最终CorsPolicyBuilder通过Build()方法来返回一个“加工”好的CorsPolicy

public class CorsPolicyBuilder 
{
CorsPolicyBuilder WithOrigins(params string[] origins);
CorsPolicyBuilder WithHeaders(params string[] headers);
CorsPolicyBuilder WithExposedHeaders(params string[] exposedHeaders);
CorsPolicyBuilder WithMethods(params string[] methods);
CorsPolicyBuilder AllowCredentials();
CorsPolicyBuilder DisallowCredentials();
CorsPolicyBuilder AllowAnyOrigin();
CorsPolicyBuilder AllowAnyMethod();
CorsPolicyBuilder AllowAnyHeader();
CorsPolicyBuilder SetPreflightMaxAge(TimeSpan preflightMaxAge);
CorsPolicyBuilder SetIsOriginAllowed(Func<string, bool> isOriginAllowed);
CorsPolicyBuilder SetIsOriginAllowedToAllowWildcardSubdomains();
CorsPolicy Build();
}

除了通过CorsPolicyBuilder来生成跨域策略,我们还可以通过ICorsPolicyProvider来生成跨域策略。如果你经常使用ASP.NET Core中的配置系统依赖注入,对于这种“套路”应该不会感到陌生。这里,微软提供了一个默认实现:DefaultCorsPolicyProviderDefaultCorsPolicyProvider本身依赖CorsOptions,允许使用者传入一个CorsPolicy的实例 或者是一个委托,来自定义跨域策略的“加工”细节,并在其内部维护一个字典,来实现具名的跨域策略。如果使用者不为当前跨域策略指定名称,则会使用默认的跨域策略名称。在大多数场景下,我们并不会直接使用CorsPolicyBuilder,而是在Startup类中通过委托来定义跨域策略,两者可以说是不同层次上的跨域策略的“提供者”。

// DefaultCorsPolicyProvider的GetPolicyAsync()
public Task<CorsPolicy?> GetPolicyAsync(HttpContext context, string? policyName)
{
if (context == null){
throw new ArgumentNullException(nameof(context));
}

policyName ??= _options.DefaultPolicyName;
if (_options.PolicyMap.TryGetValue(policyName, out var result)) {
return result.policyTask!;
}

return NullResult;
}

// CorsOptions
public void AddDefaultPolicy(CorsPolicy policy);
public void AddDefaultPolicy(Action<CorsPolicyBuilder> configurePolicy);
public void AddPolicy(string name, CorsPolicy policy);
public void AddPolicy(string name, Action<CorsPolicyBuilder> configurePolicy);
public CorsPolicy? GetPolicy(string name);

CorsService

OK,说完了跨域策略的“定义”,现在我们来看看跨域策略是如何被中间件“执行”的,这部分代码被定义在CoreService类的EvaluatePolicy()方法中。可以注意到,如果受限资源允许任意源(Origin)访问,则服务器端会认为这是一个不安全的跨域策略。

接下来,从HttpContext中提取客户端的源(Origin),请求方法(HttpMethod)。此时,服务器端可以根据请求方法和 HTTP 头 判断当前请求是都为预检请求。按照CORS规范,当请求方法为OPTION且请求头中含有Access-Control-Request-Method时,即表示这是一个预检请求。

至此,我们有了两种选择,预检请求会交给EvaluatePreflightRequest()方法去处理,非预检请求会交给EvaluateRequest()方法去处理。除了HttpContextCorsPolicy这两个参数以外,它们都会接受第三个参数CorsResult,它里面封装了我们一开始判断出来的关于源和预检请求的信息。继续细看,我们会发现这两个方法,都调用了PopulateResult()方法,继续顺着这条线索下去,我们就会发现,这个方法的主要作用是,结合跨域策略设定的各种参数,进一步对上一步生成的CorsResult进行“加工”。

public CorsResult EvaluatePolicy(HttpContext context, CorsPolicy policy)
{
// ...
if (policy.AllowAnyOrigin && policy.SupportsCredentials) {
throw new ArgumentException(Resources.InsecureConfiguration, nameof(policy));
}

var requestHeaders = context.Request.Headers;
var origin = requestHeaders[CorsConstants.Origin];

var isOptionsRequest = HttpMethods.IsOptions(context.Request.Method);
var isPreflightRequest = isOptionsRequest
&& requestHeaders.ContainsKey(CorsConstants.AccessControlRequestMethod);

var corsResult = new CorsResult {
IsPreflightRequest = isPreflightRequest,
IsOriginAllowed = IsOriginAllowed(policy, origin),
};

if (isPreflightRequest) {
//预检请求
EvaluatePreflightRequest(context, policy, corsResult);
}
else {
//非预检请求
EvaluateRequest(context, policy, corsResult);
}

return corsResult;
}

private static void PopulateResult(HttpContext context,
CorsPolicy policy,
CorsResult result
)
{
var headers = context.Request.Headers;
if (policy.AllowAnyOrigin) {
result.AllowedOrigin = CorsConstants.AnyOrigin;
result.VaryByOrigin = policy.SupportsCredentials;
} else {
var origin = headers[CorsConstants.Origin];
result.AllowedOrigin = origin;
result.VaryByOrigin = policy.Origins.Count > 1
|| !policy.IsDefaultIsOriginAllowed;
}

// 支持凭据
result.SupportsCredentials = policy.SupportsCredentials;
// 预检请求超时时间
result.PreflightMaxAge = policy.PreflightMaxAge;

// https://fetch.spec.whatwg.org/#http-new-header-syntax
AddHeaderValues(result.AllowedExposedHeaders, policy.ExposedHeaders);

// 允许的HTTP方法
var allowedMethods = policy.AllowAnyMethod ?
new[] { result.IsPreflightRequest ?
(string)headers[CorsConstants.AccessControlRequestMethod] :
context.Request.Method } :
policy.Methods;
AddHeaderValues(result.AllowedMethods, allowedMethods);

// 允许的HTTP头
var allowedHeaders = policy.AllowAnyHeader ?
headers.GetCommaSeparatedValues(CorsConstants.AccessControlRequestHeaders) :
policy.Headers;
AddHeaderValues(result.AllowedHeaders, allowedHeaders);
}

那么,这些参数最终的走向是哪里呢?我们注意到CorsService里有一个叫做ApplyResult()的方法,观察方法签名可以发现,它负责把跨域检测的结果应用到 HTTP 响应上,相信大家都能想到,这里会设置各种Access-Control系列的头,比如Access-Control-Allow-OriginAccess-Control-Allow-MethodsAccess-Control-Allow-Headers
Access-Control-Max-Age…等等。事实上,在CorsMiddleware中间件中,原本就是先调用EvaluateResult()方法,再调用ApplyResult()方法。当然,实际的代码中,还需要考虑[DisableCors][EnableCors]两个特性的影响,会多出一点判断的代码。关于跨域的代码层面的东西,我们就先讲到这里,在下一部分,我们会专门讲CORS里的简单请求和复杂请求。

public Task Invoke(HttpContext context, ICorsPolicyProvider corsPolicyProvider)
{
// ...
if (!context.Request.Headers.ContainsKey(CorsConstants.Origin)) {
return _next(context);
}

// [DisableCors]
var corsMetadata = endpoint?.Metadata.GetMetadata<ICorsMetadata>();
if (corsMetadata is IDisableCorsAttribute) {
var isOptionsRequest = HttpMethods.IsOptions(context.Request.Method);
var isCorsPreflightRequest = isOptionsRequest
&& context.Request.Headers.ContainsKey(CorsConstants.AccessControlRequestMethod);
if (isCorsPreflightRequest) {
// If this is a preflight request, and we disallow CORS, complete the request
context.Response.StatusCode = StatusCodes.Status204NoContent;
return Task.CompletedTask;
}

return _next(context);
}

// ...
// [EnableCors]
else if (corsMetadata is IEnableCorsAttribute enableCorsAttribute &&
enableCorsAttribute.PolicyName != null) {
// ...
// Evaluate && Apply
return EvaluateAndApplyPolicy(context, corsPolicy);
async Task InvokeCoreAwaited(HttpContext context, Task<CorsPolicy?> policyTask) {
var corsPolicy = await policyTask;
await EvaluateAndApplyPolicy(context, corsPolicy);
}
}
}

再论CORS

好了,行文至此。既然这篇博客的主题是“跨域”,那么,我们不妨多说一点。我们知道,“跨域”产生的背景是,浏览器作为一个公共环境,它本身是不被信任的,所以,为了杜绝非当前域的资源,例如Cookie、API等等被“窃取”,浏览器便增加了“跨域”这一限制。而为了顺应“前后端分离”、“微服务”等等的开发思想,“跨域”这个问题开始频繁地出现在人们的视野中,从最初的JSONP,到如今成为事实标准的CORS,甚至从Vue里的代理服务器、Nginx里的反向代理,我们总是能窥出一点“跨域”的影子,“跨域”可谓是无处不在。

那么,什么是 CORS 呢? CORS ,即跨域资源共享,是一种利用 HTTP 头部来指示服务器端对除自身以外的源(域、协议、端口)是否可以访问指定的资源。你可能会联想到OAuth2JWT等等关于认证授权的词汇,请注意,“跨域”始终发生在浏览器端,相对于浏览器,一般意义上的客户端都被视为可信任的。除此之外,CORS提供了一种被称之为“预检”的机制,它可以用来检测服务器端支持的 HTTP 请求头、HTTP 动词,在预检中,浏览器发送的头中标示有 HTTP 方法和真实请求中会用到的头。

为什么会发生跨域?
为什么会发生跨域?

如上图所示,浏览器端,特别是XMLHttpRequestFetch APIWeb字体 和 Canvas等始终遵循同源策略,domain-a.comdomain-b.com被视为两个不同域,因此,当domain-a.com试图访问domain-b.com下的资源时,就会被浏览器所限制,这就是我们所说的“跨域”。可能,这并不是一个特别好的例子,因为 HTML 中某些元素天生就被设计为允许跨域,例如:imageiframelinkscript等等。而如果我们通过“协商”来告诉domain-bdomain-a希望访问它下面的资源,这其实就是我们所说的 CORS 啦!这个“协商”过程呢,主要有两种,即 简单请求复杂请求

我们将不触发 CORS 预检 的请求称为简单请求,通常情况下,简单请求满足下列条件:

对于 简单请求 ,由于它的 HTTP 动词是确定的,故其跨域主要体现在服务器端返回的 HTTP 响应中,可能出现的响应头有:Access-Control-Allow-OriginAccess-Control-Allow-Headers等。所以,如果客户端请求的Origin被包含在服务器端返回的Access-Control-Allow-Origin中,则表示跨域被允许,反之则不被允许。所以,现在大家应该能想明白,为啥那些年里大家稀里糊涂地,把Access-Control-Allow-OriginAccess-Control-Allow-Headers设置为*就万事大吉了吧,而对照着中间件的代码,理解这层含义会更容易一点!

与简单请求不同,复杂请求 要求必须首先使用 OPTIONS 方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。”预检请求“的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。

预检请求

当浏览器检测到,从JavaScript中发起的请求需要被预检。此时,可以注意到,预检请求中同时携带了下面两个首部字段:

Access-Control-Request-Method: POST
Access-Control-Request-Headers:X-PINGOTHER, Content-Type

服务器在接受预检请求后,会返回以下响应头:

Access-Control-Allow-Origin: http://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400
  • 首部字段Access-Control-Allow-Methods表明服务器允许客户端使用 POST、GET 和 OPTIONS 方法发起请求。
  • 首部字段Access-Control-Allow-Headers表明服务器允许请求中携带字段 X-PINGOTHER 与 Content-Type。
  • 首部字段Access-Control-Max-Age表明该响应的有效时间为 86400 秒,即 24 小时。在有效时间内,浏览器无须为同一请求再次发起预检请求。

下面整理了 CORS 中常见的 Access-Control 系列头部字段:

Access-Control-Allow-Origin
Access-Control-Expose-Headers
Access-Control-Max-Age
Access-Control-Allow-Credentials
Access-Control-Allow-Methods
Access-Control-Allow-Headers
Origin
Access-Control-Request-Method
Access-Control-Request-Headers

本文分别从 源代码规范 两个角度探讨了 “跨域” 这个话题,两者可以说是相辅相成的存在,CORS 中间件实现了 CORS 规范,而通过 CORS 规范帮助我们理解了中间件。“跨域”产生的背景是,浏览器作为一个公共环境,它本身是不被信任的,所以,为了杜绝非当前域的资源,例如Cookie、API等等被“窃取”,浏览器便增加了 “跨域” 这一限制。最初我们通过 JSONP 这种方案来解决跨域问题,而后来我们有了CORS 这种事实上的标准,其原理上利用 OriginAccess-Control系列的头来标识服务器端可以允许哪些源、以什么样的 HTTP 动词 / 头来访问资源,按照 CORS 规范,浏览器端发起的请求被分为: 简单请求复杂请求 两种,两者最大的区别是,复杂请求 必须首先通过 OPTIONS 方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。好了,以上就是这篇博客的全部内容啦,欢迎大家在博客评论中参与讨论,再次谢谢大家,晚安!


Recommend

  • 79
    • 掘金 juejin.im 5 years ago
    • Cache

    跨域资源共享——CORS

    跨域资源共享(Cross-Origin Resource Sharing)是一种机制,它使用额外的 HTTP 头部告诉浏览器可以让一个web应用进行跨域资源请求。 请求类型 简单请求 若一个请求同时满足下述所有条件,则该请求可视为“简单请求”(注:灰色字体内容

  • 39
    • www.cnblogs.com 4 years ago
    • Cache

    Springboot CORS跨域访问

    Springboot CORS跨域访问 什么是跨域 浏览器的同源策略限制: 它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。可以说Web是构建在同源策略基础之上的,浏览器只是针...

  • 14
    • 微信 mp.weixin.qq.com 3 years ago
    • Cache

    跨域,不止CORS

    我们通常提到跨域问题的时候,相信大家首先会想到的是 CORS (跨源资源共享),其实  CORS 只是众多跨域访问场...

  • 7
    • www.wenyuanblog.com 3 years ago
    • Cache

    Django 通过设置 CORS 解决跨域问题

    Django 通过设置 CORS 解决跨域问题 一、Ajax 跨域请求Ajax 请求一个目标地址为非本域(协议、主机、端口任意一个不同)的 web 资源。 前端http://192.168.10.50:8080 后端

  • 3

    源代码探案系列之 .NET Core 限流中间件 AspNetCoreRateLimit2021-03-106 20 min.在上一篇文章中,博主带领大家一起深入了解 ConcurrencyLi...

  • 4

    2021-03-0438 15 min.打算开一个新的专栏——源代码探案系列,目的是通过源代码来探索更广阔的技术世界。因为我越来越意识到,我可能缺乏一个结构化的知识体系,虽然处在一个碎片化的时代,从外界接收了大量的信息,可这些碎片化的...

  • 4
    • xmanyou.com 2 years ago
    • Cache

    Nginx 配置允许跨域访问 CORS

    11 April 2020 / 开发笔记 Nginx 配置允许跨域访问 CORS 原文地址: https://en...

  • 4
    • segmentfault.com 2 years ago
    • Cache

    安全系列之:跨域资源共享CORS

    什么是跨域资源共享呢? 我们知道一个域是由scheme、domain和port三部分来组成的,这三个部分可以唯一标记一个域,或者一个服务器请求的地址。跨域资源共享的意思就是服务器允许其他的域来访问它自己域的资源。CORS是一个基于HTTP-header检测的机制,本文...

  • 4

    利用跨域资源共享(CORS)实现ajax跨域调用 浏览:3024次  出处信息     前几天看了一篇E文说部署CORS

  • 5
    • garywu520.github.io 2 years ago
    • Cache

    nginx开启跨域CORS

    nginx开启跨域CORS 2022-02-08 字数统计: 268字

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK