ASP.NET Core 認證與授權[7]:動態授權


ASP.NET Core 中基於策略的授權旨在分離授權與應用程序邏輯,它提供了靈活的策略定義模型,在一些權限固定的系統中,使用起來非常方便。但是,當要授權的資源無法預先確定,或需要將權限控制到每一個具體的操作當中時,基於策略的授權便不再適用,本章就來介紹一下如何進行動態的授權。

目錄

  1. 基於資源的授權
  2. 基於權限的授權

基於資源的授權

有些場景下,授權需要依賴於要訪問的資源,例如:每個資源通常會有一個創建者屬性,我們只允許該資源的創建者才可以對其進行編輯,刪除等操作,這就無法通過[Authorize]特性來指定授權了。因為授權過濾器會在我們的應用代碼,以及MVC的模型綁定之前執行,無法確定所訪問的資源。此時,我們需要使用基於資源的授權,下面就來演示一下具體是如何操作的。

定義資源Requirement

在基於資源的授權中,我們要判斷的是用戶是否具有針對該資源的某項操作,因此,我們先定義一個代表操作的Requirement

public class MyRequirement : IAuthorizationRequirement
{
    public string Name { get; set; }
}

可以根據實際場景來定義需要的屬性,在本示例中,只需要一個Name屬性,用來表示針對資源的操作名稱(如:增查改刪等)。

然后,我們預定義一些常用的操作,方便業務中的調用:

public static class Operations
{
    public static MyRequirement Create = new MyRequirement { Name = "Create" };
    public static MyRequirement Read = new MyRequirement { Name = "Read" };
    public static MyRequirement Update = new MyRequirement { Name = "Update" };
    public static MyRequirement Delete = new MyRequirement { Name = "Delete" };
}

上面定義的 MyRequirement 雖然很簡單,但是非常通用,因此,在 ASP.NET Core 中也內置了一個OperationAuthorizationRequirement

public class OperationAuthorizationRequirement : IAuthorizationRequirement
{
    public string Name { get; set; }
}

在實際應用中,我們可以直接使用OperationAuthorizationRequirement,而不需要再自定義 Requirement,而在這里只是為了方便理解,后續也繼續使用 MyRequirement 來演示。

實現資源授權Handler

每一個 Requirement 都需要有一個對應的 Handler,來完成授權邏輯,可以直接讓 Requirement 實現IAuthorizationHandler接口,也可以單獨定義授權Handler,在這里使用后者。

在本示例中,我們是根據資源的創建者來判斷用戶是否具有操作權限,因此,我們定義一個資源創建者的接口,而不是直接依賴於具體的資源:

public interface IDocument
{
    string Creator { get; set; }
}

然后實現我們的授權Handler:

public class DocumentAuthorizationHandler : AuthorizationHandler<OperationAuthorizationRequirement, IDocument>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, OperationAuthorizationRequirement requirement, IDocument resource)
    {
        // 如果是Admin角色就直接授權成功
        if (context.User.IsInRole("admin"))
        {
            context.Succeed(requirement);
        }
        else
        {
            // 允許任何人創建或讀取資源
            if (requirement == Operations.Create || requirement == Operations.Read)
            {
                context.Succeed(requirement);
            }
            else
            {
                // 只有資源的創建者才可以修改和刪除
                if (context.User.Identity.Name == resource.Creator)
                {
                    context.Succeed(requirement);
                }
                else
                {
                    context.Fail();
                }
            }
        }
        return Task.CompletedTask;
    }
}

在前面章節的《自定義策略》示例中,我們繼承的是AuthorizationHandler<NameAuthorizationRequirement>,而這里繼承了AuthorizationHandler<OperationAuthorizationRequirement, Document>,很明顯,比之前的多了resource參數,以便用來實現基於資源的授權。

如上,我們並沒有驗證用戶是否已登錄,以及context.User是否為空等。這是因為在 ASP.NET Core 的默認授權中,已經對這些進行了判斷,我們只需要在要授權的控制器上添加[Authorize]特性即可,無需重復性的工作。

