ASP.NET Core 6框架揭秘实例演示[41]:跨域资源的共享(CORS)花式用法
同源策略是所有浏览器都必须遵循的一项安全原则,它的存在决定了浏览器在默认情况下无法对跨域请求的资源做进一步处理。为了实现跨域资源的共享,W3C制定了CORS规范。ASP.NET利用CorsMiddleware中间件提供了针对CORS规范的实现。(本文提供的示例演示已经同步到《ASP.NET Core 6框架揭秘-实例演示版》)
[S2901]跨域调用API(源代码)
[S2902]显式指定授权Origin列表(源代码)
[S2903]手工检验指定Origin是否的权限(源代码)
[S2904]基于策略的资源授权(匿名策略)(源代码)
[S2905]基于策略的资源授权(具名策略)(源代码)
[S2906]将CORS规则应用到路由终结点上(代码编程形式)(源代码)
[S2907]将CORS规则应用到路由终结点上(特性标注形式)(源代码)
[S2901]跨域调用API
为了方便在本机环境下模拟跨域API调用,我们通过修改Host文件将本地IP映射为多个不同的域名。我们以管理员身份打开文件“%windir%\System32\drivers\etc\hosts”,并以如下所示的方式添加了针对四个域名的映射。
127.0.0.1 www.foo.com
127.0.0.1 www.bar.com
127.0.0.1 www.baz.com
127.0.0.1 www.qux.com
我们的演示程序由图1所示的两个ASP.NET程序构成。我们将API定义在Api项目中,App是一个JavaScript应用程序,它会在浏览器环境下以跨域请求的方式调用承载于Api应用中的API。

