基于策略和资源的授权机制

身份认证完成后 HttpContext.User 中包含了用户的身份信息,但是用户的身份信息并不包含用户的权限信息。用户的权限信息需要通过授权系统来获取。 ASP.NET Core 中的授权系统 ASP.NET Core 中的授权系统是基于策略的授权系统,可以通过声明式的方式来定义授权策略。授权策略可以基于角色,也可以基于资源,也可以基于其他的条件。授权策略可以通过声明式的方式来定义,也可以通过代码的方式来定义。 使用 IAuthorizationService 接口 IAuthorizationService 接口是 ASP.NET Core 中的授权服务接口,可以通过该接口来进行授权操作。IAuthorizationService 接口提供了多个授权方法,可以通过这些方法来进行授权操作。 public interface IAuthorizationService { Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, IEnumerable<IAuthorizationRequirement> requirements); Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, string policyName); Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, AuthorizationPolicy policy); } AuthorizationResult 是授权结果,包含了授权的结果和失败的原因。 实现授权策略提供者 使用 OperationAuthorizationRequirement 表示授权条件,将上一期视频中的权限定义转成授权条件提供给系统,一个策略中本可以有多个条件,但为了简单这里的策略中只加一个条件。 需要注意的是,多个策略是 OR 的关系,多个条件是 AND 的关系。 public class CustomAuthorizationPolicyProvider(IOptions<AuthorizationOptions> options, IPermissionDefinitionManager permissionDefinitionManager) : DefaultAuthorizationPolicyProvider(options) 实现资源表示 public interface IAuthorizationResource { string ResourceType => GetType().Name; string ResourceId => GetType().GetProperty("Id")?.GetValue(this)?.ToString() ?? throw new NotImplementedException(); } 设计一个表示资源的记录类 一般来说,实体类型就是一种资源,可以直接从 IAuthorizationResource 接口实现,但为了方便表示资源,可以设计一个表示资源的记录类。 public record struct ResourceInfo : IAuthorizationResource 实现授权条件处理器 public class PermissionRequirementHandler(IPermissionChecker permissionChecker) : AuthorizationHandler<OperationAuthorizationRequirement> public class ResourcePermissionRequirementHandler(IPermissionChecker permissionChecker) : AuthorizationHandler<OperationAuthorizationRequirement, IAuthorizationResource> protected abstract Task HandleRequirementAsync(AuthorizationHandlerContext context, TRequirement requirement); protected abstract Task HandleRequirementAsync(AuthorizationHandlerContext context, TRequirement requirement, TResource resource); 在授权处理器中使用 IPermissionChecker public class PermissionRequirementHandler(IPermissionChecker permissionChecker) : AuthorizationHandler<OperationAuthorizationRequirement> { private readonly IPermissionChecker _permissionChecker; public PermissionRequirementHandler(IPermissionChecker permissionChecker) { _permissionChecker = permissionChecker; } protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, OperationAuthorizationRequirement requirement) { if (await _permissionChecker.IsGrantedAsync(context.User, requirement.Name)) { context.Succeed(requirement); } } }

2026年3月10日

基于资源授权的最佳实践

在 Identity Service 中提供权限检查接口 [HttpHead] public async Task<IActionResult> CheckPermission(string permissionName, string? resourceType = null, string? resourceId = null { if (await permissionChecker.IsGrantedAsync(permissionName, resourceType, resourceId)) { return Ok(); } return Forbid(); } 远程权限检查器 Dictionary<string, string?> parameters = new() { [nameof(name)] = name, [nameof(resourceType)] = resourceType, [nameof(resourceId)] = resourceId }; string queryString = QueryHelpers.AddQueryString(string.Empty, parameters); HttpRequestMessage request = new(HttpMethod.Head, queryString); using HttpResponseMessage response = await httpClient.SendAsync(request); return response.IsSuccessStatusCode; 重新实现权限处理程序 public class PermissionRequirementHandler(IPermissionChecker permissionChecker) : AuthorizationHandler<OperationAuthorizationRequirement> { protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, OperationAuthorizationRequirement requirement) { if (context.Resource is IAuthorizationResource resource) { if (await permissionChecker.IsGrantedAsync(context.User, requirement.Name, resource.ResourceType, resource.ResourceId)) { context.Succeed(requirement); } else { context.Fail(); } return; } if (await permissionChecker.IsGrantedAsync(context.User, requirement.Name)) { context.Succeed(requirement); return; } context.Fail(); } } 取消缓存以便测试 await distributedCache.SetObjectAsync(cacheKey, new PermissionGrantCacheItem(isGranted), new DistributedCacheEntryOptions { AbsoluteExpiration = DateTimeOffset.Now }); 可读性重构 重新生成演示数据,并将授权中的 Name 改为 PermissionName,具有更强的可读性。 ...

