環境
- VS 2017
- ASP.NET Core 2.2
標的
以相對簡單優雅的方式實現使用者身份驗證和鑒權,解決以下兩個問題:
- 無狀態的身份驗證服務,使用請求頭附加訪問令牌,幾乎適用於手機、網頁、桌面應用等所有客戶端
- 基於功能點的許可權訪問控制,可以將任意功能點許可權集合授予使用者或角色,無需硬編碼角色許可權,非常靈活
專案準備
-
建立一個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頁面。
身份驗證
定義基本型別和介面
-
ClaimTypes 定義一些常用的宣告型別常量
-
IClaimsSession 表示當前會話資訊的介面
-
ClaimsSession 會話資訊實現類
根據宣告型別從ClaimsPrincipal.ClaimsIdentity屬性中讀取使用者ID、使用者名稱等資訊。實際專案中可從此類繼承或完全重新實現自己的Session類,以新增更多的會話資訊(例如工作部門)
-
IToken 登入令牌介面
包含訪問令牌、掃清令牌、令牌時效等令牌 -
IIdentity 身份證明介面
包含使用者基本資訊及令牌資訊 -
IAuthenticationService 驗證服務介面
抽象出來的驗證服務介面,僅規定了四個身份驗證相關的方法,如需擴充套件可定義由此介面派生的介面。Login(userName, password) IIdentity 根據使用者名稱及密碼驗證其身份,成功則傳回身份證明 Logout() void 登出本次登入,即使未登入也不報錯 RefreshToken(refreshToken) Token 掃清登入令牌,如果當前使用者未登入則報錯 ValidateToken(accessToken) IIdentity 驗證訪問令牌,成功則傳回身份證明 -
SimpleToken 登入令牌的簡化實現
這個類提不提供都可以,實際專案中大家生成Token的演演算法肯定是各不相同的,提供簡單實現僅用於演示
編寫驗證處理器
-
BearerDefaults 定義了一些與身份驗證相關的常量
如:AuthenticationScheme
-
BearerOptions 身份驗證選項類
從
AuthenticationSchemeOptions
繼承而來 -
BearerValidatedContext 驗證結果背景關係
-
BearerHandler 身份驗證處理器 <= 關鍵類
改寫了
HandleAuthenticateAsync()
方法,實現自定義的身份驗證邏輯,簡述如下:-
獲取訪問令牌。從請求頭中獲取
authorization
資訊,如果沒有則從請求的引數中獲取 -
如果訪問令牌為空,則終止驗證,但不報錯,直接傳回
AuthenticateResult.NoResult()
-
呼叫從建構式註入的
IAuthenticationService
實體的ValidateToken()
方法,驗證訪問令牌是否有效,如果該方法觸發異常(例如令牌過期)則捕獲後透過AuthenticateResult.Fail()
傳回錯誤資訊,如果該方法傳回值為空(例如訪問令牌根本不存在)則傳回AuthenticateResult.NoResult()
,不報錯。 -
到這一步說明身份驗證已經透過,而且拿到身份證明資訊,根據該資訊建立
Claim
陣列,然後再建立一個包含這些Claim
資料的ClaimsPrincipal
實體,並將Thread.CurrentPrincipal設定為該實體。重點:其實,
HttpContext.User
屬性的型別正是CurrentPrincipal
,而其值應該就是來自於Thread.CurrentPrincipal
。 -
構造
BearerValidatedContext
實體,並將其Principal
屬性賦值為上面建立的ClaimsPrincipal
實體,然後呼叫Success()
方法,表示驗證成功。最後傳回該實體的Result
屬性值。
-
-
BearerExtensions 包含一些擴充套件方法,提供使用便利
重點在於
AddBearer()
方法內呼叫builder.AddScheme()
泛型方法時,分別使用了前面編寫的BearerOptions
、BearerHandler
類作為泛型引數。public static AuthenticationBuilder AddBearer(...) { return builder.AddScheme(...); }
如果想要自己實現
BearerHandler
類的驗證邏輯,可以拋棄此類,重新編寫使用新Handler類的擴充套件方法
實現使用者身份驗證
說明
這部分是身份驗證的落地,實際專案中應該將上面兩步(定義基本型別和介面、編寫驗證處理器)的程式碼抽象出來,成為獨立可復用的軟體包,利用該軟體包進行身份驗證的實現邏輯可參照此示例程式碼。
實現步驟
-
Identity 身份證明實現類
-
SampleAuthenticationService 驗證服務的簡單實現
出於演示方便,固化了三個使用者(admin/123456、user/123、tester/123)
-
AuthController 透過HTTP向前端提供驗證服務的控制器類
提供了使用者登入、令牌掃清、令牌驗證等方法。
-
還需要修改專案中
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
將不會被呼叫。
基於這個思路,我們設計了以下方案:
-
編寫一個Attribute(特性)類,包含以下兩個屬性:
Permissions:需要檢查的許可權陣列
RequireAllPermissions:是否需要擁有陣列中全部許可權,如果為否則擁有任一許可權即可
-
定義一個
IPermissionChecker
介面,在介面中定義IsGrantedAsync()
方法,用於執行許可權鑒定邏輯 -
編寫一個AuthorizationFilterAttribute特性類(應用標的為class),透過屬性註入
IPermissionChecker
實體。然後在OnAuthorization()
方法內呼叫IPermissionChecker
實體的IsGrantedAsync()
方法,如果該方法傳回值為false,則傳回403錯誤,否則正常放行。
編寫過濾器類及相關介面
-
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; } }
-
IPermissionChecker介面定義
public interface IPermissionChecker { Task<bool> IsGrantedAsync(string permissionName); }
-
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 }; } }
-
配合屬性註入提供NullPermissionChecker類,在
IsGrantedAsync()
方法內直接傳回true。
實現屬性註入
做好上面的準備,我們應該可以開始著手在專案內應用許可權鑒定功能了,不過ASP.NET Core內建的DI框架並不支援屬性註入,所以還得新增屬性註入的功能。
-
定義InjectionAttribute類,用於顯式宣告應用了此特性的屬性將使用依賴註入
[AttributeUsage(AttributeTargets.Property)] public class InjectionAttribute : Attribute { }
-
新增一個
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); } } }
-
還有非常關鍵的一步,在
Startup.ConfigureServices()
中新增下麵的程式碼,替換IFilterProvider
介面的實現類為上面編寫的PropertiesAutowiredFilterProvider
類services.Replace(ServiceDescriptor.Singleton<Microsoft.AspNetCore.Mvc.Filters.IFilterProvider, PropertiesAutowiredFilterProvider>());
實現使用者許可權鑒定
終於,我們可以在專案內應用許可權鑒定功能了。
編碼
-
首先,我們定義一些功能點許可權常量
public static class PermissionNames { public const string TestAdd = "Test.Add"; public const string TestEdit = "Test.Edit"; public const string TestDelete = "Test.Delete"; }
-
接著,新增一個新的用於測試的控制器類
[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
。 -
實現一個用於演示的許可權檢查器類
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); } }
-
最後還需要修改專案中
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