ASP.NET Core 中基於策略的授權旨在分離授權與應用程序邏輯,它提供了靈活的策略定義模型,在一些權限固定的系統中,使用起來非常方便。但是,當要授權的資源無法預先確定,或需要將權限控制到每一個具體的操作當中時,基於策略的授權便不再適用,本章就來介紹一下如何進行動態的授權。
目錄
基於資源的授權
有些場景下,授權需要依賴於要訪問的資源,例如:每個資源通常會有一個創建者屬性,我們只允許該資源的創建者才可以對其進行編輯,刪除等操作,這就無法通過[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,踏上時代的浪潮。
