【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 设置角色就行了,毕竟我们的主题是角色授权。
IActionResult Login() => Login(= = (= 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 类的源代码就知道了。
current = _cursor.GetNextFilter<IAuthorizationFilter, IAsyncAuthorizationFilter> (_authorizationContext == = = (_authorizationContext == = = != != filter = authorizationContext = task = (!=!= != filter = authorizationContext = (authorizationContext.Result != != !=
【ASP.NET Core】按用户角色授权
声明:除非特别标注,否则均为本站原创文章,转载时请以链接形式注明文章出处。如若本站内容侵犯了原著者的合法权益,可联系本站删除。




