ASP.NET Core Identity 实战(4)授权过程

时间:2024-09-04 16:02:50

这篇文章我们将一起来学习 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中基于策略的授权来学习更过关于策略授权的内容

授权时指定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这个东西,现在它仍然在,如果你不了解它,我建议你稍作了解,建议参考官方文档

正如这一节的标题,授权发生在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)认证过程的最后有认证的源代码

再进行授权

授权总共分三步

  1. 拿到IAuthorizeHandler的实例(前面我们写了一个)(可能一个或者多个
  2. 执行授权(每个Handler都会进行授权)
  3. 没了

这部分代码还是很简单的:

    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

到此,授权过程就结束了,另外一些就是边边角角的知识点,比如授权之后如何操作,这些不难,就不再文中赘述了