ASP.NET Core 身份驗證及鑒權 目錄 + 項目準備 + 身份驗證 定義基本類型和介面 編寫驗證處理器 實現用戶身份驗證 + 許可權鑒定 思路 編寫過濾器類及相關介面 實現屬性註入 實現用戶許可權鑒定 + 測試 環境 + VS 2017 + ASP.NET Core 2.2 目標 以相對簡單優雅 ...
ASP.NET Core 身份驗證及鑒權
目錄
環境
- 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<TOptions,THandler>()
泛型方法時,分別使用了前面編寫的BearerOptions
、BearerHandler
類作為泛型參數。public static AuthenticationBuilder AddBearer(...) { return builder.AddScheme<BearerOptions, BearerHandler>(...); }
如果想要自己實現
BearerHandler
類的驗證邏輯,可以拋棄此類,重新編寫使用新Handler類的擴展方法
實現用戶身份驗證
說明
這部分是身份驗證的落地,實際項目中應該將上面兩步(定義基本類型和介面、編寫驗證處理器)的代碼抽象出來,成為獨立可復用的軟體包,利用該軟體包進行身份驗證的實現邏輯可參照此示例代碼。
實現步驟
Identity 身份證明實現類
SampleAuthenticationService 驗證服務的簡單實現
出於演示方便,固化了三個用戶(admin/123456、user/123、tester/123)
AuthController 通過HTTP向前端提供驗證服務的控制器類
提供了用戶登錄、令牌刷新、令牌驗證等方法。
還需要修改項目中
Startup.cs
文件,添加依賴註入規則、身份驗證,並啟用身份驗證中間件。
在ConfigureServices
方法內添加代碼://添加依賴註入規則 services.AddScoped<IClaimsSession, ClaimsSession>(); services.AddScoped<IAuthenticationService, SampleAuthenticationService>(); //添加身份驗證 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類,用於顯式聲明應用了此特性的屬性將使用依賴註入
/// <summary> /// 在屬性上添加此特性,以聲明該屬性需要使用依賴註入 /// </summary> [AttributeUsage(AttributeTargets.Property)] public class InjectionAttribute : Attribute { }
添加一個
PropertiesAutowiredFilterProvider
類,從DefaultFilterProvider
類派生public class PropertiesAutowiredFilterProvider : DefaultFilterProvider { private static IDictionary<string, IEnumerable<PropertyInfo>> _publicPropertyCache = new Dictionary<string, IEnumerable<PropertyInfo>>(); public override void ProvideFilter(FilterProviderContext context, FilterItem filterItem) { base.ProvideFilter(context, filterItem); //在調用基類方法之前filterItem變數不會有值 var filterType = filterItem.Filter.GetType(); if (!_publicPropertyCache.ContainsKey(filterType.FullName)) { var ps=filterType.GetProperties(BindingFlags.Public|BindingFlags.Instance) .Where(c => c.GetCustomAttribute<InjectionAttribute>() != 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[]> { //Id=1的用戶具有Test模塊的全部功能 { 1, new[] { PermissionNames.TestAdd, PermissionNames.TestEdit, PermissionNames.TestDelete } }, //Id=2的用戶具有Test模塊的編輯和刪除功能 { 2, new[] { PermissionNames.TestEdit, PermissionNames.TestDelete } } }; public IClaimsSession Session { get; } //通過構造函數註入IClaimsSession實例,以便在許可權鑒定方法中獲取用戶信息 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
歡迎轉載,請在明顯位置給出出處及鏈接。