在ASP.NET Core中,授权不再是简单的一个特性标签就能搞定的事情。官方推荐的基于策略的授权模型把权限判断拆成了策略定义、要求和处理器三个部分,这种设计给了开发者极大的自由度。不过很多资料只停留在Demo层面,真正要在项目里落地一套自定义授权,还需要理解它的执行机制、生命周期以及组合方式。本文结合实际项目经验,把自定义授权策略的完整实现思路梳理一遍。

基于策略的授权模型是怎么回事
先弄清楚几个概念。在ASP.NET Core中,一个策略代表一组授权要求,每个要求由对应的Handler来判定是否满足。默认的授权中间件在请求进入管线后,会检查当前用户是否通过了指定策略的校验。平时写的[Authorize(Roles = "Admin")]本质上也是被框架转换成一个策略来执行的,角色、声明都会被包装成对应的要求。
策略的定义通常在Program.cs或Startup中完成,通过AddAuthorization方法注册。下面是一个最基础的例子,定义了一个要求用户年龄大于18的策略:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("AdultOnly", policy =>
policy.Requirements.Add(new MinimumAgeRequirement(18)));
});
builder.Services.AddSingleton<IAuthorizationHandler>,
MinimumAgeAuthorizationHandler>();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.Run();
注意这里有一个容易忽略的细节:MinimumAgeRequirement是一个自定义类型,它实现了IAuthorizationRequirement接口。这个接口本身是空的,作用仅仅是打一个标记,让框架能够识别它并找到对应的Handler。策略的判断逻辑完全由Handler承担,Requirement只负责携带参数数据,两者职责分离得非常清晰。
编写Requirement和自定义Handler
Requirement的定义很简单,通常就是一个携带参数的普通类:
public class MinimumAgeRequirement : IAuthorizationRequirement
{
public int MinimumAge { get; }
public MinimumAgeRequirement(int minimumAge)
{
MinimumAge = minimumAge;
}
}
Handler需要继承AuthorizationHandler<T>,在HandleRequirementAsync方法里写判定逻辑。判定成功就调用context.Succeed(requirement),失败时可以调用Fail,也可以什么都不做。这里有个重要的机制:如果一个策略挂了多个Requirement,只有全部Requirement都被标记为Succeed,授权才算通过;而调用Fail会立即终结整个授权流程,直接判定失败。
public class MinimumAgeAuthorizationHandler
: AuthorizationHandler<MinimumAgeRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
MinimumAgeRequirement requirement)
{
// 尝试从用户的Claims中取出出生日期
var birthClaim = context.User.FindFirst(c => c.Type == "birthdate");
if (birthClaim != null && DateTime.TryParse(birthClaim.Value, out var birth))
{
var age = DateTime.Today.Year - birth.Year;
if (birth.Date > DateTime.Today.AddYears(-age)) age--;
if (age >= requirement.MinimumAge)
{
context.Succeed(requirement);
}
}
return Task.CompletedTask;
}
}
关于Handler的注册方式还有一个坑值得说。很多人习惯用AddScoped注册Handler,这本身没问题,但如果Handler的构造函数里注入了DbContext之类的服务,就必须用Scoped或Transient注册,用Singleton会直接报错。反过来,如果Handler是无状态的纯逻辑判断,注册成Singleton性能更好。理解这一点的前提是知道授权服务在请求管线中的生命周期,默认情况下授权是在当前HTTP请求的作用域内执行的。
在控制器和页面中应用策略
策略注册好之后,使用方式非常灵活。最常见的是直接在控制器或Action上标注策略名称:
[Authorize(Policy = "AdultOnly")]
public class OrderController : Controller
{
public IActionResult Create()
{
return View();
}
}
除了特性标注,还可以在Razor Pages中通过@attribute指令应用策略,或者在Program.cs里给整个区域、整个路由模型设置全局授权要求。比如希望所有页面默认都需要登录,可以配置一个全局回退策略:
builder.Services.AddAuthorization(options =>
{
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
});
设置了FallbackPolicy之后,所有没有显式标注AllowAnonymous或具体策略的端点,都会默认要求已认证用户。这在做全站登录校验时特别省事,避免了逐个控制器加特性的繁琐。需要放行的接口,比如健康检查、验证码接口,单独加上[AllowAnonymous]即可。
还有一种更动态的用法是通过编程方式在代码内部调用授权服务,配合IAuthorizationService可以在方法体里手动触发一次授权判断,适合需要在业务逻辑中途做权限校验的场景:
public class DocumentService
{
private readonly IAuthorizationService _authService;
public DocumentService(IAuthorizationService authService)
{
_authService = authService;
}
public async Task<bool> CanEditAsync(ClaimsPrincipal user, Document doc)
{
var result = await _authService.AuthorizeAsync(user, doc, "EditPolicy");
return result.Succeeded;
}
}
基于资源的授权与多个Handler组合
前面例子的上下文只有用户信息,但实际业务里经常需要结合具体资源判断,比如“只有文档的创建者才能编辑”。这时就要用到资源重载的Handler版本:
public class DocumentEditHandler
: AuthorizationHandler<EditRequirement, Document>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
EditRequirement requirement,
Document resource)
{
var userId = context.User.FindFirstValue(ClaimTypes.NameIdentifier);
if (resource.OwnerId == userId || context.User.IsInRole("Admin"))
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
调用时把资源传进去即可:_authService.AuthorizeAsync(user, document, "EditPolicy")。这种把资源和权限判断解耦的方式,比在业务代码里到处写if判断要干净得多,权限规则集中管理,后期调整也不用大面积改代码。
另一个实用技巧是给同一个Requirement注册多个Handler。框架的规定是:只要任意一个Handler调用了Succeed,该Requirement就算通过。利用这个特性可以轻松实现“或”逻辑,例如系统既支持部门主管审批,也支持拥有特殊权限令牌的用户操作,两个Handler各管一条路径,任何一个通过即可放行。而策略内多个Requirement之间则是“与”的关系,通过这种组合,基本能覆盖绝大多数权限场景。
最后提两个常见踩坑点。一是策略名大小写敏感,特性里的名字和注册时的名字必须完全一致,否则运行时会抛出找不到策略的异常;二是自定义的Requirement对应的Handler忘记注册到容器里,此时授权永远不会Succeed,请求会收到403但没有明显报错,排查起来比较费劲,建议在开发环境写个断言检查关键Handler是否已注册。把这些细节掌握之后,配合声明、角色和资源的多维判断,就可以在ASP.NET Core里搭出一套相当完整的权限体系了。
ASP.NET Core授权自定义授权策略IAuthorizationHandler修改时间:2026-09-07 22:36:39