ASP.NET Core 认证与授权[7]:动态授权
基于資源的授權
有些場景下,授權需要依賴于要訪問的資源,例如:每個資源通常會有一個創(chuàng)建者屬性,我們只允許該資源的創(chuàng)建者才可以對其進行編輯,刪除等操作,這就無法通過[Authorize]特性來指定授權了。因為授權過濾器會在我們的應用代碼,以及MVC的模型綁定之前執(zhí)行,無法確定所訪問的資源。此時,我們需要使用基于資源的授權,下面就來演示一下具體是如何操作的。
定義資源Requirement
在基于資源的授權中,我們要判斷的是用戶是否具有針對該資源的某項操作,因此,我們先定義一個代表操作的Requirement:
public class MyRequirement : IAuthorizationRequirement{ ??public string Name { get; set; } }
可以根據實際場景來定義需要的屬性,在本示例中,只需要一個Name屬性,用來表示針對資源的操作名稱(如:增查改刪等)。
然后,我們預定義一些常用的操作,方便業(yè)務中的調用:
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,而在這里只是為了方便理解,后續(xù)也繼續(xù)使用?MyRequirement?來演示。
實現資源授權Handler
每一個?Requirement?都需要有一個對應的?Handler,來完成授權邏輯,可以直接讓?Requirement?實現IAuthorizationHandler接口,也可以單獨定義授權Handler,在這里使用后者。
在本示例中,我們是根據資源的創(chuàng)建者來判斷用戶是否具有操作權限,因此,我們定義一個資源創(chuàng)建者的接口,而不是直接依賴于具體的資源:
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{ ? ? ? ? ? ?// 允許任何人創(chuàng)建或讀取資源if (requirement == Operations.Create || requirement == Operations.Read){context.Succeed(requirement);} ? ? ? ? ?
?else{ ? ? ? ? ? ? ? ?// 只有資源的創(chuàng)建者才可以修改和刪除if (context.User.Identity.Name == resource.Creator){context.Succeed(requirement);} ? ? ? ? ? ?
?? ?else{context.Fail();}}} ? ? ? ?return Task.CompletedTask;} }
在前面章節(jié)的《自定義策略》示例中,我們繼承的是AuthorizationHandler<NameAuthorizationRequirement>,而這里繼承了AuthorizationHandler<OperationAuthorizationRequirement, Document>,很明顯,比之前的多了resource參數,以便用來實現基于資源的授權。
如上,我們并沒有驗證用戶是否已登錄,以及context.User是否為空等。這是因為在 ASP.NET Core 的默認授權中,已經對這些進行了判斷,我們只需要在要授權的控制器上添加[Authorize]特性即可,無需重復性的工作。
最后,不要忘了,還需要將DocumentAuthorizationHandler注冊到DI系統(tǒng)中:
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攔截器來實現,將授權驗證與業(yè)務代碼分離。
基于權限的授權
在一個通用的用戶權限管理系統(tǒng)中,通常每一個Action都代表一種權限,用戶擁有哪些權限也是可以動態(tài)分配的。本小節(jié)就來介紹一下在 ASP.NET Core 中,如何實現一個簡單權限管理系統(tǒng)。
定義權限項
首先,我們要確定我們的系統(tǒng)分為哪些權限項,這通常是由業(yè)務所決定的,并且是預先確定的,我們可以硬編碼在代碼中,方便統(tǒng)一調用:
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"; }
如上,我們簡單定義了“創(chuàng)建用戶”,“查詢用戶”,“更新用戶”,“刪除用戶”四個權限。通常會對權限項進行分組,構成一個樹形結構,這樣在展示和配置權限時,都會方便很多。在這里,使用.來表示層級進行分組,其中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完成授權。
權限檢查的具體邏輯就屬于業(yè)務層面的了,通常會從數據庫中查找用的的權限列表進行驗證,這里就不在多說,簡單模擬了一下:
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系統(tǒng)中:
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">創(chuàng)建</a></p>}不過,上面的代碼是通過策略名稱來授權的,如果我們使用了上面創(chuàng)建的授權過濾器,而沒有定義授權策略的話,需要使用如下方式來實現:
@inject IAuthorizationService AuthorizationService@if ((await AuthorizationService.AuthorizeAsync(User, new PermissionAuthorizationRequirement(AuthorizationSample.Authorization.Permissions.UserCreate))).Succeeded) { ? ?<p><a asp-action="Create">創(chuàng)建</a></p>}我們也可以定義一個AuthorizationService的擴展方法,實現通過權限名稱進行授權,這里就不再多說。
我們不能因為隱藏了操作按鈕,就不在后端進行授權驗證了,就像JS的驗證一樣,前端的驗證就為了提升用戶的體驗,后端的驗證在任何時候都是必不可少的。
總結
在大多數場景下,我們只需要使用授權策略就可以應對,而在授權策略不能滿足我們的需求時,由于 ASP.NET Core 提供了一個統(tǒng)一的?IAuthorizationService?授權接口,這就使我們擴展起來也非常方便。ASP.NET Core 的授權部分到這來也就介紹完了,總的來說,要比ASP.NET 4.x的時候,簡單,靈活很多,可見 ASP.NET Core 不僅僅是為了跨平臺,而是為了適應現代應用程序的開發(fā)方式而做出的全新的設計,我們也應該用全新的思維去學習.NET Core,踏上時代的浪潮。
相關文章:
-
ASP.NET Core 認證與授權[4]:JwtBearer認證
-
ASP.NET Core 認證與授權[2]:Cookie認證
-
ASP.NET Core 認證與授權[3]:OAuth & OpenID Connect認證
-
Asp.Net Core 2.0 多角色權限認證
-
asp.net core 2.0 web api基于JWT自定義策略授權
-
ASP.NET Core 認證與授權[5]:初識授權
-
ASP.NET Core 認證與授權[6]:授權策略是怎么執(zhí)行的?
原文:http://www.cnblogs.com/RainingNight/p/dynamic-authorization-in-asp-net-core.html
.NET社區(qū)新聞,深度好文,歡迎訪問公眾號文章匯總 http://www.csharpkit.com
總結
以上是生活随笔為你收集整理的ASP.NET Core 认证与授权[7]:动态授权的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 使用Identity Server 4建
- 下一篇: 改造独立部署(SCD)模式下.NET C