上次老周和大伙伴们分享了有关按用户Level授权的技巧,本文咱们聊聊以用户角色来授权的事。

按用户角色授权其实更好弄,毕竟这个功能是内部集成的,多数场景下我们不需要扩展,不用自己写处理代码。从功能语义上说,授权分为按角色授权和按策略授权,而从代码本质上说,角色权授其实是包含在策略授权内的。怎么说呢?往下看。

角色授权主要依靠 RolesAuthorizationRequirement 类,来看一下源码精彩片段回放。

public class RolesAuthorizationRequirement : AuthorizationHandler<RolesAuthorizationRequirement>, IAuthorizationRequirement
{
public RolesAuthorizationRequirement(IEnumerable<string> allowedRoles)
{
……
AllowedRoles = allowedRoles;
} public IEnumerable<string> AllowedRoles { get; } protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RolesAuthorizationRequirement requirement)
{
if (context.User != null)
{
var found = false; foreach (var role in requirement.AllowedRoles)
{
// 重点在这里
if (context.User.IsInRole(role))
{
found = true; //说明是符合角色要求的
break;
}
} if (found)
{
// 满足要求
context.Succeed(requirement);
}
}
return Task.CompletedTask;
} ……
}

这个是不是有点熟悉呢?对的,上一篇博文里老周介绍过,实现 IAuthorizationRequirement 接口表示一个用于授权的必备条件(或者叫必备要素),AuthorizationHandler 负责验证这些必备要素是否满足要求。上一篇博文中,老周是把实现 IAuthorizationRequirement 接口和重写抽象类 AuthorizationHandler<TRequirement> 分成两部分完成,而这里,RolesAuthorizationRequirement 直接一步到位,两个一起实现。

好,理论先说到这儿,下面咱们来过一把代码瘾,后面咱们回过头来再讲。咱们的主题是说授权,不是验证。当然这两者通常是一起的,因为授权的前提是要验证通过。所以为了方便简单,老周还是选择内置的 Cookie 验证方案。不过这一回不搞用户名、密码什么的了,而是直接用 Claim 设置角色就行了,毕竟我们的主题是角色授权。

public class LoginController : Controller
{
[HttpGet("/login")]
public IActionResult Login() => View(); [HttpPost("/login")]
public async void Login(string role)
{
Claim c = new(ClaimTypes.Role, role);
ClaimsIdentity id = new(new[] { c }, CookieAuthenticationDefaults.AuthenticationScheme);
ClaimsPrincipal p = new ClaimsPrincipal(id);
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, p);
} [HttpGet("/denied")]
public IActionResult DeniedAcc() => Content("不好意思,你无权访问");

[HttpGet("/logout")]
      public async void Logout()=> await HttpContext.SignOutAsync();

}

无比聪明的你一眼能看出,这是 MVC 控制器,并且实现登录有关的功能:

/login:进入登录页

/logout:注销

/denied:表白失败被拒绝,哦不,授权失败被拒绝后访问

Login 方法有两个,没参数的是 GET 版,有参数的是 POST 版。当以 POST 方式访问时,会有一个 role 参数,表示被选中的角色。这里为了简单,不用输用户名密码了,直接选个角色就登录。

Login 视图如下:

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<div>
<p>登录角色:</p>
<form method="post">
<select name="role">
<option value="admin">管理员</option>
<option value="svip" selected>超级会员</option>
<option value="gen">普通客户</option>
</select>
<button type="submit">登入</button>
</form>
</div>

select 元素的名称为 role,正好与 Login 方法(post)的参数 role 相同,能进行模型绑定。

admin 角色表示管理员,svip 角色表示超级VIP客户,gen 角色表示普通客户。假设这是一家大型纸尿裤批发店的客户管理系统。这年头,连买纸尿裤也要分三六九等了。

下面是该纸尿裤批发店为不同客户群提供的服务。

