ASP.NET Core
本身已经内置了一系列服务和中间件组件,提供开发 Web 应用中通常的开发需求。熟悉和掌握这些特性工具,绝对让我们节省时间,提供效率。有些功能需求如果让我们自己实现的话,绞尽脑汁都可能做不完全,结果发现,用几个语句应用某个特性就万事大吉了。
准备我们的应用:
Listing 14-1 新建 NET 8 项目
dotnet new globaljson --sdk-version 8.0.400 --output ASPNETCoreFeatures
dotnet new web --output ASPNETCoreFeatures --framework8.0
dotnet enw sln --output ASPNETCoreFeatures
dotnet sln ASPNETCoreFeatures add ASPNETCoreFeatures
用 VS Code 打开项目文件或者目录。修改 Properties 文件夹里的 launchSettings.json 文件中的 http 端口为 5000, https 端口为 5001.
使用配置服务
在项目文件夹里有两个 appsettings Json 文件,它们是 dotnet new 模板创建的主配置文件。appsettings.json 是如下内容:
Listing 14-2 appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
这个文件包含两个部分。
Logging 部分 这部分是配置应用的 logging 行为。
LogLevel :定义最小的 log 级别。
Default: “Information”
表示所有严重程度小于等于 Information 的 log ,包括 Warning, Error 或者 Critical 都会被记录。低级别的 Debug 或者 Trace 将被忽略。Microsoft.AspNetCore: “Warning”
表示由 Microsoft.AspNetCore 命名空间产生的可以被记录的 log 最低严重级别是 Warning ,因此,Error 和 Critical 也将被记录。
AllowedHosts 部分 指定应用可以接受从任何主机名发来的请求,也可以指定类似 “www.example.com” 这样的限定主机名。
而 appsettings.Development.json 的内容是这样的:
Listing 14-3 appsettings.Development.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
这些配置只适用开发环境,由全局变量 ASPNETCORE_ENVIRONMENT 来设定是否开发模式还是生成模式。我们经常会改变记录级别,以获得更多的信息或者帮助调试。在 launchSettings.json 文件里模板初始指定用的环境变量值是 Development ,因此这是现在的默认配置文件。
在开发过程中,我们为了获得更多的信息,可以在这个文件里添加其他重要的记录入口,比如: System 。
Listing 14-5 修改 appsettings.Development.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"System": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
访问配置
配置数据可以通过 IConfiguration 接口提供。我们通过接口的 API 来浏览配置的机构和读取配置设置。
Listing 14-6 在 Program.cs 文件里添加读取配置
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("config", async (HttpContext context, IConfiguration config) =>
{
string? defaultLogLevel = config["Logging:LogLevel:Default"];
await context.Response.WriteAsync($"Default log level: {defaultLogLevel}");
});
app.MapGet("/", () => "Hello World!");
app.Run();
我们用带冒号 : 的区域分隔字符串来指定路径。运行应用,在浏览器地址栏的 localhost:5000 后面加上 /config ,得到输出结果 Default log level: Information
。
介绍 launchSettings.json
launchSettings.json 也是一个配置文件,包含应用的启动配置设置,HTTP 和 HTTPS 请求端口等。
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:5000",
"sslPort": 44381
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:5001;http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
IIS 配置设置服务是用于旧版的应用部署。环境变量集合里指定了一个环境变量 ASPNETCORE_ENVIRONMENT,其值为 Development 。这个变量也可以通过 IConfiguration 接口访问;如果我们在 Program.cs 文件里的 “config” 端点方法里添加访问 config[“ASPNETCORE_ENVIRONMENT”] 环境变量的语句:
Listing 14-7 修改 Program.cs
...
app.MapGet("config", async (HttpContext context, IConfiguration config) =>
{
string? defaultLogLevel = config["Logging:LogLevel:Default"];
await context.Response.WriteAsync($"Default log level: {defaultLogLevel}");
string? env = config["ASPNETCORE_ENVIRONMENT"];
await context.Response.WriteAsync($"nThe environment Setting is: {env}");
});
...
我们可以得到 The environment Setting is: Development
的输出结果。
IWebHostEnvironment 服务
对于环境变量,ASP.NET Core
提供了一个 IWebHostEnvironment 的服务,方便我们取得当前环境变量设置。这些环境变量方法都在 Microsoft.Extensions.
IWebHostEnvironment 扩展方法
方法名 | 描述 |
---|---|
EnvironmentName | 这个属性返回当前环境 |
IsDevelopment() | 如果是开发环境,此方法返回真值 |
IsStaging() | 如果是演示环境, 此方法返回真值 |
IsProduction() | 如果是生产环境,此方法返回真值 |
IsEnvironment(env) | 如果按参数定义的环境被选择,此方法返回真值 |
Listing 14-8 修改 Program.cs
...
app.MapGet("config", async (HttpContext context, IConfiguration config,
IWebHostEnvironment webHostEnv) =>
{
string? defaultLogLevel = config["Logging:LogLevel:Default"];
await context.Response.WriteAsync($"Default log level: {defaultLogLevel}");
string? env = config["ASPNETCORE_ENVIRONMENT"];
await context.Response.WriteAsync($"nThe environment Setting is: {env}");
await context.Response.WriteAsync($"nEnvName is: {webHostEnv.EnvironmentName}");
});
...
运行应用,在前面输出结果后面会增加一行 EnvName is: Development
。
存储用户机密数据
在开发过程中,我们经常需要用到一些比较敏感的数据。比如,API 密钥、数据库连接密码等等;如果我们把这些数据的明文都放在类或者Json 配置文件中,就有非常严重的泄露风险。因为基本上源代码库的安全性都不会特别地高,更不用提项目是做成开发源码的话。
我们可以使用用户机密服务(user secrets) ,把敏感数据存储在项目之外的一个文件里,这个文件不会被版本控制检查到,可以有效防止这些数据暴露给其他人员。
Listing 14-9 在终端中初始化用户机密
dotnet user-secrets Init
终端将显示类似的信息: “Set UserSecretsId to ‘xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx’ for MSBuild project ‘XXXXXXASPNETCoreFeaturesASPNETCoreFeatures.csproj’.” ,表示初始化成功。
接着设置我们的机密。
Listing 14-10 在终端中设置机密数据
dotnet user-secrets set "TheSecret:Id" "User"
dotnet user-secrets set "TheSecret:Password" "P@ssword"
机密数据可以用前缀:机密名
格式保存,这里我们的前缀是 TheSecret ,便于我们分组。
Listing 14-11 用 list 显示保存的机密数据
dotnet user-secrets list
前面的一番操作后,在
%APPDATA%MicrosoftUserSecrets 文件夹(或 Linux 下的
~/.microsoft/usersecrets 文件夹)中创建了一个 JSON 文件来存储机密。
Listing 14-12 修改 Program.cs 的 MapGet 方法,加入读取机密代码
...
app.MapGet("config", async (HttpContext context, IConfiguration config,
IWebHostEnvironment webHostEnv) =>
{
string? defaultLogLevel = config["Logging:LogLevel:Default"];
await context.Response.WriteAsync($"Default log level: {defaultLogLevel}");
string? env = config["ASPNETCORE_ENVIRONMENT"];
await context.Response.WriteAsync($"nThe environment Setting is: {env}");
await context.Response.WriteAsync($"nEnvName is: {webHostEnv.EnvironmentName}");
string? userId = config["TheSecret:Id"];
string? userPassword = config["TheSecret:Password"];
await context.Response.WriteAsync($"nUser Id: {userId}");
await context.Response.WriteAsync($"nUser Password: {userPassword}");
});
...
运行应用后,我们可以得到输出结果中显示正确的机密数据。
User Id: User
User Password: P@ssword
使用日志 Logging
ASP.NET Core
包含一个内置日志服务,使开发人员能够将不同级别(如调试、警告、错误)的信息记录到各种输出,如控制台、文件或第三方日志提供程序。
默认情况下启用三个内置提供程序:控制台提供程序、调试提供程序和事件源提供程序、调试提供程序和事件源提供程序。
其他两个常用的提供者: 文件提供程序(用第三方扩展 Serilog 或者 NLog 把日志文件记录到一个文件),Azure Monitor 整合。
调试程序提供程序提供程序转发消息,以便通过 System.Diagnostics.Debug 类处理,而事件源提供程序则转发消息到事件跟踪工具进行后续处理。
日志主要功能
- 内置抽象
ASP.NET Core
使用 ILogger 接口来抽象日志实现。 - 日志级别 可按不同的严重程度编写日志:
- 跟踪: 最详细的日志,用于调试。
- 调试: 用于开发和调试。
- 信息: 用于一般应用程序流程跟踪。
- 警告: 用于意外情况或可恢复的错误。
- 错误: 用于妨碍功能的错误。
- 严重: 用于需要立即处理的严重错误。
- 无: 禁用日志记录。
- 可扩展性 可使用 Serilog、NLog 或 log4net 等第三方库进行高级日志记录。
- 结构化日志记录 日志信息可包括结构化数据(如键值对)。
- 配置 日志设置可在 appsettings.json 中配置,也可在 appsettings.Development.json 等特定环境中重写。
使用日志记录的必要性
- 故障排除: 帮助识别和调试生产和开发环境中的问题。
- 监控: 深入了解应用程序的行为和性能。
- 审计跟踪: 跟踪用户活动和系统事件。
- 集中管理: 日志可发送到 Azure Monitor、Splunk 或 ELK 等服务进行分析。
使用日志
对于中间件 middleware 和端点 endpoint ,ASP.NET Core
默认是可以直接让它们直接存取 ILogger 。
在项目文件夹新建一个文件夹 Endpoints ,在其中新建一个 Coffee.cs 文件。
Listing 14-13 在 Endpoints 文件夹里新建 Coffee.cs 文件
namespace ASPNETCoreFeatures.Endpoints
{
public class Coffee
{
public static async Task GetCoffee(HttpContext context
, ILogger<Coffee> logger)
{
logger.LogDebug($"Getting coffee for {context.Request.Path}");
string coffeeType = context.Request.RouteValues["type"] as string
?? "espresso";
int calorie = coffeeType.ToLower() switch
{
"espresso" => 5,
"latte" => 190,
"cappuccino" => 120,
_ => 0
};
await context.Response.WriteAsync($"Your {coffeeType} has {calorie} calories.");
logger.LogDebug($"Coffee is ready from {context.Request.Path}");
}
}
}
我们先给端点一个 ILogger<Coffee>
参数,因为日志信息使用 ILogger<T>
接口;接着在这个端点类的方法的开始和结束前各添加一条调用 LogDebug 的语句。日志使用的约定是用类来特指泛型 T 代表的日志类别。中间件和端点可以不用构造函数,直接默认已经依赖注入了 ILogger 接口。在方法作用,用 LogDebug 方法来生成日志。
ILogger<T>
接口具有的扩展方法:
- LogTrace 用于在开发中跟踪低级别调试
- LogDebug 用于在开发中或者生产版中低级别调式
- LogInformation 提供应用的通用状态信息
- LogError 用于记录被应用处理的异常或者错误
_ LogCritcal 用于记录严重错误
接着,我们在 Program.cs 中添加端点方法。
Listing 14-14 修改 Program.cs
using ASPNETCoreFeatures.Endpoints;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("coffee/{type?}", Coffee.GetCoffee);
app.MapGet("/", () => "Hello World!");
app.Run();
在终端里输入 dotnet run
运行应用,在浏览器地址栏主地址后面加上 /coffee/latte ,调用端点方法,页面会显示:
Your latte has 190 calories.
但是,日志信息在哪里呢? 终端没有输入我们设定的信息。是我们的代码错误?不是!终端没有显示日志信息的原因是开发环境配置文件 appsettings.Development.json 的 Logging 项里 Default 的值是 Information ,不能显示低级的调试信息。
Listing 14-15 修改 appsettings.Development.json 内容
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
重新启动应用,输入 /coffee/latte 回车,在终端会显示类似信息:
dbug: Microsoft.Extensions.Hosting.Internal.Host[2]
Hosting started
dbug: ASPNETCoreFeatures.Endpoints.Coffee[0]
Getting coffee for /coffee/latte
dbug: ASPNETCoreFeatures.Endpoints.Coffee[0]
Coffee is ready from /coffee/latte
我们可以看到这两条日志分类是 ASPNETCoreFeatures.Endpoints.Coffee 。
在主入口里使用日志
如果我们把主入口改成这样:
Listing 14-16 给 Program.cs 添加日志
using ASPNETCoreFeatures.Endpoints;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.Logger.LogDebug("Starting the pipeline configuration ... ");
app.MapGet("coffee/{type?}", Coffee.GetCoffee);
app.MapGet("/", () => "Hello World!");
app.Logger.LogDebug("Pipeline configuration is done.");
app.Run();
在终端运行 dotnet run
命令,输出内容类似:
dbug: ASPNETCoreFeatures[0]
Starting the pipeline configuration ...
dbug: ASPNETCoreFeatures[0]
Pipeline configuration is done.
dbug: Microsoft.Extensions.Hosting.Internal.Host[1]
Hosting starting
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://localhost:5000
这两条日志的分类是项目名称 ASPNETCoreFeatures ,因为 ILogger 是属于 WebApplication 类的。我们也可以直接在主入口创建自定义日志分类。
Listing 14-17 修改 Program.cs ,创建自定义日志分类
using ASPNETCoreFeatures.Endpoints;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var logger = app.Services.GetRequiredService<ILoggerFactory>()
.CreateLogger("Coffee");
logger.LogDebug("Starting the pipeline configuration ... ");
app.MapGet("coffee/{type?}", Coffee.GetCoffee);
app.MapGet("/", () => "Hello World!");
logger.LogDebug("Pipeline configuration is done.");
app.Run();
我们使用 ILoggerFactory 接口来创建自定义日志分类 Coffee 。运行应用,终端输出类似内容:
dbug: Coffee[0]
Starting the pipeline configuration ...
dbug: Coffee[0]
Pipeline configuration is done.
dbug: Microsoft.Extensions.Hosting.Internal.Host[1]
Hosting starting
日志文件的分类改为了 Coffee 。
HTTP 请求和响应日志
ASP.NET Core
内建了一个记录 HTTP 日志的中间件,可以显示管道中详细的请求和响应信息。不过,在开发中,我们可能会更自然的查看浏览器的 Inspect 开发信息,更有条理也更详细。
Listing 14-18 在 appsettings.Development.json 文件添加 HttpLogging 入口
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft.AspNetCore": "Warning",
"Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware": "Information"
}
}
}
在主入口添加 HTTP Logging 服务。
Listing 14-19 在 Program.cs 中取消 logger,添加 HttpLogging 服务
using ASPNETCoreFeatures.Endpoints;
using Microsoft.AspNetCore.HttpLogging;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpLogging(static options => {
options.LoggingFields = HttpLoggingFields.RequestProperties
| HttpLoggingFields.ResponsePropertiesAndHeaders
| HttpLoggingFields.ResponseStatusCode;
});
var app = builder.Build();
app.UseHttpLogging();
app.MapGet("coffee/{type?}", Coffee.GetCoffee);
app.MapGet("/", () => "Hello World!");
app.Run();
运行应用,输入应用地址加 /coffee/latte ,终端窗口将显示 Http 日志:
info: Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware[1]
Request:
Protocol: HTTP/1.1
Method: GET
Scheme: http
PathBase:
Path: /coffee/latte
dbug: ASPNETCoreFeatures.Endpoints.Coffee[0]
Getting coffee for /coffee/latte
dbug: ASPNETCoreFeatures.Endpoints.Coffee[0]
Coffee is ready from /coffee/latte
info: Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware[2]
Response:
StatusCode: 200
Date: Sat, 18 Jan 2025 12:28:19 GMT
Server: Kestrel
Transfer-Encoding: chunked
适用静态内容
大部分的应用都会是静态内容和动态内容的一个混合体。动态内容基本上会和后端服务已经数据发生交互后,在客户端更新内容。静态内容会是一些常用的,内容保持不变的轻量级网页。ASP.NET Core
的 Web 应用里,按约定,静态内容会保持在 wwwroot 文件夹下。dotnet new web 模板没有添加这个文件夹,我们可以手动添加一个 wwwroot 文件夹。
Listing 14-20 在 wwwroot 文件夹里添加一个 Index.html 文件
<!DOCTYPE html>
<head>
<title>Index-wwwroot</title>
</head>
<body>
<h3>Hello World!</h3>
</body>
</html>
为了显示静态内容,我们需要在主入口添加静态内容中间件。
Listing 14-21 在 Program.cs 文件中添加静态内容中间件
...
app.UseHttpLogging();
app.UseStaticFiles();
app.MapGet("coffee/{type?}", Coffee.GetCoffee);
...
运行应用,主地址里得到的是普通的文本 Hello World! ,如果在主地址后面添加 /index.html ,会显示一个标题 H3 的 Hello World! 。
静态内容的配置可以通过传入一个 StaticfileOptions 对象给 UseStaticFiles 方法来进行设置。最常用的配置属性是 FileProvider 和 RequestPath ,前者指定提供静态内容的物理地址,后者指定 Http 请求中的静态内容的路径。
管理客户端软件包
在 ASP.NET Core
项目中,客户端包用于将JavaScript、CSS、字体或其他资源添加到应用程序中。有效管理这些包可确保开发过程顺利进行。如何在 ASP.NET Core
应用程序中使用和管理客户端软件包? 我们现在有下面几个实践较好的选择。
使用 LibMan(库管理者)
微软提供了一个和 Visual Studio 整合的轻量级包管理工具 LibMan ,我们也可以在终端命令行使用它。一般说来,这个工具是首选:
- 简单:快速方便,是为
ASP.NET Core
定制 - 轻量:不需要额外的依赖和配置
- 按需使用:随时安装和删除软件包
我们需要先用终端命令行来安装。
Listing 14-22 一般是全局安装这个工具
dotnet tool uninstall --global Microsoft.Web.LibraryManager.Cli
dotnet tool install --global Microsoft.Web.LibraryManager.Cli
我们没有指定版本,默认安装最新版。
我们在终端里,转到项目文件夹,执行下面的命令初始化 LibMan 工具。
Listing 14-23 初始化 LibMan
libman init -p cdnjs
-p 选项指定提供者是 cdnjs。这个命令会创建一个 Json 文件:
{
"version": "1.0",
"defaultProvider": "cdnjs",
"libraries": []
}
当我们安装或者移除包时,这个文件内容会自动被修改。最好不要手动修改它。如果客户端包有缺失,可以运行命令 libman restore
,就会按 Json 文件的设置自动恢复软件包。
安装客户端软件包
我们先安装一个 Bootstrap 包:
libman install bootstrap -d wwwroot/lib/bootstrap
-d 选项是指定软件包的目的文件夹。安装完成后,如果我们查看 libman.json ,将会看到在 libraries 里自动被添加了一个 bootstrap 库的入口。
要使用 bootstrap ,我们可以在静态文件 Index.html 里添加引用链接。
Listing14-23 在 wwwroot 文件夹里的 Index.html 文件添加 bootstrap
<!DOCTYPE html>
<head>
<link rel="stylesheet" href="/lib/bootstrap/css/bootstrap.min.css" />
<title>Index-wwwroot</title>
</head>
<body>
<h3 class="bg-primary text-white text-center">Hello World!</h3>
</body>
</html>
修改后,这个静态文件将会在客户端显示一条蓝底居中白字。
使用 npm(Node Package Manager)
npm 是最流行的功能超强的 JavaScript 库管理工具。npm 使用超级广泛,被绝大部分软件环境支持;是整合 ASP.NET Core
和现代前端框架(React,Angular,Vue 等)的必备工具;高效管理包的依赖和版本。
在终端检查 node 版本
node -v
如果没有正确显示版本,请去 Nodejs官网 安装。
我们先移除 LibMan 安装的 bootstrap 。
libman uninstall bootstrap
我们先用命令行在 wwwroot 文件夹下新建一个 lib 文件夹。
mdkir wwwroot/lib
在终端,改变当前文件夹至 wwwwroot/lib 。运行 npm 安装命令:
npm i bootstrap
npm 在 lib 文件夹里自动新建 node_modules 文件夹和 package.json 以及 package-lock.json 文件。修改 Index.html 文件的 bootstrap 引用为:
<link rel="stylesheet" href="/lib/node_modules/bootstrap/dist/css/bootstrap.min.css" />
使用 CDN
对于一些小型应用,直接使用 CDN 是合适的选择。
在一些复杂的应用系统结构里,甚至应该考虑使用 webpack 或者 gulp 等自动任务工具来管理应用程序的资源。
对于客户端软件包管理,要遵循以下原则:
- 依赖包最小化:只安装必要的库
- 版本控制:锁定库版本防止不必要的更新
- 组合和最小化资源:达到减少 HTTP 请求和提升载入时间的目的
- 监控表现:测试和监控客户端软件包的表现,纠错和提高效率
使用 Cookies
Cookie 是网络应用程序在用户浏览器中存储用户特定信息的一种方式;它 是客户端浏览器存储的小段数据,每次 HTTP 请求都会发送回服务器。ASP.NET Core
通过组件的 HttpRequest 和 HttpResponse 对象提供 Cookie 支持。
Cookie 主要用于保存用户信息(如用户偏好、身份验证令牌),以及在无状态 HTTP 通信中维护会话状态。
我们基本上使用两类 Cookie:
- 会话 Cookie : 此类 Cookie 存储在客户端内存里,随着浏览器关闭会失效。
- 持久 Cookie : 此类 Cookie 存储在客户端的磁盘上,在规定的有效期过后失效。
设置 Cookies
我们试着用 HttpContext 的 API 给应用添加一个 Cookie 。
Listing 14-24 修改 Program.cs 文件,添加 Cookie
...
app.MapGet("/", () => "Hello World!");
app.MapGet("/set-cookie", (HttpContext context) => {
context.Response.Cookies.Append(
"MyCookie", // Cookie name
"MyValue", // Cookie value
new CookieOptions
{
HttpOnly = true, // This cookie is not accessible via JavaScript
Secure = true, // This cookie is only sent to the server over HTTPS
SameSite = SameSiteMode.Strict, // This cookie is not sent on cross-site requests
Expires = DateTimeOffset.Now.AddMinutes(10) // This cookie expires in 10 minutes
}
);
return "Cookie is set";
});
app.Run();
我们在 Append 方法设置 Cookie 名字,值和配置选项。接下来要用 dotnet run -lp https 运行应用,因为我们设置了 Secure = true 的选项;在浏览器(我是用 Microsoft Edge)地址栏输入 https://localhost:5001/set-cookie
,网页会显示 Cookie is set
文本。
想要查看 Cookie 是否正确设置,我们可以在页面点击鼠标右键,选择检查,再从 tab 栏里选择”应用”(Application);然后点击左边的 Storage 下面的 Cookies 项目,就可以看到右边表格更新了,显示”MyCookie“条目:
Figure 14-1
读取 Cookies
我们用 TryGetValue 方法读取 Cookie :
Listing 14-25 在 Program.cs 里添加读取 Cookie 的方法
...
app.MapGet("/set-cookie", (HttpContext context) => {
context.Response.Cookies.Append(
"MyCookie", // Cookie name
"MyValue", // Cookie value
new CookieOptions
{
HttpOnly = true, // This cookie is not accessible via JavaScript
Secure = true, // This cookie is only sent to the server over HTTPS
SameSite = SameSiteMode.Strict, // This cookie is not sent on cross-site requests
Expires = DateTimeOffset.Now.AddMinutes(10) // This cookie expires in 10 minutes
}
);
return "Cookie is set";
});
app.MapGet("/get-cookie", (HttpContext context) => {
var cookieValue = context.Request.Cookies.TryGetValue("MyCookie", out var value)
? value
: "not found";
return $"Cookie value is {cookieValue}";
});
app.Run();
删除 Cookies
我们当然不能在后台服务里删除 Cookies ,实际上,Delete 方法是发送一个过期的 Cookie 版本给客户端,来触发客户端移除 Cookie 。
Listing 14-26 在 Program.cs 里添加清除 Cookie 方法
...
app.MapGet("/clear-cookie", (HttpContext context) => {
context.Response.Cookies.Delete("MyCookie");
return "Cookie is cleared";
});
...
Cookie 验证
Cookies 最广泛的应用是用于验证。在 ASP.NET Core
里,我们可以用 Cookie 验证中间件来处理验证令牌(token)。
我们给主入口里的 builder 添加验证服务。
Listing 14-27 *修改 Program.cs 添加 Cookie 验证服务和 POST 方法。
using System.Security.Claims;
using ASPNETCoreFeatures.Endpoints;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.HttpLogging;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpLogging(static options => {
options.LoggingFields = HttpLoggingFields.RequestProperties
| HttpLoggingFields.ResponsePropertiesAndHeaders
| HttpLoggingFields.ResponseStatusCode;
});
builder.Services.AddAuthentication("CookieAuth")
.AddCookie("CookieAuth", options =>
{
options.LoginPath = "/login"; // Redirect to /login if unauthorized
options.ExpireTimeSpan = TimeSpan.FromMinutes(30); // Expire after 30 minutes
options.SlidingExpiration = true; // Reset expiration time after each request
});
var app = builder.Build();
...
app.MapGet("/clear-cookie", (HttpContext context) => {
context.Response.Cookies.Delete("MyCookie");
return "Cookie is cleared";
});
app.MapPost("/login", async (HttpContext context) => {
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, "Alice"),
new Claim(ClaimTypes.Role, "Admin")
};
var claimsIdentity = new ClaimsIdentity(claims, "CookieAuth");
var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
await context.SignInAsync("CookieAuth", claimsPrincipal);
return Results.Ok("Logged in");
});
app.Run();
在一个终端(powershell)运行应用,再打开新的终端(powershell)里执行 Curl 命令:
curl -X POST http://localhost:5000/login
新终端会显示响应 “Logged in”;而在运行应用的终端会显示类似日志:
info: Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware[1]
Request:
Protocol: HTTP/1.1
Method: POST
Scheme: http
PathBase:
Path: /login
info: Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware[2]
Response:
StatusCode: 200
Content-Type: application/json; charset=utf-8
Date: Sat, 18 Jan 2025 21:06:44 GMT
Server: Kestrel
Cache-Control: [Redacted]
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Pragma: [Redacted]
Set-Cookie: [Redacted]
Transfer-Encoding: chunked
由于是 POST 请求,用 Curl 或者 Postman 等工具来测试比较方便,用 Powershell 的 Invoke-WebRequest 也行,只是不太流行而已。
Cookie 政策中间件
ASP.NET Core
包含 Cookie 政策设置的一个中间件:
Listing 14-28 在 Program.cs 中增加 Cookie 中间件
...
var app = builder.Build();
app.UseCookiePolicy(new CookiePolicyOptions
{
MinimumSameSitePolicy = SameSiteMode.Strict,
HttpOnly = HttpOnlyPolicy.Always,
Secure = CookieSecurePolicy.Always
});
app.UseHttpLogging();
app.UseStaticFiles();
app.MapGet("coffee/{type?}", Coffee.GetCoffee);
app.MapGet("/", () => "Hello World!");
app.MapGet("/set-cookie", (HttpContext context) => {
context.Response.Cookies.Append(
"MyCookie", // Cookie name
"MyValue", // Cookie value
new CookieOptions
{
// HttpOnly = true, // This cookie is not accessible via JavaScript
// Secure = true, // This cookie is only sent to the server over HTTPS
// SameSite = SameSiteMode.Strict, // This cookie is not sent on cross-site requests
Expires = DateTimeOffset.Now.AddMinutes(10) // This cookie expires in 10 minutes
}
);
return "Cookie is set";
});
...
设置了政策以后,就可以把 /set-cookie 方法里的三行注释掉。
使用会话 Sessions
在 ASP.NET Core
中使用会话是在用户在服务器端存储数据的常用方法。这些数据可在多个 HTTP 请求中使用,对于维护用户特定信息(如登录凭据、购物车数据或临时设置)特别有用。
会话允许我们在服务器端存储和检索特定于用户的数据,同时将其与唯一标识符(会话 ID)相关联。会话 ID 通常存储在 cookie 中,或在请求 URL 中传递。
会话的优点
- 易于用于短期数据存储。
- 客户端不能直接存取数据,与客户端存储(如 cookie 或 localStorage)相比,安全性更高。
- 数据可在用户会话的多个请求中使用。
使用会话的场合
- 存储临时用户特定数据(如用户偏好、临时设置或购物车)。
- 用于不需要在用户会话之后持续存在的数据。
- 不想在客户端和服务器之间来回发送敏感数据时。
配置会话服务和中间件
我们先在主入口 Program.cs 里添加会话服务和中间件:
Listing 14-29 修改 Program.cs ,添加会话
...
builder.Services.AddAuthentication("CookieAuth")
.AddCookie("CookieAuth", options =>
{
options.LoginPath = "/login"; // Redirect to /login if unauthorized
options.ExpireTimeSpan = TimeSpan.FromMinutes(30); // Expire after 30 minutes
options.SlidingExpiration = true; // Reset expiration time after each request
});
builder.Services.AddDistributedMemoryCache();
builder.Services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromMinutes(30);
options.Cookie.IsEssential = true;
});
var app = builder.Build();
app.UseSession();
app.UseCookiePolicy(new CookiePolicyOptions
{
MinimumSameSitePolicy = SameSiteMode.Strict,
HttpOnly = HttpOnlyPolicy.Always,
Secure = CookieSecurePolicy.Always
});
...
当我们使用会话时,我们要决定如何存储会话数据。ASP.NET Core
提供了三种选项来存储会话数据,要根据实际情况选择。
名字 | 描述 |
---|---|
AddDistributedMemoryCache | 用于设置内存缓存。名字是分布式,但这个缓存只负责为创建它的应用实例存储数据。 |
AddDistributedSqlServerCache | 该方法设置缓存,用于在 SQL Server 中存储数据,应用必须添加 Microsoft.Extensions.Caching.SqlServer 软件包。 |
AddStackExchangeRedisCache | 此方法用于设置 Redis 缓存,在安装 Microsoft.Caching.Redis 软件包后可用。 |
前面表格总结过,AddDistributedMemoryCache 方法创建的缓存服务不是分布式的,而是为 ASP.NET Core
运行时的单个实例存储会话数据。如果我们通过部署多个运行时实例来扩展应用程序,则应使用其他缓存,如 SQL Server 缓存。
会话配置设置里的 Cookie.IsEssential = ture 属性是指定应用必须强制需要 Cookie 才能运行。它也有类似 Cookie 配置的 HttpOnly 和 SecurityPolicy 的属性。
使用会话的数据
会话数据以 键-值 的方式保持,ASP.NET Core
会话中间件提供了许多属性和扩展方法,方便我们使用会话的数据:
- Clear(): 该方法删除会话中的所有数据。
- CommitAsync(): 此异步方法会将已更改的会话数据提交到缓存中。
- GetString(key): 该方法使用指定的键检索字符串值。
- GetInt32(key): 该方法使用指定的键检索整数值。
- Id: 该属性返回会话的唯一标识符。
- IsAvailable: 此属性在会话数据已加载时返回 true。
- Keys: 该属性枚举会话数据项的键。
- Remove(key): 该方法删除与指定键相关联的值。
- SetString(key, val): 该方法使用指定的键存储字符串。
- SetInt32(key, val): 该方法使用指定的键存储一个整数。
我们试着使用其中的方法来访问会话。在这之前先准备一个演示数据类:
Listing 14-30 在项目文件夹下新建文件夹 Session ,在其中新建文件 Persion.cs
namespace ASPNETCoreFeatures.Session
{
public class Person
{
public string? Name { get; set; }
public int Age { get; set; }
public string? Email { get; set; }
}
}
在主入口里添加会话服务:
Listing 14-31 在 Program.cs 里添加会话服务和端点方法
...
builder.Services.AddAuthentication("CookieAuth")
.AddCookie("CookieAuth", options =>
{
options.LoginPath = "/login"; // Redirect to /login if unauthorized
options.ExpireTimeSpan = TimeSpan.FromMinutes(30); // Expire after 30 minutes
options.SlidingExpiration = true; // Reset expiration time after each request
});
builder.Services.AddDistributedMemoryCache();
builder.Services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromMinutes(30);
options.Cookie.IsEssential = true; // Make the session cookie essential
});
var app = builder.Build();
app.UseSession();
app.UseCookiePolicy(new CookiePolicyOptions
{
MinimumSameSitePolicy = SameSiteMode.Strict,
HttpOnly = HttpOnlyPolicy.Always,
Secure = CookieSecurePolicy.Always
});
...
app.MapGet("/set-session", (HttpContext context) => {
context.Session.SetString("Name", "Alice");
context.Session.SetInt32("Age", 25);
return "Session is set";
});
app.MapGet("/get-session", (HttpContext context) => {
var name = context.Session.GetString("Name") ?? "not found";
var age = context.Session.GetInt32("Age") ?? 0;
return $"Name: {name}, Age: {age}";
});
app.MapGet("/clear-session", (HttpContext context) => {
context.Session.Clear();
return "Session is cleared";
});
app.MapGet("/set-object-session", (HttpContext context) => {
var person = new Person { Name = "Alice", Email = "alice@example.com" };
context.Session.SetString("Person", JsonSerializer.Serialize(person));
return "Object session is set";
});
app.MapGet("/get-object-session", (HttpContext context) => {
var person = context.Session.GetString("Person");
var personObj = person != null
? JsonSerializer.Deserialize<Person>(person)
: null;
return personObj != null
? $"Name: {personObj.Name}, Email: {personObj.Email}"
: "Person object not found";
});
app.Run();
请记得要在文件开头添加引用:
using System.Text.Json;
using ASPNETCoreFeatures.Session;
如果运行应用后,在前面的会话端点没有成功显示预定信息,除了代码错误的原因,很可能是浏览器 Cookie 功能没有开启。
异常和错误处理
在开发过程中,我们希望发生异常和错误时,系统能尽可能多的有效信息,方便纠错和调试。然而,在生产版本里,出于安全性考虑,我们不希望异常和错误信息泄露给其他人;通常,我们会显示友好的回应给用户,以免造成用户担忧。
Listing 14-31 更改 Properties 文件夹里的 launchSettings.json 里的环境配置
...
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:5001;http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Production"
}
},
...
Listing 14-32 在 Program.cs 里添加返回静态错误页面的异常处理服务
...
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/error.html");
}
app.UseSession();
...
Listing 14-33 在 wwwroot 文件夹下新建 error.html 静态文件
<!DOCTYPE html>
<head>
<link rel="stylesheet" href="/lib/bootstrap/css/bootstrap.min.css" />
<title>Error!</title>
</head>
<body class="text-center">
<h3 class="p-3">Something wrong occured ...</h3>
<p class="text-danger">Please contact admin!</p>
</body>
</html>
我们现在给主入口添加一个人为的抛出异常。
Listing 14-34 在 Program.cs 文件添加抛出异常
...
app.Run(context =>
{
throw new Exception("Some error occurred");
});
app.Run();
用 dotnet run -lp https
运行应用,在浏览器里输入 https://localhost:5001
,就会显示错误静态页面。
我们这里只是对异常做了最初步的处理,在实际应用中,要加上更多的信息和链接等,帮助用户恢复到相对正常的应用路径或者初始状态。
发表回复