2026年3月10日

聚合 API 网关

使用网关可将多个单独请求聚合成一个请求。 当客户端必须向不同的后端系统发出多个调用来执行某项操作时,此模式非常有用。 上下文和问题 在某些情况下,客户端需要向多个后端系统发出多个请求。 例如,客户端可能需要从多个服务中检索数据,然后将这些数据聚合到一个响应中。 在这种情况下,客户端必须发出多个请求,这可能会导致性能问题。 此外,客户端还必须处理多个响应,这可能会导致复杂性问题。 解决方案 使用网关减少客户端与服务之间的通信频率。 网关会接收客户端请求,将请求分派到不同的后端系统,然后聚合结果并将其返回给请求客户端。此模式可以减少应用程序向后端服务发出的请求数,并通过高延迟网络改进应用程序的性能。 权限聚合 public interface IPermissionService { Task<IReadOnlyList<PermissionGroupDefinitionResponse>> GetAllPermissionDefinitionsAsync(CancellationToken cancellationToken=default); } 重构用户和角色控制器 修改 UsersController 从 IdentityServiceDbContext 更改为 UserManager<User> 实现 RolesController 使用 RoleManager<Role> UserCreateRequestValidator 从 IdentityServiceDbContext 更改为 UserManager<User> 权限定义端点返回的 Json 中 name 属性更改为 groupName 远程权限检查 BUG 修复 public async Task<IActionResult> CheckPermission(int roleId, string permissionName, string? resourceType = null, string? resourceId = null) 注意事项 网关不应在后端服务之间造成服务耦合。 网关应靠近后端服务,以尽量降低延迟。 网关服务可能会造成单一故障点。 请确保网关设计合理,符合应用程序的可用性要求。 网关可能造成瓶颈。 请确保网关可提供足够的性能来处理负载,并可根据预期的发展进行缩放。 对网关执行负载测试,确保不会对服务造成连锁故障。 使用隔舱、断路、重试和超时等技术实施弹性设计。 如果一个或多个服务调用花费的时间过长,那么超时并返回部分数据集是可以接受的。 请考虑应用程序处理这种情况的方式。 使用异步 I/O 来确保后端延迟不会导致应用程序中出现性能问题。 使用关联 ID 实施分布式跟踪,以跟踪每个调用。 监视请求指标和响应大小。 考虑返回缓存的数据(作为故障转移策略)来处理故障。 不要在网关中内置聚合,而应考虑将聚合服务放在网关后面。 请求聚合的资源要求可能与网关中其他服务不同,并可能影响网关的路由和卸载功能。

2026年3月10日

聚合 OpenApi 文档

