跟我一起學.NetCore之熟悉的介面許可權驗證不能少(Jwt)

前言

許可權管控對於一個系統來說是非常重要的,最熟悉不過的是菜單許可權和數據許可權,上一節通過Jwt實現了認證,接下來用它實現介面許可權的驗證,為什麼不是菜單許可權呢?對於前後端分離而言,稱其為介面許可權感覺比較符合場景(我是這麼理解的);數據許可權牽涉到具體業務,這裡就不說啦!

正文

對於一些比較簡單的系統,訪問角色可能只有固定的幾種,比如一些產品管理系統,通常只有管理員、維護員、用戶三種許可權,管理員擁有整個系統的許可權,維護員只能訪問產品維護相關頁面和操作,用戶只能訪問產品的一些資訊,如果類似這種情況,可以直接指定角色的方式進行許可權管控,如下:

案例程式碼直接使用上一節的項目,借用上次認證那塊程式碼(偷懶太明顯),如果沒看上一篇的小夥伴,去瞅瞅認證那塊內容([跟我一起學.NetCore之WebApi介面裸奔有風險(Jwt)](//mp.weixin.qq.com/s?__biz=MzU1MzYwMjQ5MQ==&mid=2247484105&idx=1&sn=9322333e9fc39a0a0daf2c4901cf1efd&chksm=fbf11e1dcc86970be6a815b946d43b0f6753f89d3fb6b447b21a1e10373be44cbc59e0ebfe15&scene=21#wechat_redirect)),隨便敲敲程式碼,這節要用(別說我,我是有苦衷的,想讓小夥伴多擼程式碼~~);如果只是想熟悉知識點,也可以繼續往下看的,不廢話,直接開始:

img

註:[Authorize]加在控制器上時,該控制器下所有的介面都受保護;

為了方便測試,如上圖所示,增加一個產品控制器,針對不同人員模擬了三個介面,因為介面受到保護,只能通過獲取到的Token才能正常訪問,這裡就不截圖演示了。Token調用User中的Login介面獲取,詳細請參考(跟我一起學.NetCore之WebApi介面裸奔有風險(Jwt))。哎呀,還是上個生成Token的程式碼圖:

img

下面將三個介面指定為不同角色訪問,然後運行訪問如下:

img

img

通過運行測試可知,當增加了對應角色要求之後,儘快Token驗證正確,也不能正常調用介面,返回403禁止訪問。已經為介面指定了角色,那要如何才能正常調用呢?其實只要在生成Token的時候指定對應角色即可正常訪問對應角色的介面,如下程式碼優化:

img

img

如上運行所示,在生成Token時指定了角色為Admin,則這個Token只能訪問指定角色為Admin的介面,其他介面是不能訪問的。如需要訪問其他介面,同樣需要在生成Token的時候添加對應的角色,如下:

img

運行效果這裡就不截圖演示了,小夥伴們自己試試。

看到這,小夥伴們肯定會問,管理員角色肯定是所有介面都能訪問,拿到了Token接下來該咋辦,每個介面都加管理員角色對應的特性嗎?對於多個角色訪問同一個介面的情況,一般會為授權定義策略來實現,如下:

img

運行效果如下:

img

小注意點:

  • 多個角色或運算:多個角色只要有其中一個就可以訪問;

  • 多個角色且運算:同時得有多個角色才能訪問介面;

    img

到這,相信小夥伴已經忍不住要問:不管是角色還是策略的方式,角色都寫死了,如果角色動態分配許可權咋搞? 是的,上面的方式只適合對許可權管控比較簡單的項目,絕大數的項目許可權肯定是動態分配的,即根據需求,可以針對用戶進行訪問許可權配置,所以接下來就說說這塊咋搞。

這次增加的程式碼稍微有點多,程式碼都有注釋,另外跟著我標註的步驟走,絕對No Problem:

img

img

img

通過以上步驟就完成動態許可權的驗證了,是不是很給力,這裡需要注意一下幾個點:

  • 後續用到IHttpContextAccessor需要進行註冊;

    img

  • 用戶ID在登錄的時候要放入Payload中,後續許可權驗證時要用;

    img

  • API中使用的策略名稱要和定義的一致;

    img

以上案例演示中,在登錄的時候模擬許可權數據存入記憶體,對於一些用戶數據不大的項目,這種方式還不錯,但是對於用戶數量比較大或者分散式部署的項目,建議將許可權數據存入Redis等快取資料庫中,存取統一的同時也能減少對資料庫的訪問壓力,總不能每次請求過來都從資料庫中獲取許可權數據進行校驗。

小知識點:

  • 通過獲取用戶時,訪問的介面必須要有Authorize特性,否則只能通過自己解析Token獲取;
  • 如果在統一受保護的控制器中,有個別介面不需要許可權驗證,可以為其標註AllowAnonymous特性即可;

校驗許可權邏輯的完整程式碼如下:

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace SwaggerDemo.Permission
{
    /// <summary>
    /// 許可權處理的關鍵類
    /// </summary>
    public class PermissionHandler : AuthorizationHandler<PermissionRequirement>
    {
        /// <summary>
        /// 通過IHttpContextAccessor可以獲取HttpContext相關資訊,但一定要註冊服務
        /// </summary>
        private readonly IHttpContextAccessor _accessor;
        /// <summary>
        /// 用於判斷請求是否帶有憑據和是否登錄
        /// </summary>
        public IAuthenticationSchemeProvider Scheme { get; set; }
        /// <summary>
        ///  構造函數注入
        /// </summary>
        public PermissionHandler(IHttpContextAccessor accessor,IAuthenticationSchemeProvider scheme)
        {
            this._accessor = accessor;
            Scheme = scheme;
        }
        protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement)
        {
            try
            {
                //拿到HttpContext就無所不能啦
                var httpContext = _accessor.HttpContext;
                //判斷資源數據中許可權列表中是否有許可權
                if (!requirement.Permissions.Any())
                {
                    //沒有直接返回無許可權,也可以重新獲取許可權,實現不退出重新登錄就可獲取最新許可權
                    context.Fail();
                }
                //判讀請求是否擁有憑據,即是否登錄
                var defaultAuthenticate = await Scheme.GetDefaultAuthenticateSchemeAsync();
                if (defaultAuthenticate == null)
                {
                    context.Fail();
                }
                var result = await httpContext.AuthenticateAsync(defaultAuthenticate.Name);
                //不為空代表登錄成功,為空登錄失敗
                if (result?.Principal != null)
                {
                    // 獲取生成Token時放在payload里的userId
                    string userId = _accessor.HttpContext.User.FindFirst("userId").Value;
                    // 獲取當前請求的地址
                    string requestUrl = httpContext.Request.Path.Value.ToLower();
                    // 從許可權表中查找當前用戶是否有當前請求地址的許可權
                    var permission = requirement.Permissions.FirstOrDefault(a => a.Url.ToLower() == requestUrl && a.UserId == userId);
                    // 如果沒找到,代表沒有許可權
                    if (permission == null)
                    {
                        context.Fail();
                    }
                    // 如果找到,就繼續往下執行
                    context.Succeed(requirement);
                }
                else
                {
                    // 獲取不到對應值就返回無許可權
          context.Fail();
                }
            }
            catch (Exception ex)
            {
                context.Fail();
            }
        }
    }
}

總結

關於許可權驗證這塊之前大多都是在MVC的授權過濾器中進行完成的, 本來想在說過濾器那塊一起說說許可權驗證的,但感覺關於Jwt的放在一塊比較合適。關於Jwt其實還有一個比較關鍵的點,就是對於Token的處理問題,比如刷新Token、手動失效Token等,這塊後續單獨整理一篇內容分享。下次先說說過濾器的執行順序。

關於程式碼,其實現在在寫案例的時候感覺已經開始複雜了,後面整理整理會放在github上,如果急需的可以私下找我,我單獨發給小夥伴。

一個被程式搞丑的帥小伙,關注”Code綜藝圈”,識別關注跟我一起學~~~

擼文不易,莫要白瞟,三連走起~~~~