最后,不要忘了,還需要將DocumentAuthorizationHandler注冊到DI系統中:

services.AddSingleton<IAuthorizationHandler, DocumentAuthorizationHandler>();

調用AuthorizationService

現在就可以在我們的應用代碼中調用IAuthorizationService來完成授權了,不過在此之前,我們再來回顧一下IAuthorizationService接口:

public interface IAuthorizationService
{
    Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, IEnumerable<IAuthorizationRequirement> requirements);

    Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, string policyName);
}

在《上一章》中,我們提到,使用[Authorize]設置授權時,其AuthorizationHandlerContext中的resource字段被設置為空,現在,我們將要授權的資源傳進去即可:

[Authorize]
public class DocumentsController : Controller
{
    public async Task<ActionResult> Details(int? id)
    {
        var document = _docStore.Find(id.Value);
        if (document == null)
        {
            return NotFound();
        }
        if ((await _authorizationService.AuthorizeAsync(User, document, Operations.Read)).Succeeded)
        {
            return View(document);
        }
        else
        {
            return new ForbidResult();
        }
    }

    public async Task<IActionResult> Edit(int? id)
    {
        var document = _docStore.Find(id.Value);
        if (document == null)
        {
            return NotFound();
        }
        if ((await _authorizationService.AuthorizeAsync(User, document, Operations.Update)).Succeeded)
        {
            return View(document);
        }
        else
        {
            return new ForbidReuslt();
        }
    }
}

如上,在授權失敗時,我們返回了ForbidResult,建議不要返回ChallengeResult,因為我們要明確的告訴用戶是無權訪問,而不是未登錄。

基於資源的權限非常簡單,但是每次都要在應用代碼中顯示調用IAuthorizationService,顯然比較繁瑣,我們也可以使用AOP模式,或者使用EF Core攔截器來實現,將授權驗證與業務代碼分離。

基於權限的授權

在一個通用的用戶權限管理系統中,通常每一個Action都代表一種權限,用戶擁有哪些權限也是可以動態分配的。本小節就來介紹一下在 ASP.NET Core 中,如何實現一個簡單權限管理系統。

定義權限項

首先,我們要確定我們的系統分為哪些權限項,這通常是由業務所決定的,並且是預先確定的,我們可以硬編碼在代碼中,方便統一調用:

public static class Permissions
{
    public const string User = "User";
    public const string UserCreate = "User.Create";
    public const string UserRead = "User.Read";
    public const string UserUpdate = "User.Update";
    public const string UserDelete = "User.Delete";
}

如上,我們簡單定義了“創建用戶”,“查詢用戶”,“更新用戶”,“刪除用戶”四個權限。通常會對權限項進行分組,構成一個樹形結構,這樣在展示和配置權限時,都會方便很多。在這里,使用.來表示層級進行分組,其中User權限項包含所有以User.開頭的權限。

定義權限Requirement

與基於資源的授權類似,我們同樣需要定義一個權限Requirement

public class PermissionAuthorizationRequirement : IAuthorizationRequirement
{
    public PermissionAuthorizationRequirement(string name)
    {
        Name = name;
    }

    public string Name { get; set; }
}

使用Name屬性來表示權限的名稱,與上面Permissions的常量對應。

實現權限授權Handler

然后實現與上面定義的 Requirement 對應的授權Handler:

public class PermissionAuthorizationHandler : AuthorizationHandler<PermissionAuthorizationRequirement>
{
    private readonly UserStore _userStore;

    public PermissionAuthorizationHandler(UserStore userStore)
    {
        _userStore = userStore;
    }

    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionAuthorizationRequirement requirement)
    {
        if (context.User != null)
        {
            if (context.User.IsInRole("admin"))
            {
                context.Succeed(requirement);
            }
            else
            {
                var userIdClaim = context.User.FindFirst(_ => _.Type == ClaimTypes.NameIdentifier);
                if (userIdClaim != null)
                {
                    if (_userStore.CheckPermission(int.Parse(userIdClaim.Value), requirement.Name))
                    {
                        context.Succeed(requirement);
                    }
                }
            }
        }
        return Task.CompletedTask;
    }
}