每个微服务都有自己的 OpenApi 文档,但是在实际开发中,我们更希望能够将所有微服务的 OpenApi 文档聚合到一起,以便于查看和调试,微服务都是基于 Aspire 框架开发的,所以我们可以使用 Aspire 框架提供的服务发现功能来自动聚合所有微服务的 OpenApi 文档。 使用 Aspire 服务发现自动配置 OpenApi 文档 public class OpenApiConfigureOptions() : IConfigureOptions<SwaggerUIOptions> builder.Services.AddTransient<IConfigureOptions<SwaggerUIOptions>, OpenApiConfigureOptions>(); 定制 OpenApi 文档样式 在 Resource/OpenApi 文件夹下创建 Custom.css 文件。 .swagger-ui .topbar-wrapper img { content: url('https://test.com/logo.svg'); } .swagger-ui .topbar-wrapper .link::after { margin-left: 0.5rem; content: "HelloWorld"; } 关联 CSS 文件到 OpenApi 文档 public static class OpenApiExtensions { public static IServiceCollection AddOpenApi(this IServiceCollection) { services.Configure<SwaggerUIOptions>(options => { options.DocumentTitle = Assembly.GetExecutingAssembly().GetName().Name; options.InjectStylesheet("/ServiceDefaults/Resources/OpenApi/Custom.css"); }); } } public static IApplicationBuilder UseOpenApi(this IApplicationBuilder app) { // Configure the HTTP request pipeline. app.UseSwagger(apiConfigureOptions) app.Map("/ServiceDefaults", appBuilder => appBuilder.UseStaticFiles(new StaticFileOptions { FileProvider = new EmbeddedFileProvider(Assembly.GetExecutingAssembly()) })) return app; }

2026年3月10日

零度框架升级到 Aspire 13.0 版本

简化解决方案文件格式 从 sln 到 slnx,新的 slnx 格式更易于阅读和维护,减少了冗余信息,使得项目结构更加清晰。 dotnet sln migrate 同时修改 slnf 中引用的 sln 文件为 slnx 文件。 将项目目标框架升级到 .NET 10 版本 在项目文件中将目标框架从 net9.0 修改为 net10.0: <TargetFramework>net10.0</TargetFramework> 将 Aspire 升级到 13.0.0 版本 在项目文件中将 Aspire 包的版本号更新为 13.0.0 - <Project Sdk="Microsoft.NET.Sdk"> + <Project Sdk="Aspire.AppHost.Sdk/13.0.0"> - <Sdk Name="Aspire.AppHost.Sdk" Version="9.5.2" /> <PropertyGroup> <OutputType>Exe</OutputType> - <TargetFramework>net9.0</TargetFramework> + <TargetFramework>net10.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> - <IsAspireHost>true</IsAspireHost> <UserSecretsId>0afc20a6-cd99-4bf7-aae1-1359b0d45189</UserSecretsId> </PropertyGroup> <ItemGroup> - <PackageReference Include="Aspire.Hosting.AppHost" Version="9.5.2" /> + <!-- Aspire.Hosting.AppHost is now included automatically by the SDK --> </ItemGroup> </Project> 将中央包 Directory.Packages.props 中的包更到最新 列出所有需要更新的包 ...

2026年3月10日

零度框架升级到 Aspire 9.0 版本

升级 Visual Studio 开发工具 使用 Visual Studio Installer 升级,升级到 v17.12 后,自动安装 .NET 9.0 SDK 和 Aspire 9.0 工作负载。 非 Visual Studio 开发环境 安装 .NET Core SDK 9.0 版本 安装最新 .NET SDK 9.0 版 查看 .NET SDK 版本: dotnet --version dotnet --list-sdks 升级 Aspire 工作负载 在项目目录下执行以下命令: dotnet workload uninstall aspire dotnet workload install aspire dotnet workload list dotnet workload update 升级项目文件 Host 项目添加新节点: <Sdk Name="Aspire.AppHost.Sdk" Version="9.0.0" /> 所有项目文件的目标框架改为 net9.0: <TargetFramework>net9.0</TargetFramework> 安装 EF Core 基础包 ...

2026年3月10日

零度框架升级到 Aspire 9.3 版本

从 Host 项目删除 IsAspireHost 属性 <IsAspireHost>true</IsAspireHost> 工作负载升级 dotnet workload update 升级 Visual Studio 到最新版 通过 Visual Studio Installer 升级到最新版本。 可使用 AI 升级项目 GitHub Copilot 应用现代化 - 适用于 .NET 的升级是一个功能强大的 Visual Studio 扩展,可与你配合使用,将项目升级到较新版本的 .NET、升级依赖项并应用代码修复。 GitHub Copilot 应用程序现代化作为 Visual Studio 扩展分发,是一个交互式升级过程。 GitHub Copilot app modernization - upgrade for .NET 更新项目模板 dotnet new update 使用升级助手将项目升级到最新 dotnet tool install -g UpgradeAssistant dotnet upgrade-assistant upgrade 也可以使用 Visual Studio 的升级助手。 关于 CPM 中央包管理的升级 CPM 中央包管理的升级是一个重要的更新,允许您在项目中使用中央包版本管理,升级 CPM 目前需要手动更新。 ...

2026年3月10日

零度框架中的测试

使用自动测试是确保应用程序代码按作者期望执行操作的一种绝佳方式。 零度框架中提供单元测试、集成测试和负载测试,.NET 平台上的测试框架有 xUnit、NUnit 和 MSTest,用的最多的是 xUnit,无论使用任何一种测试框架,都可以通过命令行或者 IDE 来运行测试。 单元测试 单元测试是一种试验单个软件组件或方法(也称为“工作单元”)的测试。 单元测试仅应测试开发人员控件内的代码。 它们不测试基础结构问题。 基础结构问题包括与数据库、文件系统和网络资源的交互,零度框架中的单元测试使用 xUnit 框架, 项目命名规范: <ProjectName>.UnitTests。 public class Calculator { public int Add(int a, int b) { return a + b; } } public class CalculatorTests { [Fact] public void Add_WhenCalled_ReturnsTheSumOfArguments() { // Arrange var calculator = new Calculator(); // Act var result = calculator.Add(1, 2); // Assert Assert.Equal(3, result); } } 单元测试最佳做法 https://learn.microsoft.com/zh-cn/dotnet/core/testing/unit-testing-best-practices 集成测试 集成测试也称为功能测试,与单元测试的不同之处在于,它试验两个或更多软件组件一同工作集成能力。这些测试在更广泛范围的受测系统上运行,而单元测试则侧重于单个组件函数,通常,集成测试会包括对基础结构问题的测试,例如数据库、文件系统和网络资源的交互。 项目命名规范: <ProjectName>.FunctionalTests。 public class Controller: ControllerBase { private readonly IService _service; public Controller(IService service) { _service = service; } [HttpGet] public async Task<IActionResult> Get(int id) { var result = await _service.Get(id); return Ok(result); } } public class Service: IService { public async Task<Model> Get(int id) { return await _repository.Get(id); } } public class ServiceTest: IClassFixture<WebApplicationFactory<Startup>> { private readonly WebApplicationFactory<Startup> _factory; public ServiceTest(WebApplicationFactory<Startup> factory) { _factory = factory; } [Fact] public async Task Get_WhenCalled_ReturnsModel() { // Arrange var client = _factory.CreateClient(); var response = await client.GetAsync("/api/controller/1"); response.EnsureSuccessStatusCode(); var model = await response.Content.ReadAsAsync<Model>(); // Assert Assert.NotNull(model); } }

2026年3月10日

模型自动验证机制

模型验证是 ASP.NET Core MVC 中的一个重要特性,它可以帮助我们验证用户输入的数据是否符合预期。 基于数据注解的验证 参考微软 数据注解 文档。 public class User { [Required] [StringLength(32)] public string Name { get; set; } [Required] [EmailAddress] public string Email { get; set; } } 基于链式调用的验证 FluentValidation 是一个.NET库,用于构建类型安全的验证规则。它的设计目标是提供一个简单、清晰的API,同时还能够支持复杂的验证规则。 参考 FluentValidation 官方文档。 dotnet add package FluentValidation.AspNetCore 实现验证器 public class UserValidator : AbstractValidator<User> { public UserValidator() { RuleFor(x => x.Name).NotEmpty().MaximumLength(32); RuleFor(x => x.Email).NotEmpty().EmailAddress(); } } 自动依赖注入 services.AddValidatorsFromAssembly(assembly).AddFluentValidationAutoValidation(); ValidatorOptions.Global.LanguageManager = new CustomFluentValidationLanguageManager(); 自定义验证错误消息 public class UserValidator : AbstractValidator<User> { public UserValidator() { RuleFor(x => x.Name).NotEmpty().MaximumLength(32).WithMessage("Name is required and must be less than 32 characters."); RuleFor(x => x.Email).NotEmpty().EmailAddress().WithMessage("Email is required and must be a valid email address."); } } 自定义验证逻辑 public class UserCreateRequestValidator : AbstractValidator<UserCreateRequest> { public UserCreateRequestValidator(IdentityDbContext context) { RuleFor(m => m.UserName).NotNull().NotEmpty().Length(5, 20).Matches("^[a-zA-Z]+$"); RuleFor(m => m.PhoneNumber).NotNull().NotEmpty().Length(11).Matches(@"^1\d{10}$").Must((model, phoneNumber) => { return !context.Users.Any(e => e.PhoneNumber == phoneNumber); }); RuleFor(m => m.Password).NotNull().NotEmpty().Length(5, 20); RuleFor(m => m.Email).EmailAddress().Length(5, 50); } } 通用错误消息 public class CustomFluentValidationLanguageManager : FluentValidation.Resources.LanguageManager { public CustomFluentValidationLanguageManager() { AddTranslation("en", "NotNullValidator", "The {PropertyName} field is required."); AddTranslation("en", "MaximumLengthValidator", "The {PropertyName} field must be less than {MaxLength} characters."); AddTranslation("en", "EmailAddressValidator", "The {PropertyName} field must be a valid email address."); } }

2026年3月10日

全球化与本地化

全球化是指 Web 应用程序能够适应不同的文化和地区,而不需要修改代码。本地化是指 Web 应用程序能够根据用户的文化和地区显示不同的内容。全球化和本地化是两个不同的概念,但是经常一起使用。 实现多语言的方法有很多种,可以使用资源文件、数据库、配置文件等方式,本文主要介绍使用资源文件的方式实现多语言,也是微软官方推荐的方式。其它的比如 PO 文件、JSON 文件等也可以实现多语言,但是不如资源文件方便. 创建资源文件 Welcome.en-US.resx Welcome.zh-CN.resx 添加本地化服务和资源定位 public static IServiceCollection AddCustomLocalization(this IServiceCollection services) { services.AddLocalization(options => options.ResourcesPath = "Resources"); return services; } 使用本地化服务 public class HelloWorldController(IStringLocalizerFactory stringLocalizerFactory) : ControllerBase { [HttpGet] public IActionResult Get() { var location = Assembly.GetExecutingAssembly().FullName; ArgumentException.ThrowIfNullOrWhiteSpace(location); var localizer = stringLocalizerFactory.Create("Welcome", location); return Ok(localizer["HelloWorld"].Value); } } 使用 HTTP 请求设置语言 UseRequestLocalization 中间件从请求中获取语言设置,然后设置当前线程的语言,以便在后续的请求中使用,这样就可以实现全局的本地化,而不需要在每个控制器中设置本地化。 public static IApplicationBuilder UseCustomLocalization(this IApplicationBuilder app) { var supportedCultures = new[] { "zh-CN", "en-US" }; var localizationOptions = new RequestLocalizationOptions().SetDefaultCulture(supportedCultures.First()) .AddSupportedCultures(supportedCultures) .AddSupportedUICultures(supportedCultures); app.UseRequestLocalization(localizationOptions); return app; } 使用下面的方式从 Header 、 QueryString 或者 Cookie 中获取语言设置: Accept-Language: en-US http://localhost:5000/?culture=en-Us .AspNetCore.Culture=en-US OpenApi 设置 Accept-Language 以实现多语言 services.Configure<SwaggerGenOptions>(options => options.OperationFilter<AcceptLanguageHeaderOperationFilter>()); public class AcceptLanguageHeaderOperationFilter : IOperationFilter { public void Apply(OpenApiOperation operation, OperationFilterContext context) { var parameter = new OpenApiParameter { Name = HeaderNames.AcceptLanguage, In = ParameterLocation.Header, Required = false, Schema = new OpenApiSchema { Default = new OpenApiString("zh-CN"), Type = "string", Enum = [new OpenApiString("zh-CN"), new OpenApiString("en-US")] } }; operation.Parameters.Add(parameter); } } 实现数据展示的全球化 public class HelloWorldController(IStringLocalizerFactory stringLocalizerFactory) : ControllerBase { [HttpGet("time")] public IActionResult GetTime( { //string amount=88.88.ToString("C"); return Ok(TimeProvider.System.GetUtcNow().ToString()); } } 实现模型和属性的本地化 IStringLocalizerFactory localizerFactory = app.ApplicationServices.GetRequiredService<IStringLocalizerFactory>(); ValidatorOptions.Global.DisplayNameResolver = (type, memberInfo, lambdaExpression) => { string displayName = memberInfo.Name; DisplayAttribute? displayAttribute = memberInfo.GetCustomAttribute<DisplayAttribute>(true); displayName = displayAttribute?.Name ?? displayName; DisplayNameAttribute? displayNameAttribute = memberInfo.GetCustomAttribute<DisplayNameAttribute>(true); displayName = displayNameAttribute?.DisplayName ?? displayName; var localizer = localizerFactory.Create(type); return localizer[displayName]; }; 本地化验证错误消息 public UserCreateRequestValidator(IdentityServiceDbContext dbContext, IStringLocalizer<UserCreateRequest> localizer) { RuleFor(m => m.PhoneNumber).NotNull().NotEmpty().Length(11).Matches(@"^1\d{10}$").Must((model, phoneNumber) => { return !dbContext.Users.Any(e => e.PhoneNumber == phoneNumber); }).WithMessage(localizer["PhoneNumberExists"]); } 微软 Resx 编辑工具 必应搜索 Resx Editor 关键字可以找到很多工具,可以编辑 Resx 文件。 ...

2026年3月10日