引用:https://blog.csdn.net/WuLex/article/details/81072451
软件应用程序的授权层可确保当前用户能够访问指定资源、执行给定操作或对指定资源执行给定操作。在 ASP.NET Core
中,授权层的设置方式有两种。可以使用角色,也可以使用策略。前一种方法(即基于角色的授权)一直在旧版 ASP.NET
平台中沿用,而基于策略的授权则是 ASP.NET Core
中新增的方法。
##Authorize 属性
从早期开始,ASP.NET
应用程序中使用的一直都是角色。从技术角度来讲,角色是纯字符串。不过,它的值被安全层视为元信息(检查 IPrincipal
对象中是否有值),并供应用程序使用,用于将一组权限映射到经过身份验证的给定用户。在 ASP.NET
中,登录用户由 IPrincipal
对象进行标识。在 ASP.NET Core
中,实际类是 ClaimsPrincipal
。此类可公开一系列标识,每个标识均由 IIdentity
对象(具体而言,就是 ClaimsIdentity
对象)进行表示。也就是说,任何登录用户都会随附一个声明列表,这其实就是用户的状态声明。用户名和角色是 ASP.NET Core
应用程序用户的两个常见声明。不过,角色是否显示取决于后备标识存储区。例如,如果使用社交身份验证,永远都不会看到角色。
授权比身份验证更进一步。身份验证就是发现用户标识,而授权则是定义用户调用应用程序终结点的要求。用户角色通常存储在数据库中,并在用户凭据经过验证后进行检索。此时,角色信息以某种方式附加到用户帐户。IIdentity
接口的特征之一是,必须实现 IsInRole
方法。为此,ClaimsIdentity
类检查身份验证进程生成的一系列声明中是否有角色声明。总之,当用户尝试调用安全的控制器方法时,角色应可供检查。如果不可以,用户调用任何安全的方法时则会遭拒。
Authorize
属性通过声明的方式保护控制器或其部分方法:
[Authorize]
public class CustomerController : Controller
{
...
}
- 1
- 2
- 3
- 4
- 5
如果未指定参数,此属性仅检查用户是否经过身份验证。不过,此属性支持 Roles
等其他属性。Roles
属性指明将对具有任一所列角色的用户授予访问权限。如果需要多个角色,可以多次应用 Authorize
属性,也可以编写自己的筛选器。
[Authorize(Roles="admin, system"]
public class BackofficeController : Controller
{
...
}
- 1
- 2
- 3
- 4
- 5
Authorize
属性还可以视需要通过 ActiveAuthenticationSchemes
属性,接受一个或多个身份验证方案。
[Authorize(Roles="admin, system", ActiveAuthenticationSchemes="Cookie"]
public class BackofficeController : Controller
{
...
}
- 1
- 2
- 3
- 4
- 5
ActiveAuthenticationSchemes
属性是逗号分隔字符串,用于列出授权层将在当前上下文中信任的身份验证中间件组件。也就是说,它声明仅当用户通过 Cookie
方案进行身份验证并具有任一所列角色时,才允许访问 BackofficeController
类。如前所述,传递到 ActiveAuthenticationSchemes
属性的字符串值必须与应用程序启动时注册的身份验证中间件一致。
请注意,在 ASP.NET 2.0
中,身份验证中间件被替换为包含多个处理程序的服务。因此,身份验证方案是选择处理程序的标签。若要详细了解 ASP.NET Core
中的身份验证,建议参阅我在 2017 年 9 月发表的专栏文章“ASP.NET Core
中的 Cookie
、声明和身份验证”(msdn.com/magazine/mt842501)。
授权筛选器
系统提供的授权筛选器使用 Authorize
属性提供的信息。此筛选器先于其他任何 ASP.NET Core
筛选器运行,因为它负责检查用户能否执行请求的操作。如果用户未经授权,筛选器会简化管道,并取消请求。
可以创建自定义授权筛选器,但大部分情况下无需这样做。实际上,最好配置默认筛选器依赖的现有授权层。
##角色、权限和否决
借助角色,可以根据用户能够执行或不能执行的操作,对应用程序用户轻松进行分组。不过,这种方法不是非常容易表达;至少,还不足以满足大部分新式应用程序的需求。例如,假设为相对简单的授权体系结构,可以服务于网站的普通用户,以及获得授权可访问后端办公系统软件并更新内容的 Power User
。基于角色的授权层可以围绕两个角色(即用户和管理员)进行构建,这些角色定义了每组可以访问的控制器和方法。
涉及否决方面的细微区别时,就会遇到问题,因为这些区别描述了具有给定角色的用户能够执行或不能执行的操作。例如,可能有用户喜欢访问后端办公系统。但在这些用户中,有的获得授权只能编辑客户数据,有的获得授权只能处理内容,还有的获得授权既能编辑客户数据,也能处理内容(见图 1)。
图 1:角色层次结构
角色实质上是一种平面概念。如何平展图 1 中所示的简单层次结构?可以创建四个不同的角色(即 User
、Admin
、CustomerAdmin
和 ContentsAdmin
),但只要否决数量变多,所需的角色数量就会大大增加。即使像这样的简单练习,也表明角色可能并不是处理授权的最有效方法(优先考虑向后兼容性的简单方案和实例除外)。对于其他所有情况,要求则不同。下面开始介绍基于策略的授权。
##策略到底是什么?
在 ASP.NET Core
中,基于策略的授权框架旨在分离授权与应用程序逻辑。简而言之,策略是以一系列要求的形式设计的实体,这些要求本身就是当前用户必须满足的条件。
最简单的策略是,对用户进行身份验证,同时还须满足用户与给定角色相关联这一常见要求。另一常见要求是,用户必须有特定声明或包含某值的特定声明。从最一般的意义上来讲,要求就是断言了尝试访问正确方法的用户标识。策略对象是使用以下代码进行创建:
var policy = new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes("Cookie, Bearer")
.RequireAuthenticatedUser()
.RequireRole("Admin")
.RequireClaim("editor", "contents") .RequireClaim("level", "senior")
.Build();
- 1
- 2
- 3
- 4
- 5
- 6
生成器对象使用各种扩展方法收集要求,再生成策略实例。可以看到,要求约束了身份验证状态和方案、角色以及通过身份验证 Cookie
或持有者令牌读取的任何声明组合。
如果预定义的所有扩展方法都不适用于定义要求,始终可以采取最后手段,即通过自己的断言定义新要求。命令如下:
var policy = new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes("Cookie, Bearer")
.RequireAuthenticatedUser()
.RequireRole("Admin")
.RequireAssertion(ctx =>
{
return ctx.User.HasClaim("editor", "contents") ||
ctx.User.HasClaim("level", "senior");
})
.Build();
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
RequireAssertion
方法需要使用 lambda
,以接收 HttpContext
对象,并返回布尔值。因此,断言就是条件语句。请注意,如果多次连接 RequireRole
,用户必须履行所有角色。若要改为表达 OR
条件,可能需要将断言用作最后手段。在此示例中,策略实际上允许角色为内容编辑者或高级用户的用户。
##注册策略
光定义策略还不够,还必须向授权中间件注册策略。为此,请在 Startup
类的 ConfigureServices
方法中,将授权中间件添加为服务,如下所示:
services.AddAuthorization(options=>
{
options.AddPolicy("ContentsEditor", policy =>
{
policy.AddAuthenticationSchemes("Cookie, Bearer");
policy.RequireAuthenticatedUser();
policy.RequireRole("Admin");
policy.RequireClaim("editor", "contents");
});
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
添加到中间件的每个策略都有一个名称,用于在 Controller
类的 Authorize
属性中引用策略:
[Authorize(Policy = "ContentsEditor")]
public IActionResult Save(Article article)
{
// ...
}
- 1
- 2
- 3
- 4
- 5
使用 Authorize
属性,可以声明的方式设置策略,但也可以通过操作方法以编程方式调用策略,如图 2 所示。
图 2:以编程方式检查策略
public class AdminController : Controller
{
private IAuthorizationService _authorization;
public AdminController(IAuthorizationService authorizationService)
{
_authorization = authorizationService;
}
public async Task<IActionResult> Save(Article article)
{ var allowed = await _authorization.AuthorizeAsync( User, "ContentsEditor"));
if (!allowed)
return new ForbiddenResult();
// Proceed with the method implementation
...
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
如果无法以编程方式检查权限,建议返回 ForbiddenResult
对象。另一种选择是,返回 ChallengeResult
对象。在 ASP.NET Core 1.x
中,返回质询会指示授权中间件返回 401
状态代码,或将用户重定向到登录页,具体视配置而定。不过,ASP.NET Core 2.0
中不会发生重定向;即使在 ASP.NET Core 1.x
中,如果用户已登录,质询最终也会指示返回 ForbiddenResult
对象。最后看来,最好的方法是在无法检查权限时返回 ForbiddenResult
对象。
请注意,甚至可以在 Razor
视图中以编程方式检查策略,如下面的代码所示:
@{
var authorized = await Authorization.AuthorizeAsync(
User, "ContentsEditor"))}
@if (!authorized)
{
<div class="alert alert-error">
You’re not authorized to access this page.
</div>
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
不过,为了让此代码能够正常运行,必须先注入对授权服务的依赖,如下所示:
@inject IAuthorizationService Authorization
- 1
在视图中使用授权服务,有助于隐藏当前用户在给定上下文中不得接触到的 UI
元素。但请注意,光在视图中隐藏选项还不够。始终还需要在控制器中强制执行策略。
##自定义要求
常备要求基本上涵盖了声明、身份验证,并提供了常规用途机制,用于根据断言进行自定义,但也可以创建自定义要求。策略要求由以下两种元素组成:仅保留数据的要求类,以及对用户验证数据的授权处理程序。创建自定义要求,还可以进一步表达特定策略。例如,假设要将内容编辑者策略扩展为,增添用户至少必须有三年经验的要求。具体代码如下:
public class ExperienceRequirement : IAuthorizationRequirement
{
public int Years { get; private set; }
public ExperienceRequirement(int minimumYears)
{
Years = minimumYears;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
要求至少必须有一个授权处理程序。处理程序的类型为 AuthorizationHandler<T>
,其中 T
是要求类型。图 3 展示了 ExperienceRequirement
类型的示例处理程序。
图 3:示例授权处理程序
public class ExperienceHandler :
AuthorizationHandler<ExperienceRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
ExperienceRequirement requirement)
{
// Save User object to access claims
var user = context.User; if (!user.HasClaim(c => c.Type == "EditorSince")) return Task.CompletedTask;
var since = user.FindFirst("EditorSince").Value.ToInt();
if (since >= requirement.Years)
context.Succeed(requirement);
return Task.CompletedTask;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
示例授权处理程序读取与用户关联的声明,并检查自定义 EditorSince
声明。如果找不到,处理程序便无法返回成功。只有在找到声明且包含的整数值不低于指定年数时,才能返回成功。
自定义声明应为一条信息,以某种方式与保存到身份验证 Cookie
中的用户相关联(例如,“用户”表中的列)。不过,一旦保留对用户的引用,便始终可以从声明中找到用户名,并对任何数据库或外部服务运行查询,以获取经验年数,从而在处理程序中使用此信息。(我承认,如果 EditorSince
值保留 DateTime
,并计算用户担任编辑者是否已有一定年数,此示例会更真实一点。)
授权处理程序调用方法 Succeed
,同时传递当前要求,以通知此要求已成功得到验证。如果没有传递要求,处理程序无需执行任何操作,可以直接返回内容。不过,如果处理程序要确定是否不符合要求(无论其他处理程序是否已成功验证同一要求),将会对授权上下文对象调用方法 Fail
。
下面展示了如何将自定义要求添加到策略(请注意,由于这是自定义要求,因此没有扩展方法,而必须继续处理策略对象的整个 Requirements
集合):
services.AddAuthorization(options =>
{
options.AddPolicy("AtLeast3Years",
policy => policy
.Requirements
.Add(new ExperienceRequirement(3)));
});
- 1
- 2
- 3
- 4
- 5
- 6
- 7
此外,还需要在 IAuthorizationHandler
类型的范围内向 DI
系统注册新的处理程序:
services.AddSingleton<IAuthorizationHandler, ExperienceHandler>();
- 1
如前所述,要求可包含多个处理程序。如果为授权层的同一要求向 DI
系统注册多个处理程序,有一个成功就足够了。
##访问当前 HttpContext
在实现授权处理程序的过程中,可能需要检查请求属性或路由数据,如下所示:
if (context.Resource is AuthorizationFilterContext mvc)
{
var url = mvc.HttpContext.Request.GetDisplayUrl(); ...
}
- 1
- 2
- 3
- 4
在 ASP.NET Core
中,AuthorizationHandlerContext
对象向 FilterContext
对象公开 Resource
属性集。上下文对象因所涉及的框架而异。例如,MVC
和 SignalR
发送自己的特定对象。是否发生转换视需要访问的内容而定。例如,用户信息始终可用,所以无需为此进行转换;但若要获取 MVC
专属详细信息(如路由信息),则需要进行转换。
##总结
在 ASP.NET Core
中,授权分为两种。一种是基于角色的传统授权,它的工作原理与在经典 ASP.NET MVC
中的工作原理相同,但仍存在相当平面化的结构限制,不适合表达复杂的授权逻辑。基于策略的身份验证是一种新方法,可提供更丰富、更易表达的模型。这是因为,策略包含一系列基于声明的要求,以及基于可从 HTTP
上下文或外部源注入的其他任何信息的自定义逻辑。这些要求各自与一个或多个处理程序相关联,这些处理程序负责要求的实际计算。