这篇文章我们将一起来学习 Asp.Net Core 中的 (注: 这样描述不准确, 稍后你会明白) 授权过程
前情提要
在之前的文章里, 我们有提到认证和授权是两个分开的过程, 而且认证过程不属于 Identity. 同样授权过程也不属于 Identity, 授权过程放在 Identity 系列中将的原因和认证过程一样 -- 和成员系统放在一起容易理解.
动手做
在弄清的是授权过程在哪里发生的之前, 我们先来动手写一写授权的代码, 如果了解策略授权, 那么你可以快速浏览过这部分
打开之前创建的项目, 添加一个名为 Demo 的控制器, 控制器代码如下:
- using Microsoft.AspNetCore.Authorization;
- using Microsoft.AspNetCore.Mvc;
- namespace IdentityDemo.Controllers
- {
- [Produces("application/json")]
- [Route("api/demo")]
- public class DemoController : Controller
- {
- [Authorize]
- [HttpGet]
- public object Get()
- {
- return new
- {
- User.Identity.Name,
- User.Identity.IsAuthenticated
略...
用之前注册的账户登录系统,
访问 / api/demo, 你将得到如下结果:
- {
- "name": "jbl-2011@163.com",
- "isAuthenticated": true
- }
然后退出登录, 再次访问 / api/demo, 那么将会跳转到登陆页面, 在这个过程中 Authorize 特性起到了至关重要的作用, 接下来去掉 Authorize 特性, 重复上两个操作, 未登录的结果将是:
- {
- "name": null,
- "isAuthenticated": false
- }
通过这两个小例子, 我们很容易就能推断出 Authorize 特性拦截了没有登陆的用户, 等等, 是 Authorize 特性拦截了请求吗?
授权过程的发生地
很显然, 不是 Authorize 特性拦截了请求, 特性只是标记了这个方法需要被授权才能访问, 而真正拦截了请求的是 --"Mvc 中间件".Action 是由 Mvc 执行的, Mvc 执行时会确认 Action 上的 Authorize 特性, 来确定是否要进行授权操作(成功授权可以访问, 失败了会被阻止(比如跳转到登陆)), 以及如何授权(动物园例子中, 第二个门卫根据切实的情况决定), 也就是自定义授权(角色等等).
另外, 如果我们只是简单的为 Action 方法打上 [Authorize] 标记, 那么它的默认行为就是验证 IsAuthenticated 是否是 true, 也就是在认证环节 (Authentication 中间件) 是否通过了认证
现在, 我们知道了两个点
认证过程 Authentication 发生在 Authentication 中间件中
授权过程 Authorization 发生在 Mvc 中间件中
基于策略的灵活授权
在企业应用中最为常见的就是基于角色的授权, 实现角色授权的方式有两种, 一种是直接写在 Authorize 特性上:
- [Authorize(Roles = "admin,super-admin,")]
- [HttpPost]
- [ValidateAntiForgeryToken]
- public async Task<IActionResult> Test()
不过这种方式, 不推荐, 因为这样的话我们就将 "角色" 和 "Uri" 的绑定 "硬编码在代码里了", 在很多场景这显然不合适, 所以接下来我们要介绍的基于策略的授权就允许我们自定义授权逻辑, 这样就灵活多了
基于策略 Policy 的授权
我们假设我们的授权规则是要求和上方代码片段实现相同效果, 即用户具有角色 "admin" 或者角色 "super-admin", 我们来逐步实现这个目标:
第一步在 DI 中注册一个用于我们需要的 policy
- services.AddAuthorization(options =>
- {
- options.AddPolicy("role-policy", policy =>
- {
- policy.AddRequirements(new RoleRequirement("admin","super-admin"));
- });
- });
我们为该策略指定了一个名字 role-policy, 并且指定了这个策略的需求条件, 需求条件主要是为了设置策略的初始值, 我们可以在策略注册时更改需求条件从而灵活控制授权.
接下来我们来编写 RoleRequirement
- public class RoleRequirement : IAuthorizationRequirement
- {
- public IEnumerable<string> Roles { get; }
- public RoleRequirement(params string[] roles)
- {
- Roles = roles ?? throw new ArgumentNullException(nameof(roles));
略...
那我们的 RoleRequirement 主要实现的功能就是确定要包含的角色, 因为要包含的角色是在构造函数中确定的, 那么我们就将角色授权的逻辑 (稍后介绍的 Handler) 和具体授权的数据分开了.
然后我们来实现 RoleRequirement 对应的处理程序:
- public class RoleHandler : AuthorizationHandler<RoleRequirement>
- {
- protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RoleRequirement requirement)
- {
- foreach (var item in requirement.Roles)
- {
- if (context.User.IsInRole(item))
- {
- context.Succeed(requirement);
- return Task.CompletedTask;
- }
- }
- context.Fail();
- return Task.CompletedTask;
略...
这个处理器的工作十分简单就是验证当前用户是否在任意一个由 RoleRequirement 指定的角色中. 在这里
context.Succeed(requirement);
指示授权成功, 而授权失败一般不需要调用 context.Fail(); 因为对于这个需求还可能有其它处理器进行处理, 而此例中调用 context.Fail(); 可以确保授权失败, 因为 RoleRequirement 的处理器只有一个, 所以这样做是没有问题的.
要注意的是刚刚提到的, 我们已经将角色授权的逻辑 (稍后介绍的 Handler) 和具体授权的数据分开了.
因为 RoleHandler 并不清楚要求用户有哪些角色, RoleHandler 只知道如何去验证用户含有哪些角色, 而具体要求用户含有哪些角色, 是由 RoleRequirement 来决定的, 这符合关注点分离和单一职责这两个编程概念.
再然后, 我们要将刚刚写好的 RoleHandler 注册进 Di
services.AddSingleton<IAuthorizationHandler, RoleHandler>();
最后一步, 更换原来的 Attribute:
- // [Authorize(Roles = "admin,super-admin,")]
- [Authorize(Policy ="role-policy")]
- [HttpPost]
- [ValidateAntiForgeryToken]
- public async Task<IActionResult> Test()
现在, 一个最基本的基于策略的授权就完成了.
本文中的示例较为简单, 也并没有使用全部的授权特性, 更详细的使用方法参考资料很多, 本文也就不多做介绍.
另外你可以参考 ASP.NET Core 中基于策略的授权 https://docs.microsoft.com/zh-cn/aspnet/core/security/authorization/policies?view=aspnetcore-2.1#security-authorization-policies-based-multiple-handlers 来学习更过关于策略授权的内容
授权时指定 AuthenticationScheme
指定 AuthenticationScheme 的代码类似这样:
- // [Authorize(Roles = "admin,super-admin,")]
- [Authorize(AuthenticationSchemes ="jwt"/* 注意, 这里的名字取决于你添加 AuthenticationHandler 时的名字 */, Policy ="role-policy")] [HttpPost]
- [AllowAnonymous]
- [ValidateAntiForgeryToken]
- public async Task<IActionResult> Test()
在上一篇博客 ASP.NET Core Identity 实战 (3) 认证过程中提到, 在 Authentication 中间件中可以放置多个 Handler, 而有一个是默认激活的, 那么剩下的是被动调用的, 现在我们的情况就是由我们在 Authorize 特性中去挑选一个 Handler 来执行, 例如我们在 Authentication 中间件上放置两个 Handler--CookieAuthenticationHandler 和 JwtAuthenticationHandler, 并经 CookieAuthenticationHandler 指定为默认, 那么我们想经由 Jwt 认证时怎么办?
这里有一个重要问题就是: 当 HttpContext 流过 Authentication 中间件后才到 Mvc 中间件, 而 Mvc 在确认 Action 指定的 AuthenticationHandler 时, Authentication 过程已经结束了.
那这是怎么做到的呢?
还记的 HttpContext 中有一个扩展方法叫 AuthenticateAsync, 作为 HttpContext 的扩展方法也就意味着, 我们可以在任何时候调用它进行认证操作.
- namespace Microsoft.AspNetCore.Authentication
- {
- public static class AuthenticationHttpContextExtensions
- {
- public static Task<AuthenticateResult> AuthenticateAsync(this HttpContext context);
- public static Task<AuthenticateResult> AuthenticateAsync(this HttpContext context, string scheme);
略...
看它的第二个重载, 它是指定了 AuthenticationScheme 的名字的, 所以在 Mvc 中间件探查到 Attribute 指定了 AuthenticationScheme 时, 就会重新挑选指定的 AuthenticationHandler 再次对请求进行认证
授权的发生地 --AuthorizationFilter
在旧的 Asp.Net 时代, 我们知道 MvcFilter 这个东西, 现在它仍然在, 如果你不了解它, 我建议你稍作了解, 建议参考官方文档 https://docs.microsoft.com/zh-cn/aspnet/core/mvc/controllers/filters?view=aspnetcore-2.1
正如这一节的标题, 授权发生在
Microsoft.AspNetCore.Mvc.Authorization.AuthorizationFilter
中, 授权的逻辑类似这样:
先进行认证
如果指定了 scheme, 那么重新认证, 如果没有, 则使用之前 Authentication 中间件的授权结果:
- public virtual async Task<AuthenticateResult> Microsoft.AspNetCore.Authorization.Policy.PolicyEvaluator.AuthenticateAsync(AuthorizationPolicy policy, HttpContext context)
- {
- if (policy.AuthenticationSchemes != null && policy.AuthenticationSchemes.Count> 0)
- {
- ClaimsPrincipal newPrincipal = null;
- foreach (var scheme in policy.AuthenticationSchemes)
- {
- var result = await context.AuthenticateAsync(scheme);
- if (result != null && result.Succeeded)
- {
- newPrincipal = SecurityHelper.MergeUserPrincipal(newPrincipal, result.Principal);
- }
- }
- if (newPrincipal != null)
- {
- context.User = newPrincipal;
- return AuthenticateResult.Success(new AuthenticationTicket(newPrincipal, string.Join(";", policy.AuthenticationSchemes)));
- }
- else
- {
- context.User = new ClaimsPrincipal(new ClaimsIdentity());
- return AuthenticateResult.NoResult();
- }
- }
- return (context.User?.Identity?.IsAuthenticated ?? false)
- ? AuthenticateResult.Success(new AuthenticationTicket(context.User, "context.User"))
- : AuthenticateResult.NoResult();
- }
这里面值得再次深入探讨的是
context.AuthenticateAsync(scheme)
, 这是在 HttpAbstractions 项目中的扩展方法, 它的实现是:
- public static Task<AuthenticateResult> AuthenticateAsync(this HttpContext context, string scheme) =>
- context.RequestServices.GetRequiredService<IAuthenticationService>().AuthenticateAsync(context, scheme);
- IAuthenticationService
我们在 Authentication 中间件中也见过, Authentication 中间件也是使用了
IAuthenticationService
, 之前的文章有提到过, 这也再次证明了单一原则职责, 身份认证中间件负责在管道中认证, 而认证本身并非是和身份认证中间件捆绑的, 上一篇博客 ASP.NET Core Identity 实战 (3) 认证过程的最后有认证的源代码
再进行授权
授权总共分三步
拿到 IAuthorizeHandler 的实例(前面我们写了一个)(可能一个或者多个 https://docs.microsoft.com/zh-cn/aspnet/core/security/authorization/policies?view=aspnetcore-2.1#why-would-i-want-multiple-handlers-for-a-requirement )
执行授权(每个 Handler 都会进行授权)
没了
这部分代码还是很简单的:
- public async Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, IEnumerable<IAuthorizationRequirement> requirements)
- {
- // 第一步
- var authContext = _contextFactory.CreateContext(requirements, user, resource);
- var handlers = await _handlers.GetHandlersAsync(authContext);
- // 第二部
- foreach (var handler in handlers)
- {
- await handler.HandleAsync(authContext);
- if (!_options.InvokeHandlersAfterFailure && authContext.HasFailed)
- {
- break;
- }
- }
- // 没了(这主要是对结果进行处理)
- var result = _evaluator.Evaluate(authContext);
- if (result.Succeeded)
- {
- _logger.UserAuthorizationSucceeded(GetUserNameForLogging(user));
- }
- else
- {
- _logger.UserAuthorizationFailed(GetUserNameForLogging(user));
- }
- return result;
- }
这里面和我们在项目中写的代码有关就是 IAuthorizeHandler 的实例, 在本文中, 我们写了一个 RoleHandler
到此, 授权过程就结束了, 另外一些就是边边角角的知识点, 比如授权之后如何操作, 这些不难, 就不再文中赘述了
来源: https://www.cnblogs.com/rocketRobin/p/9317945.html