

ASP.NET Core 6框架揭秘实例演示[40]:基于角色的授权
source link: https://www.cnblogs.com/artech/p/inside-asp-net-core-6-40.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

ASP.NET应用并没有对如何定义授权策略做硬性规定,所以我们完全根据用户具有的任意特性(如性别、年龄、学历、所在地区、宗教信仰、政治面貌等)来判断其是否具有获取目标资源或者执行目标操作的权限,但是针对角色的授权策略依然是最常用的。角色(或者用户组)实际上就是对一组权限集的描述,将一个用户添加到某个角色之中就是为了将对应的权限赋予该用户。在《使用最简洁的代码实现登录、认证和注销》中,我们提供了一个用来演示登录、认证和注销的程序,现在我们在此基础上添加基于“角色授权的部分”。(本文提供的示例演示已经同步到《ASP.NET Core 6框架揭秘-实例演示版》)
[S2801]基于“要求”的授权
[S2802]基于“策略”的授权
[S2803]将“角色”绑定到路由终结点
[S2804]将“授权策略”绑定到路由终结点
[S2801]基于“要求”的授权
我们提供的演示实例提供了IAccountService和IPageRenderer两个服务,前者用用来进行校验密钥,后者用来呈现主页和登录页面。为了在认证的时候一并将用户拥有的角色提取出来,我们按照如下的方式为IAccountService接口的Validate方法添加了表示角色列表的输出参数。对于实现类AccountService提供的三个账号来说,只有“Bar”拥有一个名为“Admin”的角色。
public interface IAccountService { bool Validate(string userName, string password, out string[] roles); } public class AccountService : IAccountService { private readonly Dictionary<string, string> _accounts = new(StringComparer.OrdinalIgnoreCase) { { "Foo", "password" }, { "Bar", "password" }, { "Baz", "password" } }; private readonly Dictionary<string, string[]> _roles = new(StringComparer.OrdinalIgnoreCase) { { "Bar", new string[]{"Admin" } } }; public bool Validate(string userName, string password, out string[] roles) { if (_accounts.TryGetValue(userName, out var pwd) && pwd == password) { roles = _roles.TryGetValue(userName, out var value) ? value : Array.Empty<string>(); return true; } roles = Array.Empty<string>(); return false; } }
我们假设演示的应用是供拥有“Admin”角色的管理人员使用的,所以只能拥有该角色的用户才能访问应用的主页,未授权访问会自动定向到我们提供的“访问拒绝”页面。我们在另一个IPageRenderer服务接口中添加了如下这个RenderAccessDeniedPage方法,并在PageRenderer类型中完成了对应的实现。
public interface IPageRenderer { IResult RenderLoginPage(string? userName = null, string? password = null, string? errorMessage = null); IResult RenderAccessDeniedPage(string userName); IResult RenderHomePage(string userName); } public class PageRenderer : IPageRenderer { public IResult RenderAccessDeniedPage(string userName) { var html = @$" <html> <head><title>Index</title></head> <body> <h3>{userName}, your access is denied.</h3> <a href='/Account/Logout'>Change another account</a> </body> </html>"; return Results.Content(html, "text/html"); } ... }
在现有的演示程序基础上,我们不需要作太大的修改。由于需要引用授权功能,我们调用了IServiceCollection接口的AddAuthorization扩展方法注册了必要的服务。由于引入了“访问决绝”页面,我们注册了对应的终结点,该终结点依然采用标准的路径“Account/AccessDenied”,对应的处理方法DenyAccess直接调用上面这个RenderAccessDeniedPage方法将该页面呈现出来。
using App; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization.Infrastructure; using System.Security.Claims; using System.Security.Principal; var builder = WebApplication.CreateBuilder(); builder.Services .AddSingleton<IPageRenderer, PageRenderer>() .AddSingleton<IAccountService, AccountService>() .AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(); builder.Services.AddAuthorization(); var app = builder.Build(); app.UseAuthentication(); app.Map("/", WelcomeAsync); app.MapGet("Account/Login", Login); app.MapPost("Account/Login", SignInAsync); app.Map("Account/Logout", SignOutAsync); app.Map("Account/AccessDenied", DenyAccess); app.Run(); Task WelcomeAsync(HttpContext context, ClaimsPrincipal user, IPageRenderer renderer, IAuthorizationService authorizationService); IResult Login(IPageRenderer renderer); Task SignInAsync(HttpContext context, HttpRequest request, IPageRenderer renderer,IAccountService accountService); Task SignOutAsync(HttpContext context); IResult DenyAccess(ClaimsPrincipal user, IPageRenderer renderer) => renderer.RenderAccessDeniedPage(user?.Identity?.Name!);
我们需要对用来认证请求的SignInAsync方法作相应的修改。如下的代码片段所示,对于成功通过认证的用户,我们会为它创建一个ClaimsPrincipal对象来表示当前用户。这个对象也是授权的目标对象,授权的本质就是确定该对象是否携带了授权资源或者操作所要求的“资质”。由于我们采用的是基于“角色”的授权,所以我们将该用于拥有的角色以“声明(Claim)”的形式添加到表示身份的ClaimsIdentity对象上。
Task SignInAsync(HttpContext context, HttpRequest request, IPageRenderer renderer,IAccountService accountService) { var username = request.Form["username"]; if (string.IsNullOrEmpty(username)) { return renderer.RenderLoginPage(null, null, "Please enter user name.").ExecuteAsync(context); } var password = request.Form["password"]; if (string.IsNullOrEmpty(password)) { return renderer.RenderLoginPage(username, null, "Please enter user password.").ExecuteAsync(context); } if (!accountService.Validate(username, password, out var roles)) { return renderer.RenderLoginPage(username, null, "Invalid user name or password.").ExecuteAsync(context); } var identity = new GenericIdentity(name: username, type: CookieAuthenticationDefaults.AuthenticationScheme); foreach (var role in roles) { identity.AddClaim(new Claim(ClaimTypes.Role, role)); } var user = new ClaimsPrincipal(identity); return context.SignInAsync(user); }
演示实例授权的效果就是让拥有“Admin”角色的用户才能访问主页,所以我们将授权实现在如下这个WelcomeAsync方法中。如果当前用户(由注入的ClaimsPrincipal对象表示)并未通过认证,我们依然调用HttpContext上下文的ChallengeAsync扩展方法返回一个“匿名请求”的质询。在确定用户通过认证的前提下,我们创建了一个RolesAuthorizationRequirement来表示主页针对授权用户的“角色要求”。授权检验通过调用注入的IAuthorizationService对象的AuthorizeAsync方法来完成,我们将代表当前用户的ClaimsPrincipal对象和包含RolesAuthorizationRequirement对象的数组作为参数。如果授权成功,主页得以正常呈现,否则我们调用HttpContext上下文的ForbidAsync扩展方法返回“权限不足”的质询,上面提供的“拒绝访问”页面将会呈现出来。
async Task WelcomeAsync(HttpContext context, ClaimsPrincipal user, IPageRenderer renderer,IAuthorizationService authorizationService) { if (user?.Identity?.IsAuthenticated ?? false) { var requirement = new RolesAuthorizationRequirement(new string[] { "admin" }); var result = await authorizationService.AuthorizeAsync( user:user, resource: null, requirements: new IAuthorizationRequirement[] { requirement }); if (result.Succeeded) { await renderer.RenderHomePage(user.Identity.Name!).ExecuteAsync(context); } else { await context.ForbidAsync(); } } else { await context.ChallengeAsync(); } }
程序启动之后,具有“Admin”权限的“Bar”用户能够正常主页,其他的用户(比如“Foo”)会自动重定向到“访问拒绝”页面,具体效果体现在图1中。
图1 针对主页的授权
[S2802]基于“策略”的授权
我们调用IAuthorizationService服务的AuthorizeAsync方法进行授权检验的时候,实际上是将授权要求定义在一个RolesAuthorizationRequirement对象中,这是一种比较烦琐的编程方式。另一种推荐的做法是在应用启动的过程中创建一系列通过AuthorizationPolicy对象表示的授权规则,并指定一个唯一的名称对它们进行全局注册,那么后续就可以针对注册的策略名称进行授权检验。如下面的代码片段所示,在调用AddAuthorization扩展方法注册授权相关服务时,我们利用作为输入参数的Action<AuthorizationOptions>对象对授权策略进行了全局注册。表示授权规策略的AuthorizationPolicy对象实际上是对基于角色“Admin”的RolesAuthorizationRequirement对象的封装,我们调用AuthorizationOptions配置选项的AddPolicy方法对授权策略进行注册,并将注册名称设置为“Home”。
using App; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization.Infrastructure; using System.Security.Claims; using System.Security.Principal; var builder = WebApplication.CreateBuilder(); builder.Services .AddSingleton<IPageRenderer, PageRenderer>() .AddSingleton<IAccountService, AccountService>() .AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(); builder.Services.AddAuthorization(AddAuthorizationPolicy); var app = builder.Build(); app.UseAuthentication(); app.Map("/", WelcomeAsync); app.MapGet("Account/Login", Login); app.MapPost("Account/Login", SignInAsync); app.Map("Account/Logout", SignOutAsync); app.Map("Account/AccessDenied", DenyAccess); app.Run(); void AddAuthorizationPolicy(AuthorizationOptions options) { var requirement = new RolesAuthorizationRequirement(new string[] { "admin" }); var requirements = new IAuthorizationRequirement[] { requirement }; var policy = new AuthorizationPolicy(requirements: requirements, authenticationSchemes: Array.Empty<string>()); options.AddPolicy("Home", policy); }
在呈现主页的WelcomeAsync方法中,我们依然调用IAuthorizationService服务的AuthorizeAsync方法来检验用户是否具有对应的权限,但这次采用的是另一个可以直接指定授权策略注册名称的AuthorizeAsync方法重载。
async Task WelcomeAsync(HttpContext context, ClaimsPrincipal user, IPageRenderer renderer, IAuthorizationService authorizationService) { if (user?.Identity?.IsAuthenticated ?? false) { var result = await authorizationService.AuthorizeAsync(user: user, policyName: "Home"); if (result.Succeeded) { await renderer.RenderHomePage(user.Identity.Name!).ExecuteAsync(context); } else { await context.ForbidAsync(); } } else { await context.ChallengeAsync(); } }
[S2803]将“角色”绑定到路由终结点
上面演示的例子都调用IAuthorizationService对象的AuthorizeAsync方法来确定指定的用户是否满足提供的授权规则,实际上针对请求的授权直接交给AuthorizationMiddleware中间件来完成,该中间件可以采用如下的方式调用UseAuthorization扩展方法进行注册。
... var builder = WebApplication.CreateBuilder(); builder.Services .AddSingleton<IPageRenderer, PageRenderer>() .AddSingleton<IAccountService, AccountService>() .AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(); builder.Services.AddAuthorization(); var app = builder.Build(); app .UseAuthentication() .UseAuthorization(); ...
当该中间件在进行授权检验的时候,会从当前终结点的元数据中提取授权规则,所以我们在注册对应终结点的时候需要提供对应的授权规则。由于WelcomeAsync方法不再需要自行完成授权检验,所以它只需要将主页呈现出来就可以了。针对“Admin”角色的授权要求直接利用标注在该方法上的AuthorizeAttribute特性来指定,该特性就是为AuthorizationMiddleware中间件提供授权规则的元数据。
[Authorize(Roles ="admin")] IResult WelcomeAsync(ClaimsPrincipal user, IPageRenderer renderer)=> renderer.RenderHomePage(user.Identity!.Name!);
[S2804]将“授权策略”绑定到路由终结点
如果在调用AddAuthorization扩展方法时已经定义了授权策略,我们也可以按照如下的方式将策略名称设置为AuthorizeAttribute特性大的Policy属性。
[Authorize(Policy = "Home")] IResult WelcomeAsync(ClaimsPrincipal user, IPageRenderer renderer) => renderer.RenderHomePage(user.Identity!.Name!);
如果采用Lambda表达式来定义终结点处理器,我们可以按照如下的方式将AuthorizeAttribute特性标注在表达式上。注册终结点的各种Map方法会返回一个IEndpointConventionBuilder对象,我们可以安装如下的方式调用它的RequireAuthorization扩展方法将AuthorizeAttribute特性作为一个IAuthorizeData对象添加到注册终结点的元数据集合。RequireAuthorization扩展方法来有一个将授权策略名称作为参数的重载。
app.Map("/",[Authorize(Roles ="admin")]ClaimsPrincipal user, IPageRenderer renderer) => renderer.RenderHomePage(user.Identity!.Name!)); app.Map("/",[Authorize(Policy = "Home")](ClaimsPrincipal user, IPageRenderer renderer) => renderer.RenderHomePage(user.Identity!.Name!)); app.Map("/", WelcomeAsync).RequireAuthorization(new AuthorizeAttribute { Roles = "Admin"}); app.Map("/", WelcomeAsync).RequireAuthorization(new AuthorizeAttribute { Policy = "Home"}); app.Map("/", WelcomeAsync).RequireAuthorization(policyNames: "Home");
Recommend
-
6
ASP.NET Core 6框架揭秘实例演示[01]: 编程初体验 作为《ASP.NET C...
-
9
ASP.NET Core 6框架揭秘实例演示[06]:依赖注入框架设计细节 由于依...
-
8
作为《ASP.NET Core 3框架揭秘》的升级版,《ASP.NET Core 6框架揭秘》提供了很多新的章节,同时对现有的内容进行大量的修改。虽然本书旨在对ASP.NET Core框架的架构设计和实现原理进行剖析,但是其中提供的
-
6
ASP.NET Core框架建立在一个依赖注入框架之上,已注入的方式消费服务已经成为了ASP.NET Core基本的编程模式。为了使读者能够更好地理解原生的注入框架框架,我按照类似的设计创建了一个简易版本的依赖注入框架,并它命名为“Cat”。本篇提供的四个实例主要体现了...
-
7
ASP.NET Core 6框架揭秘实例演示[12]:诊断跟踪的进阶用法 一个好...
-
6
毫不夸张地说,整个ASP.NET Core就是建立在依赖注入框架之上的。ASP.NET Core应用在启动时构建管道所需的服务,以及管道处理请求使用到的服务,均来源于依赖注入容器。依赖注入容器不仅为ASP.NET Core框架自身提供必要的服务,还为应用程序提供服务,依赖注入已...
-
4
我们倾向于将IConfiguration对象转换成一个具体的对象,以面向对象的方式来使用配置,我们将这个转换过程称为配置绑定。除了将配置树叶子节点配置节的绑定为某种标量对象外,我们还可以直接将一个配置节绑定为一个具有对应结构的符合对象。除此之外,配置绑定还...
-
11
ASP.NET Core可以视为一种底层框架,它为我们构建出了基于管道的请求处理模型,这个管道由一个服务器和多个中间件构成,而与路由相关的EndpointRoutingMiddleware和EndpointMiddleware是两个最为重要的中间件。MVC和gRPC开发框架就建立在路由基础上。本篇提供了...
-
6
ASP.NET Core应用具有很多读取文件的场景,如读取配置文件、静态Web资源文件(如CSS、JavaScript和图片文件等)、MVC应用的视图文件,以及直接编译到程序集中的内嵌资源文件。这些文件的读取都需要使用一个IFileProvider对象。IFileProvider对象构建了一个抽象...
-
6
《数据加解密与哈希》演示了“数据保护”框架如何用来对数据进行加解密,而“数据保护”框架的核心是“密钥管理”。数据保护框架以XML的形式来存储密钥,默认的IKeyManager实现类型为X...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK