歡迎光臨
每天分享高質量文章

ASP.NET Core 專案簡單實現身份驗證及鑒權

環境

  • VS 2017
  • ASP.NET Core 2.2

標的

  以相對簡單優雅的方式實現用戶身份驗證和鑒權,解決以下兩個問題:

  • 無狀態的身份驗證服務,使用請求頭附加訪問令牌,幾乎適用於手機、網頁、桌面應用等所有客戶端
  • 基於功能點的權限訪問控制,可以將任意功能點權限集合授予用戶或角色,無需硬編碼角色權限,非常靈活

專案準備

  1. 創建一個ASP.NET Core Web應用程式

  • 使用ASP.NET Core 2.2
  • 模板選[空]
  • 不啟用HTTPS
  • 不進行身份驗證
  • 通過NuGet安裝Swashbuckle.AspNetCore程式包,併在Startup類中啟用Swagger支持

    因為這個示例專案不打算編寫前端網頁,所以直接使用Swagger來除錯,真的很方便。

  • 添加一個空的MVC控制器(HomeController)和一個空的API控制器(AuthController)

    HomeController.Index()方法中只寫一句簡單的跳轉代碼即可:

    return new RedirectResult("~/swagger");

    AuthController類中隨便寫一兩個骨架方法,方便看效果。

  • 運行專案,會自動打開瀏覽器並跳轉到Swagger頁面。

身份驗證

定義基本型別和接口

  1. ClaimTypes 定義一些常用的宣告型別常量

  2. IClaimsSession 表示當前會話信息的接口

  3. ClaimsSession 會話信息實現類
    根據宣告型別從ClaimsPrincipal.ClaimsIdentity屬性中讀取用戶ID、用戶名等信息。

    實際專案中可從此類繼承或完全重新實現自己的Session類,以添加更多的會話信息(例如工作部門)

  4. IToken 登錄令牌接口
    包含訪問令牌、掃清令牌、令牌時效等令牌

  5. IIdentity 身份證明接口
    包含用戶基本信息及令牌信息

  6. IAuthenticationService 驗證服務接口
    抽象出來的驗證服務接口,僅規定了四個身份驗證相關的方法,如需擴展可定義由此接口派生的接口。

    Login(userName, password) IIdentity 根據用戶名及密碼驗證其身份,成功則傳回身份證明
    Logout() void 註銷本次登錄,即使未登錄也不報錯
    RefreshToken(refreshToken) Token 掃清登錄令牌,如果當前用戶未登錄則報錯
    ValidateToken(accessToken) IIdentity 驗證訪問令牌,成功則傳回身份證明
  7. SimpleToken 登錄令牌的簡化實現

    這個類提不提供都可以,實際專案中大家生成Token的演算法肯定是各不相同的,提供簡單實現僅用於演示

編寫驗證處理器

  1. BearerDefaults 定義了一些與身份驗證相關的常量

    如:AuthenticationScheme

  2. BearerOptions 身份驗證選項類

    AuthenticationSchemeOptions繼承而來

  3. BearerValidatedContext 驗證結果背景關係

  4. BearerHandler 身份驗證處理器 <= 關鍵類

    改寫了HandleAuthenticateAsync()方法,實現自定義的身份驗證邏輯,簡述如下:

    1. 獲取訪問令牌。從請求頭中獲取authorization信息,如果沒有則從請求的引數中獲取

    2. 如果訪問令牌為空,則終止驗證,但不報錯,直接傳回AuthenticateResult.NoResult()

    3. 呼叫從建構式註入的IAuthenticationService實體的ValidateToken()方法,驗證訪問令牌是否有效,如果該方法觸發異常(例如令牌過期)則捕獲後通過AuthenticateResult.Fail()傳回錯誤信息,如果該方法傳回值為空(例如訪問令牌根本不存在)則傳回AuthenticateResult.NoResult(),不報錯。

    4. 到這一步說明身份驗證已經通過,而且拿到身份證明信息,根據該信息創建Claim陣列,然後再創建一個包含這些Claim資料的ClaimsPrincipal實體,並將Thread.CurrentPrincipal設置為該實體。

      重點:其實,HttpContext.User屬性的型別正是CurrentPrincipal,而其值應該就是來自於Thread.CurrentPrincipal

    5. 構造BearerValidatedContext實體,並將其Principal屬性賦值為上面創建的ClaimsPrincipal實體,然後呼叫Success()方法,表示驗證成功。最後傳回該實體的Result屬性值。

  5. BearerExtensions 包含一些擴展方法,提供使用便利

    重點在於AddBearer()方法內呼叫builder.AddScheme()泛型方法時,分別使用了前面編寫的BearerOptionsBearerHandler類作為泛型引數。

    public static AuthenticationBuilder AddBearer(...)
    {
        return builder.AddScheme(...);
    }

    如果想要自己實現BearerHandler類的驗證邏輯,可以拋棄此類,重新編寫使用新Handler類的擴展方法

實現用戶身份驗證

說明

  這部分是身份驗證的落地,實際專案中應該將上面兩步(定義基本型別和接口、編寫驗證處理器)的代碼抽象出來,成為獨立可復用的軟體包,利用該軟體包進行身份驗證的實現邏輯可參照此示例代碼。

實現步驟

  1. Identity 身份證明實現類

  2. SampleAuthenticationService 驗證服務的簡單實現

    出於演示方便,固化了三個用戶(admin/123456、user/123、tester/123)

  3. AuthController 通過HTTP向前端提供驗證服務的控制器類

    提供了用戶登錄、令牌掃清、令牌驗證等方法。

  4. 還需要修改專案中Startup.cs檔案,添加依賴註入規則、身份驗證,並啟用身份驗證中間件。
    ConfigureServices方法內添加代碼:

    
    services.AddScoped();
    services.AddScoped();
    
    services.AddAuthentication(options =>
    {
     options.DefaultAuthenticateScheme = BearerDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = BearerDefaults.AuthenticationScheme;
    }).AddBearer();

    Configure()方法內添加代碼:

    
    app.UseAuthentication();

通過Swagger測試

  • 測試登錄功能

    啟動專案,自動進入[Swagger UI]界面,點擊/api/Auth/Login方法,不修改輸入框中的內容直接點擊[Execute]按鈕,可以見到傳回401錯誤碼。

    在輸入框中輸入{"userName": "admin", "password": "123456"},然後點擊[Execute]按鈕,系統驗證成功並傳回身份證明信息。

記下訪問令牌2ad43df2c11d48a18a88441adbf4994a和掃清令牌9bbaf811ed8b4d29b638777d4f89238e

  • 測試掃清登錄令牌

    點擊/api/Auth/Refresh方法,在輸入框中輸入上面獲取到的掃清令牌9bbaf811ed8b4d29b638777d4f89238e,然後點擊[Execute]按鈕,傳回401錯誤碼。原因是因為我們並未提供訪問令牌。

    點擊方法名右側的[鎖]圖標,在彈出框中輸入之前獲取的訪問令牌2ad43df2c11d48a18a88441adbf4994a並點擊[Authorize]按鈕後關閉對話框,重新點擊[Execute]按鈕,成功獲取到新的登錄令牌。

  • 測試驗證訪問令牌

    點擊/api/Auth/Validate方法,在輸入框中輸入第一次獲取的到訪問令牌2ad43df2c11d48a18a88441adbf4994a,然後點擊[Execute]按鈕,傳回400錯誤碼,表明發起的請求引數有誤。因為此方法是支持匿名訪問的,所以錯誤碼不會是401.

    將輸入框內容修改為新的訪問令牌f37542e162ed4855921ddf26b05c3f25,然後點擊[Execute]按鈕,驗證成功,傳回了對應的用戶身份證明信息。

權限鑒定

  在ASP.NET Core專案中實現基於角色的授權很容易,在一些權限管理並不複雜的專案中,採取這種方式來實現權限鑒定簡單可行。有興趣可以參考這篇博文ASP.NET Core 認證與授權5:初識授權
但是,對於稍微複雜一些的專案,權限劃分又細又多,如果採用這種方式,要改寫到各種各樣的權限組合,需要在代碼中定義相當多的角色,大大增加專案維護工作,並且很不靈活。
這裡借鑒ABP框架中權限鑒定的一些思想,來實現基於功能點的權限訪問控制。
非常感謝ASP.NET Core和ABP等諸多優秀的開源專案,向你們致敬!
不得不說ABP框架非常優秀,但是我並不喜歡使用它,因為我沒有能力和精力搞清楚它的詳細設計思路,而且很多功能我根本不需要。

思路

  ASP.NET Core提供了一個IAuthorizationFilter接口,如果在控制器類上添加[授權過濾]特性,相應的AuthorizationFilter類的OnAuthorization()方法會在控制器的Action之前運行,如果在該方法中設置AuthorizationFilterContext.Result為一個錯誤的response,Action將不會被呼叫。

基於這個思路,我們設計了以下方案:

  1. 編寫一個Attribute(特性)類,包含以下兩個屬性:

    Permissions:需要檢查的權限陣列

    RequireAllPermissions:是否需要擁有陣列中全部權限,如果為否則擁有任一權限即可

  2. 定義一個IPermissionChecker接口,在接口中定義IsGrantedAsync()方法,用於執行權限鑒定邏輯

  3. 編寫一個AuthorizationFilterAttribute特性類(應用標的為class),通過屬性註入IPermissionChecker實體。然後在OnAuthorization()方法內呼叫IPermissionChecker實體的IsGrantedAsync()方法,如果該方法傳回值為false,則傳回403錯誤,否則正常放行。

編寫過濾器類及相關接口

  1. ApiAuthorizeAttribute類

        [AttributeUsage(AttributeTargets.Method)]
        public class ApiAuthorizeAttribute : Attribute, IFilterMetadata
        {
            public string[] Permissions { get; }
    
            public bool RequireAllPermissions { get; set; }
    
            public ApiAuthorizeAttribute(params string[] permissions)
            {
                Permissions = permissions;
            }
        }
  2. IPermissionChecker接口定義

        public interface IPermissionChecker
        {
            Task<bool> IsGrantedAsync(string permissionName);
        }
  3. AuthorizationFilterAttribute類

        [AttributeUsage(AttributeTargets.Class)]
        public class AuthorizationFilterAttribute : Attribute, IAuthorizationFilter
        {
            [Injection] 
            public IPermissionChecker PermissionChecker { get; set; } = NullPermissionChecker.Instance;
    
            public void OnAuthorization(AuthorizationFilterContext context)
            {
                if(存在[AllowAnonymous]特性) return;
                var authorizeAttribute = 從context.Filters中析出ApiAuthorizeAttribute
                foreach (var permission in authorizeAttribute.Permissions)
                {
                 
                 var granted = PermissionChecker.IsGrantedAsync(permission).Result;
                }
                if(檢查未通過)
                 context.Result = new ObjectResult("未授權") { StatusCode = 403 };
            }
        }
  4. 配合屬性註入提供NullPermissionChecker類,在IsGrantedAsync()方法內直接傳回true。

實現屬性註入

  做好上面的準備,我們應該可以開始著手在專案內應用權限鑒定功能了,不過ASP.NET Core內置的DI框架並不支持屬性註入,所以還得添加屬性註入的功能。

  1. 定義InjectionAttribute類,用於顯式宣告應用了此特性的屬性將使用依賴註入

    
    
    
    [AttributeUsage(AttributeTargets.Property)]
    public class InjectionAttribute : Attribute { }
  2. 添加一個PropertiesAutowiredFilterProvider類,從DefaultFilterProvider類派生

    public class PropertiesAutowiredFilterProvider : DefaultFilterProvider
    {
        private static IDictionary<string, IEnumerable> _publicPropertyCache = new Dictionary<string, IEnumerable>();
    
        public override void ProvideFilter(FilterProviderContext context, FilterItem filterItem)
        {
            base.ProvideFilter(context, filterItem); 
            var filterType = filterItem.Filter.GetType();
            if (!_publicPropertyCache.ContainsKey(filterType.FullName))
            {
                var ps=filterType.GetProperties(BindingFlags.Public|BindingFlags.Instance)
                    .Where(c => c.GetCustomAttribute() != null);
                _publicPropertyCache[filterType.FullName] = ps;
            }
    
            var injectionProperties = _publicPropertyCache[filterType.FullName];
            if (injectionProperties?.Count() == 0)
                return;
            
            var serviceProvider = context.ActionContext.HttpContext.RequestServices;
            foreach (var item in injectionProperties)
            {
                var service = serviceProvider.GetService(item.PropertyType);
                if (service == null)
                {
                    throw new InvalidOperationException($"Unable to resolve service for type '{item.PropertyType.FullName}' while attempting to activate '{filterType.FullName}'");
                }
                item.SetValue(filterItem.Filter, service);
            }
        }
    }
  3. 還有非常關鍵的一步,在Startup.ConfigureServices()中添加下麵的代碼,替換IFilterProvider接口的實現類為上面編寫的PropertiesAutowiredFilterProvider

    services.Replace(ServiceDescriptor.Singleton<Microsoft.AspNetCore.Mvc.Filters.IFilterProvider, PropertiesAutowiredFilterProvider>());

實現用戶權限鑒定

  終於,我們可以在專案內應用權限鑒定功能了。

