ASP.NET Core 认证与授权[7]:动态授权
基于資源的授權(quán)
有些場(chǎng)景下,授權(quán)需要依賴于要訪問(wèn)的資源,例如:每個(gè)資源通常會(huì)有一個(gè)創(chuàng)建者屬性,我們只允許該資源的創(chuàng)建者才可以對(duì)其進(jìn)行編輯,刪除等操作,這就無(wú)法通過(guò)[Authorize]特性來(lái)指定授權(quán)了。因?yàn)槭跈?quán)過(guò)濾器會(huì)在我們的應(yīng)用代碼,以及MVC的模型綁定之前執(zhí)行,無(wú)法確定所訪問(wèn)的資源。此時(shí),我們需要使用基于資源的授權(quán),下面就來(lái)演示一下具體是如何操作的。
定義資源Requirement
在基于資源的授權(quán)中,我們要判斷的是用戶是否具有針對(duì)該資源的某項(xiàng)操作,因此,我們先定義一個(gè)代表操作的Requirement:
public class MyRequirement : IAuthorizationRequirement{ ??public string Name { get; set; } }
可以根據(jù)實(shí)際場(chǎng)景來(lái)定義需要的屬性,在本示例中,只需要一個(gè)Name屬性,用來(lái)表示針對(duì)資源的操作名稱(如:增查改刪等)。
然后,我們預(yù)定義一些常用的操作,方便業(yè)務(wù)中的調(diào)用:
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?雖然很簡(jiǎn)單,但是非常通用,因此,在 ASP.NET Core 中也內(nèi)置了一個(gè)OperationAuthorizationRequirement:
public class OperationAuthorizationRequirement : IAuthorizationRequirement{ ? ?public string Name { get; set; } }在實(shí)際應(yīng)用中,我們可以直接使用OperationAuthorizationRequirement,而不需要再自定義?Requirement,而在這里只是為了方便理解,后續(xù)也繼續(xù)使用?MyRequirement?來(lái)演示。
實(shí)現(xiàn)資源授權(quán)Handler
每一個(gè)?Requirement?都需要有一個(gè)對(duì)應(yīng)的?Handler,來(lái)完成授權(quán)邏輯,可以直接讓?Requirement?實(shí)現(xiàn)IAuthorizationHandler接口,也可以單獨(dú)定義授權(quán)Handler,在這里使用后者。
在本示例中,我們是根據(jù)資源的創(chuàng)建者來(lái)判斷用戶是否具有操作權(quán)限,因此,我們定義一個(gè)資源創(chuàng)建者的接口,而不是直接依賴于具體的資源:
public interface IDocument{ ? ?string Creator { get; set; } }然后實(shí)現(xiàn)我們的授權(quán)Handler:
public class DocumentAuthorizationHandler : AuthorizationHandler<OperationAuthorizationRequirement, IDocument> { ? ?protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, OperationAuthorizationRequirement requirement, IDocument resource) ? ?{ ? ? ? ?// 如果是Admin角色就直接授權(quán)成功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參數(shù),以便用來(lái)實(shí)現(xiàn)基于資源的授權(quán)。
如上,我們并沒(méi)有驗(yàn)證用戶是否已登錄,以及context.User是否為空等。這是因?yàn)樵?ASP.NET Core 的默認(rèn)授權(quán)中,已經(jīng)對(duì)這些進(jìn)行了判斷,我們只需要在要授權(quán)的控制器上添加[Authorize]特性即可,無(wú)需重復(fù)性的工作。
最后,不要忘了,還需要將DocumentAuthorizationHandler注冊(cè)到DI系統(tǒng)中:
services.AddSingleton<IAuthorizationHandler, DocumentAuthorizationHandler>();調(diào)用AuthorizationService
現(xiàn)在就可以在我們的應(yīng)用代碼中調(diào)用IAuthorizationService來(lái)完成授權(quán)了,不過(guò)在此之前,我們?cè)賮?lái)回顧一下IAuthorizationService接口:
public interface IAuthorizationService{ ??Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, IEnumerable<IAuthorizationRequirement> requirements); ?
??Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, string policyName); }
在《上一章》中,我們提到,使用[Authorize]設(shè)置授權(quán)時(shí),其AuthorizationHandlerContext中的resource字段被設(shè)置為空,現(xiàn)在,我們將要授權(quán)的資源傳進(jìn)去即可:
[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();}} }
如上,在授權(quán)失敗時(shí),我們返回了ForbidResult,建議不要返回ChallengeResult,因?yàn)槲覀円鞔_的告訴用戶是無(wú)權(quán)訪問(wèn),而不是未登錄。
基于資源的權(quán)限非常簡(jiǎn)單,但是每次都要在應(yīng)用代碼中顯示調(diào)用IAuthorizationService,顯然比較繁瑣,我們也可以使用AOP模式,或者使用EF Core攔截器來(lái)實(shí)現(xiàn),將授權(quán)驗(yàn)證與業(yè)務(wù)代碼分離。
基于權(quán)限的授權(quán)
在一個(gè)通用的用戶權(quán)限管理系統(tǒng)中,通常每一個(gè)Action都代表一種權(quán)限,用戶擁有哪些權(quán)限也是可以動(dòng)態(tài)分配的。本小節(jié)就來(lái)介紹一下在 ASP.NET Core 中,如何實(shí)現(xiàn)一個(gè)簡(jiǎn)單權(quán)限管理系統(tǒng)。
定義權(quán)限項(xiàng)
首先,我們要確定我們的系統(tǒng)分為哪些權(quán)限項(xiàng),這通常是由業(yè)務(wù)所決定的,并且是預(yù)先確定的,我們可以硬編碼在代碼中,方便統(tǒng)一調(diào)用:
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"; }
如上,我們簡(jiǎn)單定義了“創(chuàng)建用戶”,“查詢用戶”,“更新用戶”,“刪除用戶”四個(gè)權(quán)限。通常會(huì)對(duì)權(quán)限項(xiàng)進(jìn)行分組,構(gòu)成一個(gè)樹(shù)形結(jié)構(gòu),這樣在展示和配置權(quán)限時(shí),都會(huì)方便很多。在這里,使用.來(lái)表示層級(jí)進(jìn)行分組,其中User權(quán)限項(xiàng)包含所有以User.開(kāi)頭的權(quán)限。
定義權(quán)限Requirement
與基于資源的授權(quán)類似,我們同樣需要定義一個(gè)權(quán)限Requirement:
public class PermissionAuthorizationRequirement : IAuthorizationRequirement{ ? ?public PermissionAuthorizationRequirement(string name) ? ?{Name = name;} ? ?public string Name { get; set; } }使用Name屬性來(lái)表示權(quán)限的名稱,與上面Permissions的常量對(duì)應(yīng)。
實(shí)現(xiàn)權(quán)限授權(quán)Handler
然后實(shí)現(xiàn)與上面定義的?Requirement?對(duì)應(yīng)的授權(quán)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;} }
如上,把a(bǔ)dmin角色設(shè)置為內(nèi)部固定角色,直接跳過(guò)授權(quán)檢查。其他角色則從Claims中取出用戶Id,然后調(diào)用CheckPermission完成授權(quán)。
權(quán)限檢查的具體邏輯就屬于業(yè)務(wù)層面的了,通常會(huì)從數(shù)據(jù)庫(kù)中查找用的的權(quán)限列表進(jìn)行驗(yàn)證,這里就不在多說(shuō),簡(jiǎn)單模擬了一下:
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注冊(cè)到DI系統(tǒng)中:
services.AddSingleton<IAuthorizationHandler, PermissionAuthorizationHandler>();使用策略授權(quán)
那么,怎么在應(yīng)用代碼中使用基于權(quán)限的授權(quán)呢?
最為簡(jiǎn)單的,我們可以直接借助于 ASP.NET Core 的授權(quán)策略來(lái)實(shí)現(xiàn)基于權(quán)限的授權(quán),因?yàn)榇藭r(shí)并不需要資源。
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))); });如上,針對(duì)每一個(gè)權(quán)限項(xiàng)都定義一個(gè)對(duì)應(yīng)的授權(quán)策略,然后,就可以在控制器中直接使用[Authorize]來(lái)完成授權(quán):
[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) ? ?{} }
當(dāng)然,我們也可以像基于資源的授權(quán)那樣,在應(yīng)用代碼中調(diào)用IAuthorizationService完成授權(quán),這樣做的好處是無(wú)需定義策略,但是,顯然一個(gè)一個(gè)來(lái)定義策略太過(guò)于繁瑣。
還有一種更好方式,就是使用MVC過(guò)濾器來(lái)完成對(duì)IAuthorizationService的調(diào)用,下面就來(lái)演示一下。
自定義授權(quán)過(guò)濾器
我們可以參考上一章中介紹的《AuthorizeFilter》來(lái)自定義一個(gè)權(quán)限過(guò)濾器:
[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();}} }
上面的實(shí)現(xiàn)非常簡(jiǎn)單,我們接受一個(gè)name參數(shù),代表權(quán)限的名稱,然后將權(quán)限名稱轉(zhuǎn)化為PermissionAuthorizationRequirement,最后直接調(diào)用?authorizationService?來(lái)完成授權(quán)。
接下來(lái),我們就可以直接在控制器中使用PermissionFilter過(guò)濾器來(lái)完成基于權(quán)限的授權(quán)了:
[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) ? ?{} }?
在視圖中使用授權(quán)
通常,在前端頁(yè)面當(dāng)中,我們也需要根據(jù)用戶的權(quán)限來(lái)判斷是否顯示“添加”,“刪除”等按鈕,而不是讓用戶點(diǎn)擊“添加”,再提示用戶沒(méi)有權(quán)限,這在 ASP.NET Core 中實(shí)現(xiàn)起來(lái)也非常簡(jiǎn)單。
我們可以直接在Razor視圖中注入IAuthorizationService來(lái)檢查用戶權(quán)限:
@inject IAuthorizationService AuthorizationService@if ((await AuthorizationService.AuthorizeAsync(User, AuthorizationSample.Authorization.Permissions.UserCreate)).Succeeded) { ? ?<p><a asp-action="Create">創(chuàng)建</a></p>}不過(guò),上面的代碼是通過(guò)策略名稱來(lái)授權(quán)的,如果我們使用了上面創(chuàng)建的授權(quán)過(guò)濾器,而沒(méi)有定義授權(quán)策略的話,需要使用如下方式來(lái)實(shí)現(xiàn):
@inject IAuthorizationService AuthorizationService@if ((await AuthorizationService.AuthorizeAsync(User, new PermissionAuthorizationRequirement(AuthorizationSample.Authorization.Permissions.UserCreate))).Succeeded) { ? ?<p><a asp-action="Create">創(chuàng)建</a></p>}我們也可以定義一個(gè)AuthorizationService的擴(kuò)展方法,實(shí)現(xiàn)通過(guò)權(quán)限名稱進(jìn)行授權(quán),這里就不再多說(shuō)。
我們不能因?yàn)殡[藏了操作按鈕,就不在后端進(jìn)行授權(quán)驗(yàn)證了,就像JS的驗(yàn)證一樣,前端的驗(yàn)證就為了提升用戶的體驗(yàn),后端的驗(yàn)證在任何時(shí)候都是必不可少的。
總結(jié)
在大多數(shù)場(chǎng)景下,我們只需要使用授權(quán)策略就可以應(yīng)對(duì),而在授權(quán)策略不能滿足我們的需求時(shí),由于 ASP.NET Core 提供了一個(gè)統(tǒng)一的?IAuthorizationService?授權(quán)接口,這就使我們擴(kuò)展起來(lái)也非常方便。ASP.NET Core 的授權(quán)部分到這來(lái)也就介紹完了,總的來(lái)說(shuō),要比ASP.NET 4.x的時(shí)候,簡(jiǎn)單,靈活很多,可見(jiàn) ASP.NET Core 不僅僅是為了跨平臺(tái),而是為了適應(yīng)現(xiàn)代應(yīng)用程序的開(kāi)發(fā)方式而做出的全新的設(shè)計(jì),我們也應(yīng)該用全新的思維去學(xué)習(xí).NET Core,踏上時(shí)代的浪潮。
相關(guān)文章:
-
ASP.NET Core 認(rèn)證與授權(quán)[4]:JwtBearer認(rèn)證
-
ASP.NET Core 認(rèn)證與授權(quán)[2]:Cookie認(rèn)證
-
ASP.NET Core 認(rèn)證與授權(quán)[3]:OAuth & OpenID Connect認(rèn)證
-
Asp.Net Core 2.0 多角色權(quán)限認(rèn)證
-
asp.net core 2.0 web api基于JWT自定義策略授權(quán)
-
ASP.NET Core 認(rèn)證與授權(quán)[5]:初識(shí)授權(quán)
-
ASP.NET Core 認(rèn)證與授權(quán)[6]:授權(quán)策略是怎么執(zhí)行的?
原文:http://www.cnblogs.com/RainingNight/p/dynamic-authorization-in-asp-net-core.html
.NET社區(qū)新聞,深度好文,歡迎訪問(wèn)公眾號(hào)文章匯總 http://www.csharpkit.com
總結(jié)
以上是生活随笔為你收集整理的ASP.NET Core 认证与授权[7]:动态授权的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 使用Identity Server 4建
- 下一篇: 改造独立部署(SCD)模式下.NET C