[Route("znk")]
public class 纸尿裤Controller : Controller
{
[Route("genindex")]
[Authorize(Roles = "gen")]
public IActionResult IndexGen()
{
return Content("普通客户浏览页");
} [Route("adminindex")]
[Authorize(Roles = "admin")]
public IActionResult IndexAdmin()
{
return Content("管理员专场");
} [Route("svipindex")]
[Authorize(Roles = "svip")]
public IActionResult IndexSVIP() => Content("超级会员杀熟通道");
}

注意上面授权特性,不需要指定策略名称,只需指定你要求的角色名称即可。

在应用程序的初始化配置上,咱们设置 Cookie 验证。

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(opt =>
{
opt.LoginPath = "/login";
opt.AccessDeniedPath = "/denied";
opt.LogoutPath = "/logout";
opt.ReturnUrlParameter = "url";
opt.Cookie.Name = "_XXX_FFF_";
});
var app = builder.Build();

那几个路径就是刚才 Login 控制器上的访问路径。

因为不需要配置授权策略,所以不需要调用 AddAuthorization 扩展方法。主要是这个方法你在调用 AddControllersWithViews 方法时会自动调用,所以,如无特殊配置,咱们不用手动开启授权功能。像 MVC、RazorPages 等这些功能,默认会配置授权的。

假如我要访问纸尿裤批发店的超级会员通道,访问 /znk/svipindex,这时候会跳转到登录界面,并且 url 参数包含要回调的路径。

默认是选中“超级会员”的,此时点击“登入”,就能获取授权。

如果选择“普通客户”,就会授失败,拒绝访问。

----------------------------------------------------------------------------------------

虽然角色授权功能咱们轻松实现了,可是,随之而来的会产生一些疑问。不知道你有没有这些疑问,反正老周有。

1、既然在代码上角色授权是包含在策略授权中的,那咱们没配置策略啊,为啥不出错?

AuthorizationPolicy 类有个静态方法—— CombineAsync,这个方法的功能是合并已有的策略。但,咱们重点看这一段:

AuthorizationPolicyBuilder? policyBuilder = null;
if (!skipEnumeratingData)
{
foreach (var authorizeDatum in authorizeData)
{
if (policyBuilder == null)
{
policyBuilder = new AuthorizationPolicyBuilder();
} var useDefaultPolicy = !(anyPolicies);
// 如果有指定策略名称,就合并
if (!string.IsNullOrWhiteSpace(authorizeDatum.Policy))
{
var policy = await policyProvider.GetPolicyAsync(authorizeDatum.Policy).ConfigureAwait(false);
if (policy == null)
{
throw new InvalidOperationException(Resources.FormatException_AuthorizationPolicyNotFound(authorizeDatum.Policy));
}
policyBuilder.Combine(policy);
useDefaultPolicy = false;
} // 如果指定了角色名称,调用 RequireRole 方法添加必备要素
var rolesSplit = authorizeDatum.Roles?.Split(',');
if (rolesSplit?.Length > 0)
{
var trimmedRolesSplit = rolesSplit.Where(r => !string.IsNullOrWhiteSpace(r)).Select(r => r.Trim());
policyBuilder.RequireRole(trimmedRolesSplit);
useDefaultPolicy = false;
} // 同理,如果指定的验证方案,添加之
var authTypesSplit = authorizeDatum.AuthenticationSchemes?.Split(',');
if (authTypesSplit?.Length > 0)
{
foreach (var authType in authTypesSplit)
{
if (!string.IsNullOrWhiteSpace(authType))
{
policyBuilder.AuthenticationSchemes.Add(authType.Trim());
}
}
}
……

原来,在合并策略过程中,会根据 IAuthorizeData 提供的内容动态添加 IAuthorizationRequirement 对象。这里出现了个 IAuthorizeData  接口,这厮哪来的?莫急,你看看咱们刚才在 纸尿裤 控制器上应用了啥特性。

 [Route("adminindex")]
[Authorize(Roles = "admin")]
public IActionResult IndexAdmin()
{
return Content("管理员专场");
}

对,就是它!AuthorizeAttribute,你再看看它实现了什么接口。

public class AuthorizeAttribute : Attribute, IAuthorizeData

再回忆一下刚刚这段:

 var rolesSplit = authorizeDatum.Roles?.Split(',');
if (rolesSplit?.Length > 0)
{
var trimmedRolesSplit = rolesSplit.Where(r => !string.IsNullOrWhiteSpace(r)).Select(r => r.Trim());
policyBuilder.RequireRole(trimmedRolesSplit);
……
}

原来这里面还有玄机,Role 可以指定多个角色的哟,用逗号(当然是英文的逗号)隔开。如

 [Route("adminindex")]
[Authorize(Roles = "admin, svip")]
public IActionResult IndexAdmin()
{
……
}

2、我没有在中间件管道上调用 app.UseAuthorization(),为什么能执行授权处理?

你会发现,在 app 上不调用 UseAuthorization 扩展方法也能使授权生效。因为像 RazorPages、MVC 这些东东还有一个概念,叫 Filter,可以翻译为“筛选器”或“过滤器”。老周比较喜欢叫过滤器,因为这叫法生动自然,筛选器感觉是机器翻译。

在过滤器里,有专门用在授权方面的接口。

同步:IAuthorizationFilter

异步:IAsyncAuthorizationFilter

在过滤器中,同步接口和异步接口只实现其中一个即可。如果你两个都实现了,那只执行异步接口。所以,你两个都实现纯属白淦,毕竟异步优先。为啥?你看看 ResourceInvoker 类的源代码就知道了。

 switch (next)
{
case State.InvokeBegin:
{
goto case State.AuthorizationBegin;
} case State.AuthorizationBegin:
{
_cursor.Reset();
goto case State.AuthorizationNext;
} case State.AuthorizationNext:
{
var current = _cursor.GetNextFilter<IAuthorizationFilter, IAsyncAuthorizationFilter>();
if (current.FilterAsync != null) // 执行异步方法
{
if (_authorizationContext == null)
{
_authorizationContext = new AuthorizationFilterContextSealed(_actionContext, _filters);
} state = current.FilterAsync;
goto case State.AuthorizationAsyncBegin;
}
else if (current.Filter != null) // 执行同步方法
{
if (_authorizationContext == null)
{
_authorizationContext = new AuthorizationFilterContextSealed(_actionContext, _filters);
} state = current.Filter;
goto case State.AuthorizationSync;
}
else
{
          // 如果都不是授权过滤器,直接 End
goto case State.AuthorizationEnd;
}
} case State.AuthorizationAsyncBegin:
{
Debug.Assert(state != null);
Debug.Assert(_authorizationContext != null); var filter = (IAsyncAuthorizationFilter)state;
var authorizationContext = _authorizationContext; _diagnosticListener.BeforeOnAuthorizationAsync(authorizationContext, filter);
_logger.BeforeExecutingMethodOnFilter(
FilterTypeConstants.AuthorizationFilter,
nameof(IAsyncAuthorizationFilter.OnAuthorizationAsync),
filter); var task = filter.OnAuthorizationAsync(authorizationContext);
if (!task.IsCompletedSuccessfully)
{
next = State.AuthorizationAsyncEnd;
return task;
} goto case State.AuthorizationAsyncEnd;
} case State.AuthorizationAsyncEnd:
{
Debug.Assert(state != null);
Debug.Assert(_authorizationContext != null); var filter = (IAsyncAuthorizationFilter)state;
var authorizationContext = _authorizationContext; _diagnosticListener.AfterOnAuthorizationAsync(authorizationContext, filter);
_logger.AfterExecutingMethodOnFilter(
FilterTypeConstants.AuthorizationFilter,
nameof(IAsyncAuthorizationFilter.OnAuthorizationAsync),
filter); if (authorizationContext.Result != null)
{
goto case State.AuthorizationShortCircuit;
}
// 完成后直接下一个授权过滤器
goto case State.AuthorizationNext;
} case State.AuthorizationSync:
{
Debug.Assert(state != null);
Debug.Assert(_authorizationContext != null); var filter = (IAuthorizationFilter)state;
var authorizationContext = _authorizationContext; _diagnosticListener.BeforeOnAuthorization(authorizationContext, filter);
_logger.BeforeExecutingMethodOnFilter(
FilterTypeConstants.AuthorizationFilter,
nameof(IAuthorizationFilter.OnAuthorization),
filter); filter.OnAuthorization(authorizationContext); _diagnosticListener.AfterOnAuthorization(authorizationContext, filter);
_logger.AfterExecutingMethodOnFilter(
FilterTypeConstants.AuthorizationFilter,
nameof(IAuthorizationFilter.OnAuthorization),
filter); if (authorizationContext.Result != null)
{
goto case State.AuthorizationShortCircuit;
}
// 完成后直接一下授权过滤器
goto case State.AuthorizationNext;
} case State.AuthorizationShortCircuit:
{
Debug.Assert(state != null);
Debug.Assert(_authorizationContext != null);
Debug.Assert(_authorizationContext.Result != null);
Log.AuthorizationFailure(_logger, (IFilterMetadata)state); // This is a short-circuit - execute relevant result filters + result and complete this invocation.
isCompleted = true;
_result = _authorizationContext.Result;
return InvokeAlwaysRunResultFilters();
} case State.AuthorizationEnd:
{
goto case State.ResourceBegin;
}

代码很长,老周总结一下它的执行轨迹:

1、AuthorizationBegin 授权开始

2、AuthorizationNext 下一个过滤器

3、如果是异步,走 AuthorizationAsyncBegin

如果同步,走 AuthorizationSync

如果都不是,直接走到 AuthorizationEnd

4、异步:AuthorizationAsyncBegin --> AuthorizationAsyncEnd --> AuthorizationNext(回第2步,有请下一位过滤侠)

同步:AuthorizationSync --> AuthorizationNext(回第2步,有请下一位)

5、AuthorizationEnd 退场,进入 ResourceFilter 主会场

6、在2、3、4步过程中,如果授权失败或出错,直接短路,走 AuthorizationShortCircuit

你瞧,是不是同步和异步只执行一个?

默认的授权过滤器实现 IAsyncAuthorizationFilter,即 AuthorizeFilter 类。所以,授权处理就是在这里被触发了。

var policyEvaluator = context.HttpContext.RequestServices.GetRequiredService<IPolicyEvaluator>();

// 先进行验证
var authenticateResult = await policyEvaluator.AuthenticateAsync(effectivePolicy, context.HttpContext); // 如果允许匿名访问,后面的工作就免了
if (HasAllowAnonymous(context))
{
return;
} // 验证过了,再评估授权策略
var authorizeResult = await policyEvaluator.AuthorizeAsync(effectivePolicy, authenticateResult, context.HttpContext, context); if (authorizeResult.Challenged) //没登录呢,去登录
{
context.Result = new ChallengeResult(effectivePolicy.AuthenticationSchemes.ToArray());
}
else if (authorizeResult.Forbidden) //授权失败,拒绝访问
{
context.Result = new ForbidResult(effectivePolicy.AuthenticationSchemes.ToArray());
}

但是,这个授权过滤器在 MvcOptions 的 Filters 中没有啊,它是啥时候弄进去的?这货不是在 Filters 中配置的,而是在 Application Model 初始化时通过 AuthorizationApplicationModelProvider 类弄进去的。AuthorizationApplicationModelProvider 类实现了 IApplicationModelProvider 接口,但不对外公开。

 public void OnProvidersExecuting(ApplicationModelProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
} if (_mvcOptions.EnableEndpointRouting)
{
// When using endpoint routing, the AuthorizationMiddleware does the work that Auth filters would otherwise perform.
// Consequently we do not need to convert authorization attributes to filters.
return;
} foreach (var controllerModel in context.Result.Controllers)
{
var controllerModelAuthData = controllerModel.Attributes.OfType<IAuthorizeData>().ToArray();
if (controllerModelAuthData.Length > 0)
{
controllerModel.Filters.Add(GetFilter(_policyProvider, controllerModelAuthData));
}
foreach (var attribute in controllerModel.Attributes.OfType<IAllowAnonymous>())
{
controllerModel.Filters.Add(new AllowAnonymousFilter());
} foreach (var actionModel in controllerModel.Actions)
{
var actionModelAuthData = actionModel.Attributes.OfType<IAuthorizeData>().ToArray();
if (actionModelAuthData.Length > 0)
{
actionModel.Filters.Add(GetFilter(_policyProvider, actionModelAuthData));
} foreach (var _ in actionModel.Attributes.OfType<IAllowAnonymous>())
{
actionModel.Filters.Add(new AllowAnonymousFilter());
}
}
}
}

而 filter 是在 GetFilter 方法生成的。

    public static AuthorizeFilter GetFilter(IAuthorizationPolicyProvider policyProvider, IEnumerable<IAuthorizeData> authData)
{
// The default policy provider will make the same policy for given input, so make it only once.
// This will always execute synchronously.
if (policyProvider.GetType() == typeof(DefaultAuthorizationPolicyProvider))
{
var policy = AuthorizationPolicy.CombineAsync(policyProvider, authData).GetAwaiter().GetResult()!;
return new AuthorizeFilter(policy);
}
else
{
return new AuthorizeFilter(policyProvider, authData);
}
}

3、RolesAuthorizationRequirement 实现了 IAuthorizationHandler 接口,可是它又没注册到服务容器中,HandlerAsync 方法又是怎么调用的?

RolesAuthorizationRequirement 一步到位,既实现了 IAuthorizationRequirement 接口又实现抽象类 AuthorizationHandler<TRequirement>。它虽然没有在服务容器中注册,可服务容器中注册了 PassThroughAuthorizationHandler 类,有它在,各种实现 IAuthorizationHandler 接口的 Requirement 都能顺利执行,看看源代码。

public class PassThroughAuthorizationHandler : IAuthorizationHandler
{
…… public async Task HandleAsync(AuthorizationHandlerContext context)
{
foreach (var handler in context.Requirements.OfType<IAuthorizationHandler>())
{
await handler.HandleAsync(context).ConfigureAwait(false);
if (!_options.InvokeHandlersAfterFailure && context.HasFailed)
{
break;
}
}
}
}

看,这不就执行了吗。

至此,咱们就知道这角色授权的流程是怎么走的了。

【ASP.NET Core】按用户角色授权的更多相关文章

  1. asp.net core系列 49 Identity 授权(上)

    一.概述 授权是指用户能够访问资源的权限,如页面数据的查看.编辑.新增.删除.导出.下载等权限.ASP.NET Core 授权提供了多种且灵活的方式,包括:Razor pages授权约定.简单授权.R ...

  2. asp.net core根据用户权限控制页面元素的显示

    asp.net core根据用户权限控制页面元素的显示 Intro 在 web 应用中我们经常需要根据用户的不同允许用户访问不同的资源,显示不同的内容,之前做了一个 AccessControlHelp ...

  3. ASP.NET Core 新增用户 - ASP.NET Core 基础教程 - 简单教程,简单编程

    原文:ASP.NET Core 新增用户 - ASP.NET Core 基础教程 - 简单教程,简单编程 ASP.NET Core 新增用户 上一章节我们实现了一个注册表单,但也留了一些东西还没完成, ...

  4. asp.net core web 添加角色管理

    新建asp.net core web应用 添加RolesAdminController [Authorize(Roles = "Admin")] public class Role ...

  5. ASP.NET CORE API Swagger+IdentityServer4授权验证

    简介 本来不想写这篇博文,但在网上找到的文章博客都没有完整配置信息,所以这里记录下. 不了解IdentityServer4的可以看看我之前写的入门博文 Swagger 官方演示地址 源码地址 配置Id ...

  6. Asp.Net Core中的角色

    在前面介绍中我们知道了Asp.Net Core Identity中创建用户使用到的类UserManager<IdentityUser>,同样的,创建角色我们需要使用RoleManager& ...

  7. asp.net core系列 50 Identity 授权(中)

    1.5 基于策略的授权 在上篇中,已经讲到了授权访问(authorization)的四种方式.其中Razor Pages授权约定和简单授权二种方式更像是身份认证(authentication) ,因为 ...

  8. ASP.NET 拼多多用户登录授权后使用code去换取access_token

    一.拼多多开放平台 由于本人刚毕业进公司实习 遇到一些问题然后想通过博客来记录和分享给大家一起学习. 第一次写博客没什么经验不是写的很好 请大家多多关照 嘴下留情哈哈 谢谢! 好了 话不多说直接进入主 ...

  9. oracle创建表空间-用户-角色-授权

    1.创建数据表空间: SQL> create tablespace rusky_data datafile 'D:\rusky\rusky_data01,dbf' size 10M autoex ...

  10. asp.net core系列 51 Identity 授权(下)

    1.6 基于资源的授权 前面二篇中,熟悉了五种授权方式(对于上篇讲的策略授权,还有IAuthorizationPolicyProvider的自定义授权策略提供程序没有讲,后面再补充).本篇讲的授权方式 ...

随机推荐

  1. 一天五道Java面试题----第七天(mysql索引结构,各自的优劣--------->事务的基本特性和隔离级别)

    这里是参考B站上的大佬做的面试题笔记.大家也可以去看视频讲解!!! 文章目录 1 .mysql索引结构,各自的优劣 2 .索引的设计原则 3 .mysql锁的类型有哪些 4 .mysql执行计划怎么看 ...

  2. python的一些运算符

    # 1.算术运算符 print('1.算术运算符') # 1.1 + 求和 a = 10 b = 20 c = a + b print(c) print('a+b={}'.format(c)) pri ...

  3. 成熟企业级开源监控解决方案Zabbix6.2关键功能实战-上

    @ 目录 概述 定义 监控作用 使用理解 监控对象和指标 架构组成 常用监控软件分析 版本选型 俗语 安装 部署方式 部署 zabbix-agent 概述 定义 Zabbix 官网地址 https:/ ...

  4. AK/SK加密认证

    AK/SK认证的实现 AK/SK概述 1.什么是AKSK ak/sk是一种身份认证方式,常用于系统间接口调用时的身份验证,其中ak为Access Key ID,sk为Secret Access Key ...

  5. elasticsearch聚合之bucket terms聚合

    目录 1. 背景 2. 前置条件 2.1 创建索引 2.2 准备数据 3. 各种聚合 3.1 统计人数最多的2个省 3.1.1 dsl 3.1.2 运行结果 3.2 统计人数最少的2个省 3.2.1 ...

  6. 自学 TypeScript 第一天 环境开发配置 及 TS 基本类型声明

    前言:  自学第一天,什么是TS ,为什么要用 TS TS 全程 Typed JavaScript at Any Scale 解释起来就是 添加了类型系统的 JavaScript, 是 JavaScr ...

  7. Crypto - Caesar I

    原题链接:http://www.wechall.net/challenge/training/crypto/caesar/index.php 告诉我们这是个古凯撒密码,让我们解...我们百度下古凯撒密 ...

  8. JDK17都出了,学点JDK11新特性

    JDK8 - 转- JDK11 -转- JDK17 JShell(JDK9开始支持) G:\dowload\JDK\JDK11\jdk-11.0.2\bin ===> C:\Windows\Sy ...

  9. tostring、(string)和 String.valueOf()

    上周遇到一个问题,只怪自己平时没注意这个细节,从数据库取数据在map集合里,取出该值是我用了.tostring的方法,一次在当取出数据为空时代码报java.lang.NullPointerExcept ...

  10. 基于训练和推理场景下的MindStudio高精度对比

    摘要:MindStudio提供精度比对功能,支持Vector比对能力. 本文分享自华为云社区<[MindStudio训练营第一季]MindStudio 高精度对比随笔>,作者:Tianyi ...