編碼

  1. 首先,我們定義一些功能點權限常量

    public static class PermissionNames
    {
        public const string TestAdd = "Test.Add";
        public const string TestEdit = "Test.Edit";
        public const string TestDelete = "Test.Delete";
    }
  2. 接著,添加一個新的用於測試的控制器類

        [AuthorizationFilter]
        [Route("api/[controller]")]
        [ApiController]
        public class TestController : ControllerBase
        {
            [Injection]
            public IClaimsSession Session { get; set; }
    
            [HttpGet]
            [Route("[action]")]
            public IActionResult CurrentUser() => Ok(Session?.UserName);
    
            [ApiAuthorize]
            [HttpGet("{id}")]
            public IActionResult Get(int id)=> Ok(id);
    
            [ApiAuthorize(PermissionNames.TestAdd)]
            [HttpPost]
            [Route("[action]")]
            public IActionResult Create()=> Ok();
    
            [ApiAuthorize(PermissionNames.TestEdit, RequireAllPermissions = false)]
            [HttpPost]
            [Route("[action]")]
            public IActionResult Update()=> Ok();
    
            [ApiAuthorize(PermissionNames.TestAdd, PermissionNames.TestEdit, RequireAllPermissions = false)]
            [HttpPost]
            [Route("[action]")]
            public IActionResult Patch() => Ok();
    
            [ApiAuthorize(PermissionNames.TestDelete)]
            [HttpDelete("{id}")]
            public IActionResult Delete(int id) => Ok();
        }

    在控制器類上添加了[AuthorizationFilter]特性,除了CurrentUser()方法以外,都添加了[ApiAuthorize]特性,所需的權限各不相同,為簡化測試所有的Action都直接傳回OkResult

  3. 實現一個用於演示的權限檢查器類

    public class SamplePermissionChecker : IPermissionChecker
    {
        private readonly Dictionary<long, string[]> userPermissions = new Dictionary<long, string[]>
        {
            
            { 1, new[] { PermissionNames.TestAdd, PermissionNames.TestEdit, PermissionNames.TestDelete } },
            
            { 2, new[] { PermissionNames.TestEdit, PermissionNames.TestDelete } }
        };
    
        public IClaimsSession Session { get; }
    
        
        public SamplePermissionChecker(IClaimsSession session)
        {
            this.Session = session;
        }
    
        public Task<bool> IsGrantedAsync(string permissionName)
        {
            if(!userPermissions.Any(p => p.Key == Session.UserId))
                return Task.FromResult(false);
            var up = userPermissions.Where(p => p.Key == Session.UserId).First();
            var granted = up.Value.Any(permission => permission.Equals(permissionName, StringComparison.InvariantCultureIgnoreCase));
            return Task.FromResult(granted);
        }
    
    }
  4. 最後還需要修改專案中Startup.cs檔案,添加依賴註入規則

    services.AddSingleton<IPermissionChecker, SamplePermissionChecker>();

    因為SamplePermissionChecker類中並沒有需要行程間隔離的資料,所以使用單例樣式註冊就可以了。不過這樣一來,因為該類通過建構式註入了IClaimsSession接口實體,在構建Checker類實體時將觸發異常。考慮到CliamsSession類中只有方法沒有資料 ,改為單例也並無妨,於是將該接口也改為單例樣式註冊。

通過Swagger測試

  • 測試未登錄時僅可訪問/api/Test/CurrentUser

  • 測試以用戶user登錄,可以訪問/api/Test/CurrentUser和GET請求/api/Test/{id}

  • 測試以用戶admin登錄,可以訪問除/api/Test/Add以外的接口

測試

編寫了命令列程式,用來測試前面實現的Web API服務。

測試不同用戶同時訪問時Session是否正確

  • 測試方法

    同時運行三個測試程式,都選擇[測試身份驗證],然後分別輸入不同的用戶身份序號,快速切換三個程式並按下回車鍵,三個測試程式會各自發起100次請求,每次請求間隔100毫秒。

    例如同時打開三個命令列終端執行:dotnet .\CustomAuthorization.test.dll

  • 測試結果

    三個測試程式從後臺服務所獲取到的當前用戶信息完成匹配。

測試以不同用戶身份訪問需要權限的接口

  • 測試方法

    預設的權限為:admin=>全部權限,user=>除Test.Add以外權限,tester=>無。

    分別以admin、user、tester三個用戶身份請求/api/test下的所有接口,並模擬令牌過期的場景。

  • 測試結果

    可以見到,以過期的令牌發起請求時,後臺傳回的狀態為Unauthorized,當用戶未獲得足夠的授權時後臺傳回的狀態為Forbidden。

    測試通過!

最後

原始碼托管在gitee.com :https://gitee.com/xant77/CustomAuthorization.WebApi

原文地址:https://www.cnblogs.com/wiseant/p/10515842.html

赞(0)

分享創造快樂