图1 演示实例解决方案结构
如下所示的Api程序中定义了表示联系人的Contact记录类型。我们注册了针对路径“/contacts”的路由使之以JSON的形式返回一组联系人列表。在调用Application对象的Run方法启动时,我们显式指定了监听地址“http://0.0.0.0:8080”。
var app = Application.Create();
app.MapGet("/contacts", GetContacts);
app.Run(url:"http://0.0.0.0:8080"); static IResult GetContacts()
{
var contacts = new Contact[]
{
new Contact("张三", "123", "zhangsan@gmail.com"),
new Contact("李四","456", "lisi@gmail.com"),
new Contact("王五", "789", "wangwu@gmail.com")
};
return Results.Json(contacts);
} public readonly record struct Contact(string Name,string PhoneNo ,string EmailAddress);
下面的代码片段展示了App应用程序的完整定义。我们通过注册针对根路径的路由使之现一个包含联系人列表的Web页面,我们在该页面中采用jQuery以AJAX的方式调用上面这个API获取呈现的联系人列表。我们将AJAX请求的目标地址设置为“http://www.qux.com:8080/contacts”。在AJAX请求的回调操作中,可以将返回的联系人以无序列表的形式呈现出来。
var app = Application.Create();
app.MapGet("/", Render);
app.Run(url:"http://0.0.0.0:3721"); static IResult Render()
{
var html = @"
<html>
<body>
<ul id='contacts'></ul>
<script src='http://code.jquery.com/jquery-3.3.1.min.js'></script>
<script>
$(function()
{
var url = 'http://www.qux.com:8080/contacts';
$.getJSON(url, null, function(contacts) {
$.each(contacts, function(index, contact)
{
var html = '<li><ul>';
html += '<li>Name: ' + contact.name + '</li>';
html += '<li>Phone No:' + contact.phoneNo + '</li>';
html += '<li>Email Address: ' + contact.emailAddress + '</li>';
html += '</ul>';
$('#contacts').append($(html));
});
});
});
</script >
</body>
</html>";
return Results.Text(content: html, contentType: "text/html");
然后先后启动应用程序Api和App。如果利用浏览器采用映射的域名(www.foo.com)访问App应用,就会发现我们期待的联系人列表并没有呈现出来。如果按F12键查看开发工具,就会发现图29-2所示的关于CORS的错误,具体的错误消息为“Access to XMLHttpRequest at 'http://www.qux.com:8080/contacts' from origin 'http://www.foo.com:3721' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.”。

图2 跨域访问导致联系人无法呈现
有的读者可能会想是否是AJAX调用发生错误导致没有得到联系人信息呢。如果我们利用抓包工具捕捉AJAX请求和响应的内容,就会捕获到如下所示的HTTP报文。可以看出AJAX调用其实是成功的,只是浏览器阻止了针对跨域请求返回数据的进一步处理。如下请求具有一个名为Origin的报头,表示的正是AJAX请求的“源”,也就是跨域(Cross-Orgin)中的“域”。
GET http://www.qux.com:8080/contacts HTTP/1.1
Host: www.qux.com:8080
Connection: keep-alive
Accept: application/json, text/javascript, */*; q=0.01
Origin: http://www.foo.com:3721
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36
Referer: http://www.foo.com:3721/
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7
HTTP/1.1 200 OK
Date: Sat, 13 Nov 2021 11:24:58 GMT
Server: Kestrel
Content-Length: 205 [{"name":"张三","phoneNo":"123","emailAddress":"zhangsan@gmail.com"},{"name":"李四",
"phoneNo":"456","emailAddress":"lisi@gmail.com"},{"name":"王五","phoneNo":"789",
"emailAddress":"wangwu@gmail.com"}]
[S2902]显式指定授权Origin列表
我们可以利用注册的CorsMiddleware中间件来解决上面这个问题。对于我们演示的实例来说,作为资源提供者的Api应用如果希望将提供的资源授权给某个应用程序,可以将作为资源消费程序的“域”添加到授权域列表中。演示程序调用了UseCors扩展方法完成了针对CorsMiddleware中间件的注册,并指定了两个授权的“域”。中间件涉及的服务则通过调用AddCors扩展方法进行注册。
var builder = WebApplication.CreateBuilder();
builder.Services.AddCors();
var app = builder.Build();
app.UseCors(cors => cors.WithOrigins(
"http://www.foo.com:3721",
"http://www.bar.com:3721"));
app.MapGet("/contacts", GetContacts);
app.Run(url:"http://0.0.0.0:8080");
...
由于Api应用对“http://www.foo.com:3721”和“http://www.bar.com:3721”这两个域进行了显式授权,如果采用它们来访问App应用程序,浏览器上就会呈现出图3所示的联系人列表。倘若将浏览器地址栏的URL设置成未被授权的“http://www.baz.com:3721”,我们依然得不到想要的显示结果。

图3 针对域的显式授权
下面从HTTP消息交换的角度来介绍这次由Api应用响应的报文有何不同。如下所示的是Api针对地址为“http://www.foo.com:3721”的响应报文,可以看出它多了两个名称分别为Vary和Access-Control-Allow-Origin的报头。前者与缓存有关,它要求在对响应报文实施缓存的时候,选用的Key应该包含请求的Origin报头值,它提供给浏览器授权访问当前资源的域。
HTTP/1.1 200 OK
Date: Sat, 13 Nov 2021 11:24:58 GMT
Server: Kestrel
Vary: Origin
Access-Control-Allow-Origin: http://www.foo.com:3721
Content-Length: 205 [{"name":"张三","phoneNo":"123","emailAddress":"zhangsan@gmail.com"},{"name":"李四",
"phoneNo":"456","emailAddress":"lisi@gmail.com"},{"name":"王五","phoneNo":"789",
"emailAddress":"wangwu@gmail.com"}]
[S2903]手工检验指定Origin是否的权限
对于我们演示的实例来说,当AJAX调用成功并返回联系人列表之后,浏览器正是利用Access-Control-Allow-Origin报头确定当前请求采用的域是否有权对获取的资源做进一步处理的。只有在授权明确之后,浏览器才允许执行将数据呈现出来的操作。从演示程序可以看出“跨域资源共享”所谓的“域”是由协议前缀(如“http://”或者“https://”)、主机名(或者域名)和端口号组成的,但在很多情况下,资源提供在授权的时候往往只需要考虑域名,这样的授权策略可以采用如下所示的方式来解决。UseCors扩展方法返回一个CorsPolicyBuilder对象,我们调用它的SetIsOriginAllowed方法利用提供的Func<string, bool>来设置授权规则,此规则只会考虑域名。
var validOrigins = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"www.foo.com",
"www.bar.com"
}; var builder = WebApplication.CreateBuilder();
builder.Services.AddCors();
var app = builder.Build();
app.UseCors(cors => cors.SetIsOrigi0nAllowed(
origin => validOrigins.Contains(new Uri(origin).Host)));
app.MapGet("/contacts", GetContacts);
app.Run(url:"http://0.0.0.0:8080");
...
[S2904]基于策略的资源授权(匿名策略)
CORS本质上还是属于授权的问题,所以我们采用类似于第28章“授权”介绍的方式将资源授权的规则定义成相应的策略,CorsMiddleware中间件就可以针对某个预定义的策略来实施跨域资源授权。在调用AddCors扩展方法时可以采用如下所示的方式注册一个默认的CORS策略。
var validOrigins = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"www.foo.com",
"www.bar.com"
}; var builder = WebApplication.CreateBuilder();
builder.Services.AddCors(options => options.AddDefaultPolicy(policy => policy.
SetIsOriginAllowed(origin => validOrigins.Contains(new Uri(origin).Host))));
var app = builder.Build();
app.UseCors();
app.MapGet("/contacts", GetContacts);
app.Run(url:"http://0.0.0.0:8080");
...
[S2905]基于策略的资源授权(具名策略)
除了注册一个默认的匿名CORS策略,我们还可以为注册的策略命名。下面的演示程序在调用AddCors扩展方法时注册了一个名为“foobar”的CORS策略,在调用UseCors扩展方法注册CorsMiddleware中间件时就可以显式地指定采用的策略名称。
var validOrigins = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"www.foo.com",
"www.bar.com"
}; var builder = WebApplication.CreateBuilder();
builder.Services.AddCors(options => options.AddPolicy("foobar", policy => policy.
SetIsOriginAllowed(origin => validOrigins.Contains(new Uri(origin).Host))));
var app = builder.Build();
app.UseCors(policyName:"foobar");
app.MapGet("/contacts", GetContacts);
app.Run(url:"http://0.0.0.0:8080");
...
[S2906]将CORS规则应用到路由终结点上(代码编程形式)
除了在调用UseCors扩展方法时指定Cors策略外,我们还可以在注册终结点的时候将Cors规则作为路由元数据应用到终结点上。如下的演示程序在调用MapGet方法注册了针对“/contacts”路径的终结点后会返回一个RouteHandlerBuilder对象,它接着调用该对象的RequireCors扩展方法来指定采用的CORS策略名称。
var validOrigins = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"www.foo.com",
"www.bar.com"
}; var builder = WebApplication.CreateBuilder();
builder.Services.AddCors(options => options.AddPolicy("foobar", policy => policy.
SetIsOriginAllowed(origin => validOrigins.Contains(new Uri(origin).Host))));
var app = builder.Build();
app.UseCors();
app.MapGet("/contacts", GetContacts).RequireCors(policyName:"foobar");
app.Run(url:"http://0.0.0.0:8080");
...
[S2907]将CORS规则应用到路由终结点上(特性标注形式)
我们也可以按照如下的方式在终结点处理方法GetContacts上标注EnableCorsAttribute特性,并利用其“policyName”参数来指定采用的CORS策略名称。如果使用Lambda表达式来定义终结点处理器,我们可以将EnableCorsAttribute特性直接标注在Lambda表达式前面。
using Microsoft.AspNetCore.Cors; var validOrigins = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"www.foo.com",
"www.bar.com"
}; var builder = WebApplication.CreateBuilder();
builder.Services.AddCors(options => options.AddPolicy("foobar", policy => policy.
SetIsOriginAllowed(origin => validOrigins.Contains(new Uri(origin).Host))));
var app = builder.Build();
app.UseCors();
app.MapGet("/contacts", GetContacts);
app.Run(url:"http://0.0.0.0:8080"); [EnableCors(policyName: "foobar")]
static IResult GetContacts()
{
var contacts = new Contact[]
{
new Contact("张三", "123", "zhangsan@gmail.com"),
new Contact("李四","456", "lisi@gmail.com"),
new Contact("王五", "789", "wangwu@gmail.com")
};
return Results.Json(contacts);
}
...
ASP.NET Core 6框架揭秘实例演示[41]:跨域资源的共享(CORS)花式用法的更多相关文章
- ASP.NET Core 6框架揭秘实例演示[07]:文件系统
ASP.NET Core应用具有很多读取文件的场景,如读取配置文件.静态Web资源文件(如CSS.JavaScript和图片文件等).MVC应用的视图文件,以及直接编译到程序集中的内嵌资源文件.这些文 ...
- ASP.NET Core 6框架揭秘实例演示[08]:配置的基本编程模式
.NET的配置支持多样化的数据源,我们可以采用内存的变量.环境变量.命令行参数.以及各种格式的配置文件作为配置的数据来源.在对配置系统进行系统介绍之前,我们通过几个简单的实例演示一下如何将具有不同来源 ...
- ASP.NET Core 6框架揭秘实例演示[09]:配置绑定
我们倾向于将IConfiguration对象转换成一个具体的对象,以面向对象的方式来使用配置,我们将这个转换过程称为配置绑定.除了将配置树叶子节点配置节的绑定为某种标量对象外,我们还可以直接将一个配置 ...
- ASP.NET Core 6框架揭秘实例演示[10]:Options基本编程模式
依赖注入使我们可以将依赖的功能定义成服务,最终以一种松耦合的形式注入消费该功能的组件或者服务中.除了可以采用依赖注入的形式消费承载某种功能的服务,还可以采用相同的方式消费承载配置数据的Options对 ...
- ASP.NET Core 6框架揭秘实例演示[11]:诊断跟踪的几种基本编程方式
在整个软件开发维护生命周期内,最难的不是如何将软件系统开发出来,而是在系统上线之后及时解决遇到的问题.一个好的程序员能够在系统出现问题之后马上定位错误的根源并找到正确的解决方案,一个更好的程序员能够根 ...
- ASP.NET Core 6框架揭秘实例演示[12]:诊断跟踪的进阶用法
一个好的程序员能够在系统出现问题之后马上定位错误的根源并找到正确的解决方案,一个更好的程序员能够根据当前的运行状态预知未来可能发生的问题,并将问题扼杀在摇篮中.诊断跟踪能够帮助我们有效地纠错和排错&l ...
- ASP.NET Core 6框架揭秘实例演示[13]:日志的基本编程模式[上篇]
<诊断跟踪的几种基本编程方式>介绍了四种常用的诊断日志框架.其实除了微软提供的这些日志框架,还有很多第三方日志框架可供我们选择,比如Log4Net.NLog和Serilog 等.虽然这些框 ...
- ASP.NET Core 6框架揭秘实例演示[14]:日志的进阶用法
为了对各种日志框架进行整合,微软创建了一个用来提供统一的日志编程模式的日志框架.<日志的基本编程模式>以实例演示的方式介绍了日志的基本编程模式,现在我们来补充几种"进阶" ...
- ASP.NET Core 6框架揭秘实例演示[15]:针对控制台的日志输出
针对控制台的ILogger实现类型为ConsoleLogger,对应的ILoggerProvider实现类型为ConsoleLoggerProvider,这两个类型都定义在 NuGet包"M ...
- ASP.NET Core 6框架揭秘实例演示[16]:内存缓存与分布式缓存的使用
.NET提供了两个独立的缓存框架,一个是针对本地内存的缓存,另一个是针对分布式存储的缓存.前者可以在不经过序列化的情况下直接将对象存储在应用程序进程的内存中,后者则需要将对象序列化成字节数组并存储到一 ...
随机推荐
- [架构]辨析: 高可用 | 集群 | 主从 | 负载均衡 | 反向代理 | 中间件 | 微服务 | 容器 | 云原生 | DevOps | ...
词汇集 灾备 冷备份 双机热备份 异地容灾备份 云备份 灾难演练 磁盘阵列(RAID) 故障切换 心跳监测 高可用 集群 主从复制(Master-Slave) 多集群横向扩容(master-clust ...
- Java设计模式 —— 面向对象设计原则
1 设计模式概述 1.1 设计模式的定义与分类 设计模式的定义 Design patterns are descriptions of communicating objects and classe ...
- Python程序笔记20230301
打印九九乘法表 for i in range(1, 10): for j in range(1, i+1): print(i, "x", j, "=", i * ...
- 【谷粒商城】(一)docker搭建以及项目的创建
网络地址转换-端口转发 VmWare网络配置可以参考这篇:VMWare虚拟机网络连接设置_santirenpc的博客-CSDN博客_vmware 上网设置,真的是被折磨到了.. Docker 虚拟化容 ...
- 帝国cms将没有搜索到结果的关键字存入到数据库的方法
在帝国cms网站前台搜索一个关键字,如果在网站中查询到了,这个关键字会被记录入搜索关键字表中,但是如果在网站中没有搜索到,就不会记录入搜索关键字表中,那怎么把没有搜索结果的关键字才能记录到数据库中,方 ...
- SpringBoot整合EMQ
1.引入依赖 <dependency> <groupId>org.eclipse.paho</groupId> <artifactId>org.ecli ...
- Search Engine + Recommendation System
PageRank 该网络的 邻接矩阵 通过变换可以变成 概率转移 矩阵 设该转移矩阵为M,最终每个节点的重要性向量为r,则有了一个状态转移方程\(M \cdot r = r\),(虽然严格意义上,应该 ...
- 记一次 .NET 某外贸ERP 内存暴涨分析
一:背景 1. 讲故事 上周有位朋友找到我,说他的 API 被多次调用后出现了内存暴涨,让我帮忙看下是怎么回事?看样子是有些担心,但也不是特别担心,那既然找到我,就给他分析一下吧. 二:WinDbg ...
- 白嫖GitHub Action实现开源项目CICD
什么是CI/CD?让你的项目变得更加敏捷! 在今天这个快速变化的时代,开发者们需要与时俱进,不断提升自己的工作效率.在这篇文章里,将一起探讨如何使用CI/CD和Github Action让你的项目更加 ...
- RTSP Server(LIVE555)源码分析(一)-重要关系类
live项目包括四个基本的库,程序入口类(在mediaServer中),各种测试代码(测试代码在testProgs里面). 四个基本的库分别是: UsageEnvironment&TaskSc ...