1、前言
但凡業務系統,授權是繞不開的一環。見過太多隻在前端做菜單及按鈕顯隐控制,但後端裸奔的,覺着前端看不到,系統就安全,掩耳盜鈴也好,自欺欺人也罷,這裡不做評論。在.NET CORE中,也見過不少用操作過濾器來實作業務用例權限控制的,至少算是對後端做了權限控制。
但我們知道,操作過濾器,已經算是過濾器管道中最靠後的,基本上緊挨着我們控制器方法執行那裡了,本身,操作過濾器也不是做權限控制的地方,雖然本身它能達到權限控制效果。為什麼這麼說,試想下,在過濾器管道之前,還有中間件處理管道,即便是過濾器管道執行環節,操作過濾器也是最靠後的,它往前還有授權過濾器,資源過濾器等,假如我在資源過濾器中緩存了請求結果,那權限控制基本上就廢了。
說這麼多,隻想表達一點,合适的地方,合适的東西,幹合适的事兒。在.NET CORE中,官方推薦用政策去實作授權。政策授權,是在授權中間件環節執行,當然能解決上述執行流程先後順序的問題。但如果要直接應用于我們業務系統中的權限控制,恐怕遠遠不夠,因為你不可能為每個api用例建立一個角色或政策,更主要的,權限控制還要動态授予或回收的,不做擴充直接照搬,你是很難搞的。接下來,我們就來看看,如何基于core的授權機制,去實作我們傳統的使用者,角色,權限,及權限的動态授予與回收控制。
2、實作
我們先看看,菜單表概覽:
查詢中IsMenu代表是側邊欄菜單還是功能按鈕,這裡我把按鈕級别的給篩選出來了,每個按鈕菜單都代表一個業務用例,也對應我們一個控制器方法。 Code是唯一的,待會兒權限控制辨別,會采用這個字段。當然你用主鍵ID也可以,但比較難記。實際運維中,會把這些菜單按照業務配置設定給指定角色,再把指定角色配置設定給系統使用者。
接下來,定義一個Requirement,以權限碼作為主校驗對象:
public class PermissionRequirement : IAuthorizationRequirement
{
public PermissionRequirement(params string[] codes)
{
this.Codes = codes;
}
public string[] Codes { get; private set; }
}
有Requirement,自然有RequirementHandler:
1 public class PermissionHandler : AuthorizationHandler<PermissionRequirement>
2 {
3 private readonly IMenuService _menuService;
4
5 public PermissionHandler(IMenuService menuService)
6 {
7 _menuService = menuService;
8 }
9
10 protected override async Task HandleRequirementAsync(
11 AuthorizationHandlerContext context,
12 PermissionRequirement requirement)
13 {
14 var roles = context.User.Claims.FirstOrDefault(x => x.Type == ClaimTypes.Role).Value;
15 if (!string.IsNullOrWhiteSpace(roles))
16 {
17 var roleIds = roles.Split(',', StringSplitOptions.RemoveEmptyEntries)
18 .Select(x => long.Parse(x));
19 if (roleIds.Contains(1))
20 {
21 context.Succeed(requirement);
22 return;
23 }
24
25 var menus = await _menuService.GetMenusByRoleIds(roleIds.ToArray());
26 if (menus.Any())
27 {
28 var codes = menus.Select(x => x.Code.ToUpper());
29 if (requirement.Codes.Any(x => codes.Contains(x.ToUpper())))
30 {
31 context.Succeed(requirement);
32 }
33 }
34 }
35 }
36 }
邏輯比較正常,根據目前使用者角色,擷取其所有菜單權限,然後與Requirement中聲明要求的菜單權限做對比,如果含有,則放行。到這兒,大家應該都能看懂,典型的.NET CORE權限控制元件。
接下來,定義一個授權過濾器特性:
1 public class PermissionFilter : Attribute, IAsyncAuthorizationFilter
2 {
3 private readonly IAuthorizationService _authorizationService;
4 private readonly PermissionRequirement _requirement;
5
6 public PermissionFilter(IAuthorizationService authorizationService, PermissionRequirement requirement)
7 {
8 _authorizationService = authorizationService;
9 _requirement = requirement;
10 }
11
12 public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
13 {
14 var result = await _authorizationService.AuthorizeAsync(context.HttpContext.User, null, _requirement);
15
16 if (!result.Succeeded)
17 {
18 context.Result = new JsonResult("您沒有此操作權限")
19 {
20 StatusCode = (int)HttpStatusCode.Forbidden
21 };
22 }
23 }
24 }
從這兒開始,我估計有人就要看不懂了,下邊解釋下。首先,PermissionFilter是個授權過濾器,它實作了IAsyncAuthorizationFilter,是以會作為過濾器管道第一道去執行。構造函數中有2個參數,IAuthorizationService直接注入,PermissionRequirement則通過特性标記外部設定。在OnAuthorizationAsync重寫方法中,調用IAuthorizationService.AuthorizeAsync方法去做具體授權校驗工作,經此橋接,授權流程就轉到我們最開始定義的PermissionHandler去了。本身core源碼中,IAuthorizationService是在授權中間件中使用到的,這裡我借用了。注意,一旦過濾器注入了服務,那此過濾器便不再能夠直接以打标記的形式貼在控制器或方法上了,那種形式必須所有參數都直接指定。core中給出的方案,是TypeFilter,涉及到服務注入的時候。
那接下來,就是另一個重要對象了:
/// <summary>
/// 權限特性
/// </summary>
public class PermissionAttribute : TypeFilterAttribute
{
public PermissionAttribute(params string[] codes)
: base(typeof(PermissionFilter))
{
Arguments = new[] { new PermissionRequirement(codes) };
}
}
至此,各元件定義完畢,那我們使用就類似下邊這樣:
3、效果示範
guokun使用者角色:
網站管理者角色對應權限:
可以看到,是沒有勾選删除部門的,那我們用這個賬戶來删除下部門:
狀态碼403,并提示無權操作,删除動作已經被攔截了。那我們把删除權限賦予網站管理者:
接下來再來删除:
可以看到,已經删除部門成功。
3、總結
以上便是本項目權限控制的實作。認證&授權這塊兒,如果要做好,還是得把core的整套機制弄清楚,最好能把源碼過一遍,不然根本搞不清楚需要怎麼擴充,每個擴充點在什麼時機觸發及生效。