【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;
            }
        }
    }
}

看,這不就執行了嗎。

至此,咱們就知道這角色授權的流程是怎麼走的了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章