如上,把admin角色設置為內部固定角色,直接跳過授權檢查。其他角色則從Claims中取出用戶Id,然后調用CheckPermission完成授權。

權限檢查的具體邏輯就屬於業務層面的了,通常會從數據庫中查找用的的權限列表進行驗證,這里就不在多說,簡單模擬了一下:

public class UserStore
{
    private static List<User> _users = new List<User>() {
        new User {  Id=1, Name="admin", Password="111111", Role="admin", Email="admin@gmail.com", PhoneNumber="18800000000"},
        new User {  Id=2, Name="alice", Password="111111", Role="user", Email="alice@gmail.com", PhoneNumber="18800000001", Permissions = new List<UserPermission> {
                new UserPermission { UserId = 1, PermissionName = Permissions.User },
                new UserPermission { UserId = 1, PermissionName = Permissions.Role }
            }
        },
        new User {  Id=3, Name="bob", Password="111111", Role = "user", Email="bob@gmail.com", PhoneNumber="18800000002", Permissions = new List<UserPermission> {
                new UserPermission { UserId = 2, PermissionName = Permissions.UserRead },
                new UserPermission { UserId = 2, PermissionName = Permissions.RoleRead }
            }
        },
    };

    public bool CheckPermission(int userId, string permissionName)
    {
        var user = Find(userId);
        if (user == null) return false;
        return user.Permissions.Any(p => permissionName.StartsWith(p.PermissionName));
    }
}

最后,與上面示例一樣,將Handler注冊到DI系統中:

services.AddSingleton<IAuthorizationHandler, PermissionAuthorizationHandler>();

使用策略授權

那么,怎么在應用代碼中使用基於權限的授權呢?

最為簡單的,我們可以直接借助於 ASP.NET Core 的授權策略來實現基於權限的授權,因為此時並不需要資源。

services.AddAuthorization(options =>
{
    options.AddPolicy(Permissions.UserCreate, policy => policy.AddRequirements(new PermissionAuthorizationRequirement(Permissions.UserCreate)));
    options.AddPolicy(Permissions.UserRead, policy => policy.AddRequirements(new PermissionAuthorizationRequirement(Permissions.UserRead)));
    options.AddPolicy(Permissions.UserUpdate, policy => policy.AddRequirements(new PermissionAuthorizationRequirement(Permissions.UserUpdate)));
    options.AddPolicy(Permissions.UserDelete, policy => policy.AddRequirements(new PermissionAuthorizationRequirement(Permissions.UserDelete)));
});

如上,針對每一個權限項都定義一個對應的授權策略,然后,就可以在控制器中直接使用[Authorize]來完成授權:

[Authorize]
public class UserController : Controller
{
    [Authorize(Policy = Permissions.UserRead)]
    public ActionResult Index()
    {
    }

    [Authorize(Policy = Permissions.UserRead)]
    public ActionResult Details(int? id)
    {
    }

    [Authorize(Policy = Permissions.UserCreate)]
    public ActionResult Create()
    {
        return View();
    }

    [Authorize(Policy = Permissions.UserCreate)]
    [HttpPost]
    [ValidateAntiForgeryToken]
    public IActionResult Create([Bind("Title")] User user)
    {
    }
}

當然,我們也可以像基於資源的授權那樣,在應用代碼中調用IAuthorizationService完成授權,這樣做的好處是無需定義策略,但是,顯然一個一個來定義策略太過於繁瑣。

還有一種更好方式,就是使用MVC過濾器來完成對IAuthorizationService的調用,下面就來演示一下。

自定義授權過濾器

