ASP.NET Core Identity 實戰(4)授權過程


這篇文章我們將一起來學習 Asp.Net Core 中的(注:這樣描述不准確,稍后你會明白)授權過程

前情提要

在之前的文章里,我們有提到認證和授權是兩個分開的過程,而且認證過程不屬於Identity。同樣授權過程也不屬於Identity,授權過程放在Identity系列中將的原因和認證過程一樣——和成員系統放在一起容易理解。

動手做

在弄清的是授權過程在哪里發生的之前,我們先來動手寫一寫授權的代碼,如果了解策略授權,那么你可以快速瀏覽過這部分

打開之前創建的項目,添加一個名為Demo的控制器,控制器代碼如下:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace IdentityDemo.Controllers
{
    [Produces("application/json")]
    [Route("api/demo")]
    public class DemoController : Controller
    {
        [Authorize]
        [HttpGet]
        public object Get()
        {
            return new
            {
                User.Identity.Name,
                User.Identity.IsAuthenticated
                略...

用之前注冊的賬戶登錄系統,
訪問/api/demo,你將得到如下結果:

{
    "name": "jbl-2011@163.com",
    "isAuthenticated": true
}

然后退出登錄,再次訪問/api/demo,那么將會跳轉到登陸頁面,在這個過程中Authorize特性起到了至關重要的作用,接下來去掉Authorize特性,重復上兩個操作,未登錄的結果將是:

{
    "name": null,
    "isAuthenticated": false
}

通過這兩個小例子,我們很容易就能推斷出Authorize特性攔截了沒有登陸的用戶,等等,是Authorize特性攔截了請求嗎?

授權過程的發生地

很顯然,不是Authorize特性攔截了請求,特性只是標記了這個方法需要被授權才能訪問,而真正攔截了請求的是——“Mvc 中間件”。Action是由Mvc執行的,Mvc執行時會確認Action上的Authorize特性,來確定是否要進行授權操作(成功授權可以訪問,失敗了會被阻止(比如跳轉到登陸)),以及如何授權(動物園例子中,第二個門衛根據切實的情況決定),也就是自定義授權(角色等等)。

另外,如果我們只是簡單的為 Action方法打上[Authorize]標記,那么它的默認行為就是驗證IsAuthenticated是否是true,也就是在認證環節(Authentication 中間件)是否通過了認證

現在,我們知道了兩個點

  • 認證過程 Authentication 發生在 Authentication 中間件中
  • 授權過程 Authorization 發生在 Mvc中間件中

基於策略的靈活授權

在企業應用中最為常見的就是基於角色的授權,實現角色授權的方式有兩種,一種是直接寫在Authorize特性上:

[Authorize(Roles = "admin,super-admin,")]
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Test()

不過這種方式,不推薦,因為這樣的話我們就將“角色”和“Uri”的綁定“硬編碼在代碼里了”,在很多場景這顯然不合適,所以接下來我們要介紹的基於策略的授權就允許我們自定義授權邏輯,這樣就靈活多了

基於策略Policy的授權

我們假設我們的授權規則是要求和上方代碼片段實現相同效果,即用戶具有角色“admin”或者角色“super-admin”,我們來逐步實現這個目標:

第一步在 DI 中注冊一個用於我們需要的 policy

services.AddAuthorization(options =>
{
    options.AddPolicy("role-policy", policy =>
    {
        policy.AddRequirements(new RoleRequirement("admin","super-admin"));
    });
});

我們為該策略指定了一個名字role-policy,並且指定了這個策略的需求條件,需求條件主要是為了設置策略的初始值,我們可以在策略注冊時更改需求條件從而靈活控制授權。

接下來我們來編寫 RoleRequirement

public class RoleRequirement : IAuthorizationRequirement
{
    public IEnumerable<string> Roles { get;   }
    public RoleRequirement(params string[] roles)
    {
        Roles = roles ?? throw new ArgumentNullException(nameof(roles));
        略...

那我們的 RoleRequirement 主要實現的功能就是確定要包含的角色,因為要包含的角色是在構造函數中確定的,那么我們就將角色授權的邏輯(稍后介紹的Handler)和具體授權的數據分開了。

然后我們來實現RoleRequirement對應的處理程序:

public class RoleHandler : AuthorizationHandler<RoleRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RoleRequirement requirement)
    {

        foreach (var item in requirement.Roles)
        {
            if (context.User.IsInRole(item))
            {
                context.Succeed(requirement);
                return Task.CompletedTask;
            }
        }
        context.Fail();
        return Task.CompletedTask;
        略...

這個處理器的工作十分簡單就是驗證當前用戶是否在任意一個由RoleRequirement指定的角色中。在這里context.Succeed(requirement);指示授權成功,而授權失敗一般不需要調用 context.Fail();因為對於這個需求還可能有其它處理器進行處理,而此例中調用 context.Fail();可以確保授權失敗,因為RoleRequirement的處理器只有一個,所以這樣做是沒有問題的。

要注意的是剛剛提到的,我們已經將角色授權的邏輯(稍后介紹的Handler)和具體授權的數據分開了。

因為RoleHandler並不清楚要求用戶有哪些角色,RoleHandler只知道如何去驗證用戶含有哪些角色,而具體要求用戶含有哪些角色,是由 RoleRequirement 來決定的,這符合關注點分離和單一職責這兩個編程概念。

再然后,我們要將剛剛寫好的RoleHandler注冊進Di

services.AddSingleton<IAuthorizationHandler, RoleHandler>();

最后一步,更換原來的Attribute:

// [Authorize(Roles = "admin,super-admin,")]
[Authorize(Policy ="role-policy")]
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Test()

現在,一個最基本的基於策略的授權就完成了。

本文中的示例較為簡單,也並沒有使用全部的授權特性,更詳細的使用方法參考資料很多,本文也就不多做介紹。
另外你可以參考ASP.NET Core中基於策略的授權來學習更過關於策略授權的內容

授權時指定AuthenticationScheme

指定AuthenticationScheme的代碼類似這樣:

// [Authorize(Roles = "admin,super-admin,")]
[Authorize(AuthenticationSchemes ="jwt"/*注意,這里的名字取決於你添加AuthenticationHandler時的名字*/, Policy ="role-policy")]     [HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Test()

在上一篇博客ASP.NET Core Identity 實戰(3)認證過程中提到,在Authentication中間件中可以放置多個Handler,而有一個是默認激活的,那么剩下的是被動調用的,現在我們的情況就是由我們在Authorize特性中去挑選一個Handler來執行,例如我們在Authentication中間件上放置兩個Handler——CookieAuthenticationHandler和JwtAuthenticationHandler,並經CookieAuthenticationHandler指定為默認,那么我們想經由Jwt認證時怎么辦?

這里有一個重要問題就是:當HttpContext流過Authentication中間件后才到Mvc中間件,而Mvc在確認Action指定的AuthenticationHandler時,Authentication過程已經結束了

那這是怎么做到的呢?

還記的HttpContext中有一個擴展方法叫AuthenticateAsync,作為HttpContext的擴展方法也就意味着,我們可以在任何時候調用它進行認證操作。

namespace Microsoft.AspNetCore.Authentication
{
    public static class AuthenticationHttpContextExtensions
    {
        public static Task<AuthenticateResult> AuthenticateAsync(this HttpContext context);
        public static Task<AuthenticateResult> AuthenticateAsync(this HttpContext context, string scheme);
        略...

看它的第二個重載,它是指定了 AuthenticationScheme的名字的,所以在Mvc中間件探查到Attribute指定了AuthenticationScheme時,就會重新挑選指定的AuthenticationHandler再次對請求進行認證

授權的發生地——AuthorizationFilter

在舊的Asp.Net時代,我們知道MvcFilter這個東西,現在它仍然在,如果你不了解它,我建議你稍作了解,建議參考官方文檔

正如這一節的標題,授權發生在Microsoft.AspNetCore.Mvc.Authorization.AuthorizationFilter中,授權的邏輯類似這樣:

先進行認證

如果指定了scheme,那么重新認證,如果沒有,則使用之前 Authentication中間件的授權結果:

    public virtual async Task<AuthenticateResult> Microsoft.AspNetCore.Authorization.Policy.PolicyEvaluator.AuthenticateAsync(AuthorizationPolicy policy, HttpContext context)
    {
        if (policy.AuthenticationSchemes != null && policy.AuthenticationSchemes.Count > 0)
        {
            ClaimsPrincipal newPrincipal = null;
            foreach (var scheme in policy.AuthenticationSchemes)
            {
                var result = await context.AuthenticateAsync(scheme);
                if (result != null && result.Succeeded)
                {
                    newPrincipal = SecurityHelper.MergeUserPrincipal(newPrincipal, result.Principal);
                }
            }

            if (newPrincipal != null)
            {
                context.User = newPrincipal;
                return AuthenticateResult.Success(new AuthenticationTicket(newPrincipal, string.Join(";", policy.AuthenticationSchemes)));
            }
            else
            {
                context.User = new ClaimsPrincipal(new ClaimsIdentity());
                return AuthenticateResult.NoResult();
            }
        }

        return (context.User?.Identity?.IsAuthenticated ?? false) 
            ? AuthenticateResult.Success(new AuthenticationTicket(context.User, "context.User"))
            : AuthenticateResult.NoResult();
    }

這里面值得再次深入探討的是 context.AuthenticateAsync(scheme),這是在 HttpAbstractions項目中的擴展方法,它的實現是:

    public static Task<AuthenticateResult> AuthenticateAsync(this HttpContext context, string scheme) =>
        context.RequestServices.GetRequiredService<IAuthenticationService>().AuthenticateAsync(context, scheme);

IAuthenticationService我們在 Authentication中間件中也見過,Authentication中間件也是使用了IAuthenticationService,之前的文章有提到過,這也再次證明了單一原則職責,身份認證中間件負責在管道中認證,而認證本身並非是和身份認證中間件捆綁的,上一篇博客ASP.NET Core Identity 實戰(3)認證過程的最后有認證的源代碼

再進行授權

授權總共分三步

  1. 拿到IAuthorizeHandler的實例(前面我們寫了一個)(可能一個或者多個
  2. 執行授權(每個Handler都會進行授權)
  3. 沒了

這部分代碼還是很簡單的:

    public async Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, IEnumerable<IAuthorizationRequirement> requirements)
    {
        // 第一步
        var authContext = _contextFactory.CreateContext(requirements, user, resource);
        var handlers = await _handlers.GetHandlersAsync(authContext);
        // 第二部
        foreach (var handler in handlers)
        {
            await handler.HandleAsync(authContext);
            if (!_options.InvokeHandlersAfterFailure && authContext.HasFailed)
            {
                break;
            }
        }

        // 沒了(這主要是對結果進行處理)
        var result = _evaluator.Evaluate(authContext);
        if (result.Succeeded)
        {
            _logger.UserAuthorizationSucceeded(GetUserNameForLogging(user));
        }
        else
        {
            _logger.UserAuthorizationFailed(GetUserNameForLogging(user));
        }
        return result;
    }

這里面和我們在項目中寫的代碼有關就是IAuthorizeHandler的實例,在本文中,我們寫了一個RoleHandler

到此,授權過程就結束了,另外一些就是邊邊角角的知識點,比如授權之后如何操作,這些不難,就不再文中贅述了


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM