【ASP.NET Core】按用户角色授权
上次老周和大伙伴们分享了有关按用户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】按用户角色授权的更多相关文章
- asp.net core系列 49 Identity 授权(上)
一.概述 授权是指用户能够访问资源的权限,如页面数据的查看.编辑.新增.删除.导出.下载等权限.ASP.NET Core 授权提供了多种且灵活的方式,包括:Razor pages授权约定.简单授权.R ...
- asp.net core根据用户权限控制页面元素的显示
asp.net core根据用户权限控制页面元素的显示 Intro 在 web 应用中我们经常需要根据用户的不同允许用户访问不同的资源,显示不同的内容,之前做了一个 AccessControlHelp ...
- ASP.NET Core 新增用户 - ASP.NET Core 基础教程 - 简单教程,简单编程
原文:ASP.NET Core 新增用户 - ASP.NET Core 基础教程 - 简单教程,简单编程 ASP.NET Core 新增用户 上一章节我们实现了一个注册表单,但也留了一些东西还没完成, ...
- asp.net core web 添加角色管理
新建asp.net core web应用 添加RolesAdminController [Authorize(Roles = "Admin")] public class Role ...
- ASP.NET CORE API Swagger+IdentityServer4授权验证
简介 本来不想写这篇博文,但在网上找到的文章博客都没有完整配置信息,所以这里记录下. 不了解IdentityServer4的可以看看我之前写的入门博文 Swagger 官方演示地址 源码地址 配置Id ...
- Asp.Net Core中的角色
在前面介绍中我们知道了Asp.Net Core Identity中创建用户使用到的类UserManager<IdentityUser>,同样的,创建角色我们需要使用RoleManager& ...
- asp.net core系列 50 Identity 授权(中)
1.5 基于策略的授权 在上篇中,已经讲到了授权访问(authorization)的四种方式.其中Razor Pages授权约定和简单授权二种方式更像是身份认证(authentication) ,因为 ...
- ASP.NET 拼多多用户登录授权后使用code去换取access_token
一.拼多多开放平台 由于本人刚毕业进公司实习 遇到一些问题然后想通过博客来记录和分享给大家一起学习. 第一次写博客没什么经验不是写的很好 请大家多多关照 嘴下留情哈哈 谢谢! 好了 话不多说直接进入主 ...
- oracle创建表空间-用户-角色-授权
1.创建数据表空间: SQL> create tablespace rusky_data datafile 'D:\rusky\rusky_data01,dbf' size 10M autoex ...
- asp.net core系列 51 Identity 授权(下)
1.6 基于资源的授权 前面二篇中,熟悉了五种授权方式(对于上篇讲的策略授权,还有IAuthorizationPolicyProvider的自定义授权策略提供程序没有讲,后面再补充).本篇讲的授权方式 ...
随机推荐
- CJK备注
pip清华镜像库 :pip install XXX -i https://pypi.tuna.tsinghua.edu.cn/simple pip阿里巴巴镜像库:pip install XXX -i ...
- iptables入门到精通
iptables其实不是真正的防火墙,我们可以把它理解成一个客户端代理,用户通过iptables这个代理,将用户的安全设定执行到对应的"安全框架"中,这个"安全框架&qu ...
- dns隧道攻击原理及常用工具流量分析
DNS协议是一种请求应答协议,也是一种可用于应用层的隧道技术.虽然DNS流量的异常变化可能会被发现,但是在基于传统socket隧道已经濒临淘汰,TCP.UDP通信大量被安全设备拦截的大背景下,DNS. ...
- ML-梯度下降法的详细推导与代码实现
计算 对于线性回归,梯度下降法的目标就是找到一个足够好的向量\(\theta\),使代价函数\(J(\theta) = \sum_{i=1}^{m}(\hat{y}-y_{i})^{2}\)取得最小值 ...
- Django系列---理论一
教程:http://c.biancheng.net/django/ 特点 集成 ORM 组件:Django 的 Model 层自带数据库 ORM 组件,为操作不同类型的数据库提供了统一的方式. URL ...
- C#使用MathNet库来对进行曲线拟合
下面是用来求取一条直线和一条拟合曲线交点的代码 /// <summary> /// 拟合曲线所筛选的点的个数 /// </summary> const int CurveNum ...
- .NET MAUI 安卓应用开发初体验
一..NET MAUI开发环境搭建&安卓SDK和安卓模拟器安装提示网络连接失败问题解决 引言 本节目标是帮助第一次搭建.NET MAUI开发环境,在下载安卓SDK和安卓模拟器过程中一直提示网络 ...
- day28-jQuery01
jQuery01 参考文档1:jQuery API 中文文档 | jQuery API 中文在线手册 | jquery api 下载 | jquery api chm (cuishifeng.cn) ...
- 【数据库】SQL-随机生成区间内数值、日期、字符串,mock数据
〇.概述 1.参考 2.其他 一.随机生成数值 1.随机生成函数random_int() -- 随机数生成函数,int版 CREATE OR REPLACE FUNCTION random_int( ...
- Spring04:JdbcTemplate及事务控制(AOP、XML、注解)
今日内容 Spring中的JdbcTemplate 作业:Spring基于AOP的事务控制 Spring中的事务控制 基于XML的 基于注解的 一.JdbcTemplate 1.JdbcTemplat ...