首页 > 编程语言 >ASP.NET Core 6框架揭秘实例演示[40]:基于角色的授权

ASP.NET Core 6框架揭秘实例演示[40]:基于角色的授权

时间:2023-06-25 09:57:02浏览次数:47  
标签:Core ASP string app 40 user var 授权 renderer

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中。

image

图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方法重载(S2802)。
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中间件提供授权规则的元数据(S2803)。

[Authorize(Roles ="admin")]
IResult WelcomeAsync(ClaimsPrincipal user, IPageRenderer renderer)=> renderer.RenderHomePage(user.Identity!.Name!);

[S2804]将“授权策略”绑定到路由终结点

如果在调用AddAuthorization扩展方法时已经定义了授权策略,我们也可以按照如下的方式将策略名称设置为AuthorizeAttribute特性大的Policy属性(S2804)。

[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");

标签:Core,ASP,string,app,40,user,var,授权,renderer
From: https://www.cnblogs.com/artech/p/inside-asp-net-core-6-40.html

相关文章

  • 使用libavcodec将mp3音频文件解码为pcm音频采样数据【[mp3float @ 0x561c1ec49940] He
    一.打开和关闭输入文件和输出文件想要解决上面提到的问题,我们需要对mp3文件的格式有个大致了解,为了方便讲解,我这里画了个示意图:ID3V2包含了作者,作曲,专辑等信息,长度不固定,扩展了ID3V1的信息量。Frame一系列的帧,个数由文件大小和帧长决定ID3V1包含了作者,作曲,专......
  • mysql8 执行聚合函数报错:Error 1140: In aggregated query without GROUP BY,sql_mode
    解决办法:setglobalsql_mode='STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION';SETGLOBALlog_bin_trust_function_creators=1;setsessionsql_mode='STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZER......
  • 基于.NetCore开发博客项目 StarBlog - (29) 开发RSS订阅功能
    前言最近忙中偷闲把博客的评论功能给做完了,我可以说这个评论功能已经达到「精致」的程度了......
  • CF1400E Clear the Multiset
    CF1400ECleartheMultiset一道经典简单的分治由贪心可知,对于一段区间[L,R],一共有两种处理方式1.一个一个减,次数为l-r+12.先区间减,直到最小的减没了,在考虑最小值隔开的两个区间。如果有多个最小值,其实也不影响,再往下分的时候一定会分开。区间答案就是$min(l-r+1,f(l,p-1)+f(......
  • 网站怎么设置404页面
    404页面是网站优化中必不可少的基础优化之一,随着网站运营时间的不断延长,网站上原来的网页内容可能会被删除,但是该网页的链接地址往往会以各种内链、外链形式存在,如果使用的是一些锚文本链接,这些文字内容可能会吸引到用户点击,而对应的页面却已经删除,此时如果没有设置404页面,那么用户......
  • ASP.NET DotnetLIMS系统全套源码
    LIMS系统功能包括:检测管理(合同管理、样品管理、样品收发管理、工作任务分配、检测结果登记、复核及审核、留样管理等)、报告管理(报告编制、审核、签发、打印等)、原始记录管理、仪器设备管理、消耗品管理、文件管理、组织人员管理、标准管理、客户供应商管理、查询统计、基础数据管理......
  • P9401
    之前的theme在我的老电脑上太卡了,就先不用了。换上了高贵的PinkRabbitCSS!gcd这个东西没啥最优化的好性质,就是一个log结构有点用。这题也用不到。先考虑枚举。注意到\(b_i,a_i\)不寻常的范围,如果选了一个\(a_i\),那么答案\(\le5\times10^5\)。特判全为\(b_i\)。......
  • P9400 「DBOI」Round 1 三班不一般 做题笔记
    题目链接最近搬运一些洛谷上的题解到这里来,一是增加我的博文数量,二是缓解一下我的博客园冷清的气氛。我的做法和题解里的做法不一样,麻烦了许多。首先看到连续的几盏灯刺眼就不行了,当然能够想到动态规划,设$f[i][j]$为看到第$i$个宿舍,末尾有连续$j$个灯刺眼,且前面的灯都合......
  • spring框架里的spring context模块介绍,它和spring core有什么关联?
    springcontext模块介绍Spring框架是一个开源的Java开发框架,它提供了一系列的功能和工具,用于简化Java应用程序的开发。SpringContext模块是Spring框架的核心部分之一,它主要负责管理和协调应用程序中的对象。SpringContext模块的主要功能包括:IoC容器(Inversi......
  • 2023年如何选购一部4000元价位的笔记本电脑(附带坑的说明)
    2023年如何选购一部4000元价位的笔记本电脑(附带坑的说明)本文是一个快速指南,不包含选购中涉及的所有知识点,尤其是大量的具体硬件参数,内容主要关注在如何快速抓住自己真正的需求,快速筛选掉不匹配的型号,从而做出适合的选择。背景条件限定:价格限制4000元+;只能在指定的电商购买;......