我們可以參考上一章中介紹的《AuthorizeFilter》來自定義一個權限過濾器:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class PermissionFilter : Attribute, IAsyncAuthorizationFilter
{
    public PermissionFilter(string name)
    {
        Name = name;
    }

    public string Name { get; set; }

    public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
    {
        var authorizationService = context.HttpContext.RequestServices.GetRequiredService<IAuthorizationService>();
        var authorizationResult = await authorizationService.AuthorizeAsync(context.HttpContext.User, null, new PermissionAuthorizationRequirement(Name));
        if (!authorizationResult.Succeeded)
        {
            context.Result = new ForbidResult();
        }
    }
}

上面的實現非常簡單,我們接受一個name參數,代表權限的名稱,然后將權限名稱轉化為PermissionAuthorizationRequirement,最后直接調用 authorizationService 來完成授權。

接下來,我們就可以直接在控制器中使用PermissionFilter過濾器來完成基於權限的授權了:

[Authorize]
public class UserController : Controller
{
    [PermissionFilter(Permissions.UserRead)]
    public ActionResult Index()
    {
        return View(_userStore.GetAll());
    }

    [PermissionFilter(Permissions.UserCreate)]
    public ActionResult Create()
    {
    }

    [PermissionFilter(Permissions.UserCreate)]
    [HttpPost]
    [ValidateAntiForgeryToken]
    public IActionResult Create([Bind("Title")] User user)
    {
    }

    [PermissionFilter(Permissions.UserUpdate)]
    public IActionResult Edit(int? id)
    {
    }

    [PermissionFilter(Permissions.UserUpdate)]
    [HttpPost]
    [ValidateAntiForgeryToken]
    public IActionResult Edit(int id, [Bind("Id,Title")] User user)
    {
    }
}

在視圖中使用授權

通常,在前端頁面當中,我們也需要根據用戶的權限來判斷是否顯示“添加”,“刪除”等按鈕,而不是讓用戶點擊“添加”,再提示用戶沒有權限,這在 ASP.NET Core 中實現起來也非常簡單。

我們可以直接在Razor視圖中注入IAuthorizationService來檢查用戶權限:

@inject IAuthorizationService AuthorizationService

@if ((await AuthorizationService.AuthorizeAsync(User, AuthorizationSample.Authorization.Permissions.UserCreate)).Succeeded)
{
    <p>
        <a asp-action="Create">創建</a>
    </p>
}

不過,上面的代碼是通過策略名稱來授權的,如果我們使用了上面創建的授權過濾器,而沒有定義授權策略的話,需要使用如下方式來實現:

@inject IAuthorizationService AuthorizationService

@if ((await AuthorizationService.AuthorizeAsync(User, new PermissionAuthorizationRequirement(AuthorizationSample.Authorization.Permissions.UserCreate))).Succeeded)
{
    <p>
        <a asp-action="Create">創建</a>
    </p>
}

我們也可以定義一個AuthorizationService的擴展方法,實現通過權限名稱進行授權,這里就不再多說。

我們不能因為隱藏了操作按鈕,就不在后端進行授權驗證了,就像JS的驗證一樣,前端的驗證就為了提升用戶的體驗,后端的驗證在任何時候都是必不可少的。

總結

在大多數場景下,我們只需要使用授權策略就可以應對,而在授權策略不能滿足我們的需求時,由於 ASP.NET Core 提供了一個統一的 IAuthorizationService 授權接口,這就使我們擴展起來也非常方便。ASP.NET Core 的授權部分到這來也就介紹完了,總的來說,要比ASP.NET 4.x的時候,簡單,靈活很多,可見 ASP.NET Core 不僅僅是為了跨平台,而是為了適應現代應用程序的開發方式而做出的全新的設計,我們也應該用全新的思維去學習.NET Core,踏上時代的浪潮。

本文示例代碼地址:https://github.com/RainingNight/AspNetCoreSample/tree/master/src/Functional/Authorization/AuthorizationSample


免責聲明!

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



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