【ASP.NET Core】按用户角色授权

asp,net,core,用户,角色,授权 · 浏览次数 : 1653

小编点评

您的代码解释非常清晰地描述了授权流程的执行轨迹。以下是一些补充和总结: * **AuthorizationApplicationModelProvider** 类在应用程序启动时通过**AuthorizationMiddleware**进行配置,并为每个控制器和动作添加相应的 AuthorizeFilter。 * **GetFilter** 方法根据 policyProvider 和 authData 生成相应的 AuthorizeFilter 对象。 * **RolesAuthorizationRequirement** 实现了 IAuthorizationHandler 接口,并通过 PassThroughAuthorizationHandler 实现了抽象类 AuthorizationHandler。 * **PassThroughAuthorizationHandler** 在 HandleAsync 方法中遍历所有指定 handler 并执行其 HandleAsync 方法,并设置 HasFailed 为 false 表示应该继续执行后续的 handler。 * **该流程只执行一个 authorize 的过滤器,因为它在 ApplicationModel 初始化时通过 AuthorizationMiddleware 配置的 AuthorizationMiddleware 进行配置。** **总结:** 代码展示了如何使用 AuthorizationApplicationModelProvider 和 RolesAuthorizationRequirement 来实现基于 Policy 的授权流程,并通过 PassThroughAuthorizationHandler 中的抽象处理器将多个过滤器组合在一起。最终,授权流程会从 Policy 中加载并执行,并最终返回给客户端。

正文

上次老周和大伙伴们分享了有关按用户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】按用户角色授权

上次老周和大伙伴们分享了有关按用户Level授权的技巧,本文咱们聊聊以用户角色来授权的事。 按用户角色授权其实更好弄,毕竟这个功能是内部集成的,多数场景下我们不需要扩展,不用自己写处理代码。从功能语义上说,授权分为按角色授权和按策略授权,而从代码本质上说,角色权授其实是包含在策略授权内的。怎么说呢?

【ASP.NET Core】按用户等级授权

验证和授权是两个独立但又存在联系的过程。验证是检查访问者的合法性,授权是校验访问者有没有权限查看资源。它们之间的联系——先验证再授权。 贯穿这两过程的是叫 Claim 的东东,可以叫它“声明”。没什么神秘的,就是由两个字符串组成的对象,一曰 type,一曰 value。type 和 value 有着

.NET周报【1月第2期 2023-01-13】

国内文章 【ASP.NET Core】按用户等级授权 https://www.cnblogs.com/tcjiaan/p/17024363.html 本文介绍了ASP.NET Core如何按照用户等级进行授权。 在 C# 9 中使用 foreach 扩展 https://www.cnblogs.co

ASP.NET Core中创建中间件的几种方式

前言 今天我们一起来盘点一下在ASP.NET Core应用程序中添加和创建中间件常见的四种方式。 中间件介绍 ASP.NET Core中间件(Middleware)是用于处理HTTP请求和响应的组件,它们被安排在请求处理管道中,并按顺序执行。中间件的设计是为了使其在请求处理管道中能够以灵活和可扩展的

推荐十个优秀的ASP.NET Core第三方中间件,你用过几个?

ASP.NET Core 作为一个强大的、跨平台的、高性能的开源框架,为开发者提供了丰富的功能和灵活的扩展性。其中,中间件(Middleware)是 ASP.NET Core 架构中的核心组件之一,它负责处理 HTTP 请求和响应的管道,允许开发者在请求和响应之间插入自定义逻辑。随着 ASP.NET

Asp-Net-Core开发笔记:使用原生的接口限流功能

前言 之前介绍过使用 AspNetCoreRateLimit 组件来实现接口限流 从 .Net7 开始,AspNetCore 开始内置限流组件,当时我们的项目还在 .Net6 所以只能用第三方的 现在都升级到 .Net8 了,当然是得来试试这个原生组件 体验后:配置使用都比较简单,不过功能也没有 A

ASP.NET Core如何禁用模型验证(或者从模型状态中移除某些属性)?

这是一篇4年前的文章:【经验分享】在ASP.NET Core中,如果禁用某个请求的模型验证? 事隔多年,又有网友问到这个问题。我就来重新整理一下,顺便扩展一下之前的解决办法。 这是一个来自网友【David】的提问。在 AppBoxCore 项目的新增用户页面,新增一个上传按钮:

Asp-Net-Core开发笔记:进一步实现非侵入性审计日志功能

前言 上次说了利用 AOP 思想实现了审计日志功能,不过有同学反馈还是无法实现完全无侵入,于是我又重构了一版新的。 回顾一下:Asp-Net-Core开发笔记:实现动态审计日志功能 现在已经可以实现对业务代码完全无侵入的审计日志了,在需要审计的接口上加上 [AuditLog] 特性,就可以记录这个接

Asp .Net Core 系列:国际化多语言配置

目录概述术语本地化器IStringLocalizer在服务类中使用本地化IStringLocalizerFactoryIHtmlLocalizerIViewLocalizer资源文件区域性回退配置 CultureProvider内置的 RequestCultureProvider实现自定义 Requ

安全机密管理:Asp.Net Core中的本地敏感数据保护技巧

前言 在我们开发过程中基本上不可或缺的用到一些敏感机密数据,比如SQL服务器的连接串或者是OAuth2的Secret等,这些敏感数据在代码中是不太安全的,我们不应该在源代码中存储密码和其他的敏感数据,一种推荐的方式是通过Asp.Net Core的机密管理器。 机密管理器 在 ASP.NET Core