ASP.NET Core 项目简单实现身份验证及鉴权
環(huán)境
VS 2017
ASP.NET Core 2.2
目標
以相對簡單優(yōu)雅的方式實現(xiàn)用戶身份驗證和鑒權(quán),解決以下兩個問題:
無狀態(tài)的身份驗證服務(wù),使用請求頭附加訪問令牌,幾乎適用于手機、網(wǎng)頁、桌面應(yīng)用等所有客戶端
基于功能點的權(quán)限訪問控制,可以將任意功能點權(quán)限集合授予用戶或角色,無需硬編碼角色權(quán)限,非常靈活
項目準備
創(chuàng)建一個ASP.NET Core Web應(yīng)用程序
使用ASP.NET Core 2.2
模板選[空]
不啟用HTTPS
不進行身份驗證
通過NuGet安裝Swashbuckle.AspNetCore程序包,并在Startup類中啟用Swagger支持
因為這個示例項目不打算編寫前端網(wǎng)頁,所以直接使用Swagger來調(diào)試,真的很方便。
添加一個空的MVC控制器(HomeController)和一個空的API控制器(AuthController)
HomeController.Index()方法中只寫一句簡單的跳轉(zhuǎn)代碼即可:
return new RedirectResult("~/swagger");
AuthController類中隨便寫一兩個骨架方法,方便看效果。
運行項目,會自動打開瀏覽器并跳轉(zhuǎn)到Swagger頁面。
身份驗證
定義基本類型和接口
ClaimTypes 定義一些常用的聲明類型常量
IClaimsSession 表示當前會話信息的接口
ClaimsSession 會話信息實現(xiàn)類
根據(jù)聲明類型從ClaimsPrincipal.ClaimsIdentity屬性中讀取用戶ID、用戶名等信息。
實際項目中可從此類繼承或完全重新實現(xiàn)自己的Session類,以添加更多的會話信息(例如工作部門)
IToken 登錄令牌接口
包含訪問令牌、刷新令牌、令牌時效等令牌
IIdentity 身份證明接口
包含用戶基本信息及令牌信息
IAuthenticationService 驗證服務(wù)接口
抽象出來的驗證服務(wù)接口,僅規(guī)定了四個身份驗證相關(guān)的方法,如需擴展可定義由此接口派生的接口。
| Login(userName, password) | IIdentity | 根據(jù)用戶名及密碼驗證其身份,成功則返回身份證明 |
| Logout() | void | 注銷本次登錄,即使未登錄也不報錯 |
| RefreshToken(refreshToken) | Token | 刷新登錄令牌,如果當前用戶未登錄則報錯 |
| ValidateToken(accessToken) | IIdentity | 驗證訪問令牌,成功則返回身份證明 |
SimpleToken 登錄令牌的簡化實現(xiàn)
這個類提不提供都可以,實際項目中大家生成Token的算法肯定是各不相同的,提供簡單實現(xiàn)僅用于演示
編寫驗證處理器
BearerDefaults 定義了一些與身份驗證相關(guān)的常量
如:AuthenticationScheme
BearerOptions 身份驗證選項類
從AuthenticationSchemeOptions繼承而來
BearerValidatedContext 驗證結(jié)果上下文
BearerHandler 身份驗證處理器 <=?關(guān)鍵類
覆蓋了HandleAuthenticateAsync()方法,實現(xiàn)自定義的身份驗證邏輯,簡述如下:
獲取訪問令牌。從請求頭中獲取authorization信息,如果沒有則從請求的參數(shù)中獲取
如果訪問令牌為空,則終止驗證,但不報錯,直接返回AuthenticateResult.NoResult()
調(diào)用從構(gòu)造函數(shù)注入的IAuthenticationService實例的ValidateToken()方法,驗證訪問令牌是否有效,如果該方法觸發(fā)異常(例如令牌過期)則捕獲后通過AuthenticateResult.Fail()返回錯誤信息,如果該方法返回值為空(例如訪問令牌根本不存在)則返回AuthenticateResult.NoResult(),不報錯。
到這一步說明身份驗證已經(jīng)通過,而且拿到身份證明信息,根據(jù)該信息創(chuàng)建Claim數(shù)組,然后再創(chuàng)建一個包含這些Claim數(shù)據(jù)的ClaimsPrincipal實例,并將Thread.CurrentPrincipal設(shè)置為該實例。
重點:其實,HttpContext.User屬性的類型正是CurrentPrincipal,而其值應(yīng)該就是來自于Thread.CurrentPrincipal。
構(gòu)造BearerValidatedContext實例,并將其Principal屬性賦值為上面創(chuàng)建的ClaimsPrincipal實例,然后調(diào)用Success()方法,表示驗證成功。最后返回該實例的Result屬性值。
BearerExtensions 包含一些擴展方法,提供使用便利
重點在于AddBearer()方法內(nèi)調(diào)用builder.AddScheme<TOptions,THandler>()泛型方法時,分別使用了前面編寫的BearerOptions、BearerHandler類作為泛型參數(shù)。
public static AuthenticationBuilder AddBearer(...){
return builder.AddScheme<BearerOptions, BearerHandler>(...);
}
如果想要自己實現(xiàn)BearerHandler類的驗證邏輯,可以拋棄此類,重新編寫使用新Handler類的擴展方法
實現(xiàn)用戶身份驗證
說明
這部分是身份驗證的落地,實際項目中應(yīng)該將上面兩步(定義基本類型和接口、編寫驗證處理器)的代碼抽象出來,成為獨立可復(fù)用的軟件包,利用該軟件包進行身份驗證的實現(xiàn)邏輯可參照此示例代碼。
實現(xiàn)步驟
Identity 身份證明實現(xiàn)類
SampleAuthenticationService 驗證服務(wù)的簡單實現(xiàn)
出于演示方便,固化了三個用戶(admin/123456、user/123、tester/123)
AuthController 通過HTTP向前端提供驗證服務(wù)的控制器類
提供了用戶登錄、令牌刷新、令牌驗證等方法。
還需要修改項目中Startup.cs文件,添加依賴注入規(guī)則、身份驗證,并啟用身份驗證中間件。
在ConfigureServices方法內(nèi)添加代碼:
services.AddScoped<IClaimsSession, ClaimsSession>();
services.AddScoped<IAuthenticationService, SampleAuthenticationService>();
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = BearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = BearerDefaults.AuthenticationScheme;
}).AddBearer();
在Configure()方法內(nèi)添加代碼:
app.UseAuthentication();
通過Swagger測試
測試登錄功能
啟動項目,自動進入[Swagger UI]界面,點擊/api/Auth/Login方法,不修改輸入框中的內(nèi)容直接點擊[Execute]按鈕,可以見到返回401錯誤碼。
在輸入框中輸入{"userName": "admin", "password": "123456"},然后點擊[Execute]按鈕,系統(tǒng)驗證成功并返回身份證明信息。
記下訪問令牌2ad43df2c11d48a18a88441adbf4994a和刷新令牌9bbaf811ed8b4d29b638777d4f89238e
測試刷新登錄令牌
點擊/api/Auth/Refresh方法,在輸入框中輸入上面獲取到的刷新令牌9bbaf811ed8b4d29b638777d4f89238e,然后點擊[Execute]按鈕,返回401錯誤碼。原因是因為我們并未提供訪問令牌。
點擊方法名右側(cè)的[鎖]圖標,在彈出框中輸入之前獲取的訪問令牌2ad43df2c11d48a18a88441adbf4994a并點擊[Authorize]按鈕后關(guān)閉對話框,重新點擊[Execute]按鈕,成功獲取到新的登錄令牌。
測試驗證訪問令牌
點擊/api/Auth/Validate方法,在輸入框中輸入第一次獲取的到訪問令牌2ad43df2c11d48a18a88441adbf4994a,然后點擊[Execute]按鈕,返回400錯誤碼,表明發(fā)起的請求參數(shù)有誤。因為此方法是支持匿名訪問的,所以錯誤碼不會是401.
將輸入框內(nèi)容修改為新的訪問令牌f37542e162ed4855921ddf26b05c3f25,然后點擊[Execute]按鈕,驗證成功,返回了對應(yīng)的用戶身份證明信息。
權(quán)限鑒定
在ASP.NET Core項目中實現(xiàn)基于角色的授權(quán)很容易,在一些權(quán)限管理并不復(fù)雜的項目中,采取這種方式來實現(xiàn)權(quán)限鑒定簡單可行。有興趣可以參考這篇博文ASP.NET Core 認證與授權(quán)5:初識授權(quán)
但是,對于稍微復(fù)雜一些的項目,權(quán)限劃分又細又多,如果采用這種方式,要覆蓋到各種各樣的權(quán)限組合,需要在代碼中定義相當多的角色,大大增加項目維護工作,并且很不靈活。
這里借鑒ABP框架中權(quán)限鑒定的一些思想,來實現(xiàn)基于功能點的權(quán)限訪問控制。
非常感謝ASP.NET Core和ABP等諸多優(yōu)秀的開源項目,向你們致敬!
不得不說ABP框架非常優(yōu)秀,但是我并不喜歡使用它,因為我沒有能力和精力搞清楚它的詳細設(shè)計思路,而且很多功能我根本不需要。
思路
ASP.NET Core提供了一個IAuthorizationFilter接口,如果在控制器類上添加[授權(quán)過濾]特性,相應(yīng)的AuthorizationFilter類的OnAuthorization()方法會在控制器的Action之前運行,如果在該方法中設(shè)置AuthorizationFilterContext.Result為一個錯誤的response,Action將不會被調(diào)用。
基于這個思路,我們設(shè)計了以下方案:
編寫一個Attribute(特性)類,包含以下兩個屬性:
Permissions:需要檢查的權(quán)限數(shù)組
RequireAllPermissions:是否需要擁有數(shù)組中全部權(quán)限,如果為否則擁有任一權(quán)限即可
定義一個IPermissionChecker接口,在接口中定義IsGrantedAsync()方法,用于執(zhí)行權(quán)限鑒定邏輯
編寫一個AuthorizationFilterAttribute特性類(應(yīng)用目標為class),通過屬性注入IPermissionChecker實例。然后在OnAuthorization()方法內(nèi)調(diào)用IPermissionChecker實例的IsGrantedAsync()方法,如果該方法返回值為false,則返回403錯誤,否則正常放行。
編寫過濾器類及相關(guān)接口
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("未授權(quán)") { StatusCode = 403 };
}
}
配合屬性注入提供NullPermissionChecker類,在IsGrantedAsync()方法內(nèi)直接返回true。
實現(xiàn)屬性注入
做好上面的準備,我們應(yīng)該可以開始著手在項目內(nèi)應(yīng)用權(quán)限鑒定功能了,不過ASP.NET Core內(nèi)置的DI框架并不支持屬性注入,所以還得添加屬性注入的功能。
定義InjectionAttribute類,用于顯式聲明應(yīng)用了此特性的屬性將使用依賴注入
[]
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);
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);
}
}
}
還有非常關(guān)鍵的一步,在Startup.ConfigureServices()中添加下面的代碼,替換IFilterProvider接口的實現(xiàn)類為上面編寫的PropertiesAutowiredFilterProvider類
services.Replace(ServiceDescriptor.Singleton<Microsoft.AspNetCore.Mvc.Filters.IFilterProvider, PropertiesAutowiredFilterProvider>());實現(xiàn)用戶權(quán)限鑒定
終于,我們可以在項目內(nèi)應(yīng)用權(quán)限鑒定功能了。
編碼
首先,我們定義一些功能點權(quán)限常量
public static class PermissionNames{
public const string TestAdd = "Test.Add";
public const string TestEdit = "Test.Edit";
public const string TestDelete = "Test.Delete";
}
接著,添加一個新的用于測試的控制器類
[][]
[]
public class TestController : ControllerBase
{
[]
public IClaimsSession Session { get; set; }
[]
[]
public IActionResult CurrentUser() => Ok(Session?.UserName);
[]
[]
public IActionResult Get(int id)=> Ok(id);
[]
[]
[]
public IActionResult Create()=> Ok();
[]
[]
[]
public IActionResult Update()=> Ok();
[]
[]
[]
public IActionResult Patch() => Ok();
[]
[]
public IActionResult Delete(int id) => Ok();
}
在控制器類上添加了[AuthorizationFilter]特性,除了CurrentUser()方法以外,都添加了[ApiAuthorize]特性,所需的權(quán)限各不相同,為簡化測試所有的Action都直接返回OkResult。
實現(xiàn)一個用于演示的權(quán)限檢查器類
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文件,添加依賴注入規(guī)則
services.AddSingleton<IPermissionChecker, SamplePermissionChecker>();因為SamplePermissionChecker類中并沒有需要進程間隔離的數(shù)據(jù),所以使用單例模式注冊就可以了。不過這樣一來,因為該類通過構(gòu)造函數(shù)注入了IClaimsSession接口實例,在構(gòu)建Checker類實例時將觸發(fā)異常。考慮到CliamsSession類中只有方法沒有數(shù)據(jù) ,改為單例也并無妨,于是將該接口也改為單例模式注冊。
通過Swagger測試
測試未登錄時僅可訪問/api/Test/CurrentUser
測試以用戶user登錄,可以訪問/api/Test/CurrentUser和GET請求/api/Test/{id}
測試以用戶admin登錄,可以訪問除/api/Test/Add以外的接口
測試
編寫了命令行程序,用來測試前面實現(xiàn)的Web API服務(wù)。
測試不同用戶同時訪問時Session是否正確
測試方法
同時運行三個測試程序,都選擇[測試身份驗證],然后分別輸入不同的用戶身份序號,快速切換三個程序并按下回車鍵,三個測試程序會各自發(fā)起100次請求,每次請求間隔100毫秒。
例如同時打開三個命令行終端執(zhí)行:dotnet .\CustomAuthorization.test.dll
測試結(jié)果
三個測試程序從后臺服務(wù)所獲取到的當前用戶信息完成匹配。
測試以不同用戶身份訪問需要權(quán)限的接口
測試方法
預(yù)設(shè)的權(quán)限為:admin=>全部權(quán)限,user=>除Test.Add以外權(quán)限,tester=>無。
分別以admin、user、tester三個用戶身份請求/api/test下的所有接口,并模擬令牌過期的場景。
測試結(jié)果
可以見到,以過期的令牌發(fā)起請求時,后臺返回的狀態(tài)為Unauthorized,當用戶未獲得足夠的授權(quán)時后臺返回的狀態(tài)為Forbidden。
測試通過!
最后
源代碼托管在gitee.com :https://gitee.com/xant77/CustomAuthorization.WebApi
原文地址:https://www.cnblogs.com/wiseant/p/10515842.html
.NET社區(qū)新聞,深度好文,歡迎訪問公眾號文章匯總 http://www.csharpkit.com
總結(jié)
以上是生活随笔為你收集整理的ASP.NET Core 项目简单实现身份验证及鉴权的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Docker最全教程之使用Docker搭
- 下一篇: ML.NET 发布0.11版本:.NET