基礎信息
1.什么是鑒權授權?
- 鑒權是驗證用戶是否擁有訪問系統的權利,授權是判斷用戶是否有權限做一些其他操作。
2.傳統的Session 和Cookie
- 主要用於無狀態請求下的的用戶身份識別,只不過Session將信息存儲在服務端,Cookie將信息存儲在客戶端。
Session
-
在客戶端第一次進行訪問時,服務端會生成一個Session id返回到客戶端
-
客戶端將Session id存儲在本地Cookie后續請求都帶上這個id
-
服務端從接收到的請求中根據Session id在自己存儲的信息中去識別客戶端信息
Cookie
-
在客戶端訪問服務器時,服務端會在響應中頒發一個Cookie
-
客戶端會把cookie存儲,當再訪問服務端時會將cookie和請求一並提交
-
服務端會檢查cookie識別客戶端,並也可以根據需要修改cookie的內容
3.存在的問題
在分布式或集群系統中使用Session
假設現在服務器為了更好的承載和容災將系統做了分布式和集群,也就是有了N個服務端,那是不是每一個服務端都要具有對每一個客戶端的Session或者Cookie的識別能力呢?
其實我們可以使用Session共享的方式用於Session的識別,通常每一個分布式系統都由不同的人負責或者跨網絡,做Session共享可能會存在諸多業務因素的影響,就算實現了系統可能也會越來越重?像這種我們可以使用Token校驗的方式,每一個客戶端登錄去向統一的鑒權平台發起,由鑒權平台頒發一個Token,然后后續各個系統的請求都帶上這個Token。
4.Token
- Token是服務端生成的一串字符串,以作客戶端進行請求的一個令牌。
執行步驟
-
用戶向統一的鑒權系統發起用戶名和密碼的校驗
-
校驗通過后會頒發一個經過簽名的Token,用戶就拿着頒發的Token去訪問其他三方系統
-
三方系統根據對應的加密方式,使用秘鑰解密Token以驗證合法性,也可以直接請求鑒權授權系統驗證當前Token的合法性(除非自己內部系統),
.NET Core中鑒權
-
Authentication:
鑒定身份信息,例如用戶有沒有登錄,用戶基本信息 -
Authorization:
判定用戶有沒有權限
1.常規的Cookie+Filter模式
-
1.基本思路
1.在控制器中登錄傳入用戶名密碼,然后寫入HttpContext.Response.Cookies。
2.定義IAuthorizationFilter攔截器,用於驗證是否有Cookie信息。
-
2.實現方式
1.在Startup中注入鑒權服務和Cookie服務
public void ConfigureServices(IServiceCollection services) { services.AddAuthentication().AddCookie(); }
2.實現自定義攔截器,並在控制器上方標記,以確保調用接口前被攔截,並實施鑒權
public class CustomAuthorizationFilterAttribute : Attribute, IAuthorizationFilter { public void OnAuthorization(AuthorizationFilterContext context) { //如果控制器上被AllowAnonymousAttribute特性標記,則不檢查 if (context.ActionDescriptor.EndpointMetadata.Any(item => item is AllowAnonymousAttribute)) { return; } //獲取Cookie中的用戶信息 string sUser = context.HttpContext.Request.Cookies["CurrentUser"]; //如果沒有Cookie直接跳到登錄頁面,否則就通過 if (sUser == null) { context.Result = new RedirectResult("~/Home/Login"); } return; } }
3.實現登錄寫入Cookie
[AllowAnonymous] public IActionResult Login(string name, string password) { //用戶名密碼不正確直接返回 if (!"Admin".Equals(name) || !"123456".Equals(name)) { return new JsonResult(new{ Result = false,Message = "登錄失敗" }); } //通過校驗向Cookie中寫入信息 base.HttpContext.Response.Cookies.Append("CurrentUser", "Admin", new CookieOptions() { Expires = DateTime.UtcNow.AddMinutes(30); }); return new JsonResult(new{ Result = true,Message = "登錄成功"}); }
2.NET Core中提供的鑒權基本介紹
在.NetCore中將鑒權和授權,分別以app.UseAuthentication()和app.UseAuthorization() 這2個不同的中間件來實現,完成鑒權主要由HttpContext的擴展類AuthenticationHttpContextExtensions中提供的方法來完成的,不用自己在手動寫Cookie或者Session。
- HttpContext.SignInAsync();
- HttpContext.AuthenticateAsync();
- HttpContext.SignOutAsync();
- HttpContext.ChallengeAsync();
- HttpContext.ForbidAsync();
-
1.其實最終調用的是實現了IAuthenticationService提供的5個核心接口方法,我們是怎么知道的呢?
public interface IAuthenticationService { //查詢鑒權 Task<AuthenticateResult> AuthenticateAsync(HttpContext context, string scheme); //登錄寫入鑒權憑證 Task SignInAsync(HttpContext context, string scheme, ClaimsPrincipal principal, AuthenticationProperties properties); //退出登錄清理憑證 Task SignOutAsync(HttpContext context, string scheme, AuthenticationProperties properties); Task ChallengeAsync(HttpContext context, string scheme, AuthenticationProperties properties); //禁止指定的身份驗證方案 Task ForbidAsync(HttpContext context, string scheme, AuthenticationProperties properties); }
-
2.我們根據Startup類中,找到注冊服務時的services.AddAuthentication()源碼,查找對應源碼不難發現,其實是往IOC中注冊了幾個接口
注入接口名稱 介紹 IAuthenticationHandlerProvider 負責對用戶憑證的驗證,提供IAuthenticationHandler處理器給IAuthenticationService用於處理鑒權請求,可以實現IAuthenticationHandler自定義處理器 IAuthenticationSchemeProvider 選擇標識使用的是哪種認證方式及策略,用於映射IAuthenticationHandler的選擇 IAuthenticationService 提供鑒權統一認證的5個核心業務接口 -
3 .我們首先找到IAuthenticationService實現類AuthenticationService中的SignInAsync方法,結合IAuthenticationHandlerProvider 和IAuthenticationSchemeProvider得到一個IAuthenticationHandler。
-
4.最終將鑒權寫入和讀取都由IAuthenticationHandler它的實例來完成,至於實例的選擇根據用戶來決定,在注入時使用AddCookie()就會注入一個CookieAuthenticationHandler,如果使用AddJwtBearer()那就會注入一個JwtBearerHandler
甚至我們可以自定義實現IAuthenticationHandler的
鑒權處理器
3.自定義IAuthenticationHandler
根據上面的內容,我們自己來擴展一個自己的IAuthenticationHandler,簡單的理解一下
-
1.繼承接口IAuthenticationHandler, IAuthenticationSignInHandler, IAuthenticationSignOutHandler 實現5個接口
public class CustomAuthenticationHandler : IAuthenticationHandler, IAuthenticationSignInHandler, IAuthenticationSignOutHandler { public AuthenticationScheme Scheme { get; private set; } protected HttpContext Context { get; private set; } public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context) { Scheme = scheme; Context = context; return Task.CompletedTask; } public async Task<AuthenticateResult> AuthenticateAsync() { var cookie = Context.Request.Cookies["CustomCookie"]; if (string.IsNullOrEmpty(cookie)) { return AuthenticateResult.NoResult(); } AuthenticateResult result = AuthenticateResult.Success(Deserialize(cookie)); return await Task.FromResult(result); } public Task ChallengeAsync(AuthenticationProperties properties) { //跳轉頁面--上端返回json //Context.Response.Redirect("/Account/Login"); return Task.CompletedTask; } public Task ForbidAsync(AuthenticationProperties properties) { Context.Response.StatusCode = 403; return Task.CompletedTask; } public Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties properties) { var ticket = new AuthenticationTicket(user, properties, Scheme.Name); Context.Response.Cookies.Append("CustomCookie", Serialize(ticket)); return Task.CompletedTask; } public Task SignOutAsync(AuthenticationProperties properties) { Context.Response.Cookies.Delete("CustomCookie"); return Task.CompletedTask; } private AuthenticationTicket Deserialize(string content) { byte[] byteTicket = System.Text.Encoding.Default.GetBytes(content); return TicketSerializer.Default.Deserialize(byteTicket); } private string Serialize(AuthenticationTicket ticket) { //需要引入 Microsoft.AspNetCore.Authentication byte[] byteTicket = TicketSerializer.Default.Serialize(ticket); return Encoding.Default.GetString(byteTicket); } }
-
2.在Startup類的IOC容器中注冊服務,並且在Scheme中加入自定義處理器,因為是自定義的IAuthenticationHandler,所以需要對應一個Scheme,以供在使用時選擇以那種方式完成
services.AddAuthentication(options => { options.AddScheme<CustomHandler>("CustomScheme", "AuthenticationHandlerScheme"); }).AddCookie();
-
3.然后在登錄時和訪問Api時分別寫入鑒權信息和查詢鑒權信息,順便介紹下寫入信息時的ClaimsIdentity對象
關鍵字 描述信息 Claims 一項信息,例如工牌的姓名是一個Claims ,工牌號碼也是一個Claims ClaimsIdentity 一組Claims 組成的信息,就是一個用戶身份信息 ClaimsPrincipal 一個用戶有多個身份 AuthenticationTicket 用戶票據,用於包裹ClaimsPrincipal -
1.寫入鑒權
[AllowAnonymous] public IActionResult Login(string name, string password) { //用戶名密碼不正確直接返回 if (!"Admin".Equals(name) || !"123456".Equals(name)) { return new JsonResult(new{ Result = false,Message = "登錄失敗" }); } var claimIdentity = new ClaimsIdentity("CustomAuthentication"); claimIdentity.AddClaim(new Claim(ClaimTypes.Name, name)); claimIdentity.AddClaim(new Claim(ClaimTypes.Email, "MyEmail@qq.com")); claimIdentity.AddClaim(new Claim(ClaimTypes.System, "EmployeeManager")); var Properties = new AuthenticationProperties { ExpiresUtc = DateTime.UtcNow.AddMinutes(30), }; //寫入鑒權信息 await base.HttpContext.SignInAsync("CustomScheme", new ClaimsPrincipal(claimIdentity),Properties ); return new JsonResult(new{ Result = true,Message = "登錄成功"}); }
-
2.查詢鑒權
//2.查詢鑒權 public async Task<IActionResult> Authentication() { //調用AuthenticateAsync查詢鑒權信息 var result = await base.HttpContext.AuthenticateAsync("CustomScheme"); if (result?.Principal != null) { base.HttpContext.User = result.Principal; return new JsonResult(new{ Result = true,Message = $"認證成功,包含用戶{base.HttpContext.User.Identity.Name}"}); } return new JsonResult(new{Result = true,Message = $"認證失敗,用戶未登錄"}); }
-
3.清除鑒權信息
//3.退出清除 public async Task<IActionResult> Logout() { //退出登錄時清除鑒權信息 await base.HttpContext.SignOutAsync("CustomScheme"); return new JsonResult(new{ Result = true,Message = "退出成功"}); }
4.使用框架提供的Cookie鑒權方式
-
1.首先在服務容器注入鑒權服務和Cookie服務支持
services.AddAuthentication(options => { options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;//不能少 options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; options.DefaultChallengeScheme = "Cookie/Login"; }) .AddCookie(options =>{});
-
2.注冊鑒權和授權中間件,用於在管道中調用攔截校驗鑒權和授權
app.UseAuthentication(); app.UseAuthorization();
-
3.在控制器引入特性 [Authorize] ,調用登錄接口時使用HttpContext.SignInAsync()寫入鑒權信息
[AllowAnonymous] public IActionResult Login(string name, string password) { //用戶名密碼不正確直接返回 if (!"Admin".Equals(name) || !"123456".Equals(name)) { return new JsonResult(new{ Result = false,Message = "登錄失敗" }); } var claimIdentity = new ClaimsIdentity("Cookie"); claimIdentity.AddClaim(new Claim(ClaimTypes.Name, name)); claimIdentity.AddClaim(new Claim(ClaimTypes.Email, "MyEmail@qq.com")); claimIdentity.AddClaim(new Claim(ClaimTypes.System, "EmployeeManager")); var Properties = new AuthenticationProperties { ExpiresUtc = DateTime.UtcNow.AddMinutes(30), }; //寫入鑒權信息 await base.HttpContext.SignInAsync(new ClaimsPrincipal(claimIdentity),Properties ); return new JsonResult(new{ Result = true,Message = "登錄成功"}); }
-
4.因為調用HttpContext.AuthenticateAsync()獲取鑒權的步驟,由第二部注冊的中間件AuthenticationMiddleware已經替我們完成,所以可以直接在控制器內部獲取HttpContext.User信息,系統提供的相對於自己實現的,框架幫我們封裝了獲取鑒權信息,並把它加入管道中,而不用每次在控制器中手動獲取鑒權信息。
public async Task<IActionResult> Authentication() { //這里由中間件管道已經實現了鑒權信息取值 var CookiesInfo = base.HttpContext.User; if (CookiesInfo != null) { return new JsonResult(new { Result = true, Message = $"鑒權認證成功,用戶已登錄" }); } return new JsonResult(new { Result = true, Message = $"鑒權認證失敗,用戶未登錄" }); }
5.Cookie鑒權的擴展
主要介紹CookieAuthenticationHandler和CookieAuthenticationOptions中的Events 和 ITicketStore
1.CookieAuthenticationHandler
-
1.CookieAuthenticationHandler處理器是在.NetCore鑒權系統中,用來處理Cookie鑒權模式的核心處理方法,在上面部分已經簡單介紹過,它是由AuthenticationBuilder的擴展類CookieExtensions注冊服務AddCookie()來提供的.
-
2.在最終AddCookie()的重載方法中,我們注冊了實現自IAuthenticationHandler的CookieAuthenticationHandler並且將CookieAuthenticationOptions委托傳入,而CookieAuthenticationOptions提供的擴展功能,能使用戶能最大限度的實現個性化定制和配置。
2.Events
-
1.Events 是一個CookieAuthenticationEvents類型的屬性,在他身上定義了委托用於給用戶在鑒權的過程中擴展自己的業務
-
2.擴展Events
public Task ExtentionEvent(CookieAuthenticationOptions cookieAuthenticationOptions) { cookieAuthenticationOptions.Event = new CookieAuthenticationEvents() { OnSignedIn = async context => { Console.WriteLine($"{context.Request.Path} is OnSignedIn"); await Task.CompletedTask; }, OnSigningIn = async context => { Console.WriteLine($"{context.Request.Path} is OnSigningIn"); await Task.CompletedTask; }, OnSigningOut = async context => { Console.WriteLine($"{context.Request.Path} is OnSigningOut"); await Task.CompletedTask; } } }
-
3.注冊到IOC容器
services.AddCookie(cookieAuthenticationOptions=>{ ExtentionEvent(cookieAuthenticationOptions); })
3.ITicketStore
-
1.ITicketStore主要用於持久化Cookie,它能根據用戶自己定制選擇Cookie的存儲方式,使用ITicketStore會將完整的Cookie存儲在服務端, 然后返回一個Cookie id到客戶端,客戶端訪問帶上id,經過ITicketStore來得到完整的Cookie信息,跟Seession的方式有點類似,但是他並不是,知識實現策略相同而已。
-
2.擴展ITicketStore,實現將Cookie存儲在內存中,
當然這個存儲介質,可以是內存,也可以是Redis
public class MemoryCacheTicketStore : ITicketStore { private const string Prefix = "Extentions-"; private IMemoryCache _cache; public MemoryCacheTicketStore(IMemoryCache memoryCache) { _cache = memoryCache; } public async Task<string> StoreAsync(AuthenticationTicket ticket) { var key = KeyPrefix + Guid.NewGuid().ToString("N"); await RenewAsync(key, ticket); return key; } public Task RenewAsync(string key, AuthenticationTicket ticket) { var options = new MemoryCacheEntryOptions(); var expiresUtc = ticket.Properties.ExpiresUtc; if (expiresUtc.HasValue) { options.SetAbsoluteExpiration(expiresUtc.Value); } options.SetSlidingExpiration(TimeSpan.FromHours(1)); _cache.Set(key, ticket, options); return Task.CompletedTask; } public Task<AuthenticationTicket> RetrieveAsync(string key) { _cache.TryGetValue(key, out AuthenticationTicket ticket); return Task.FromResult(ticket); } public Task RemoveAsync(string key) { _cache.Remove(key); return Task.CompletedTask; } }
-
3.在Ioc容器中注冊
//將MemoryCacheTicketStore注冊到容器 services.AddScoped<ITicketStore, MemoryCacheTicketStore>(); //注冊內存緩存 services.AddMemoryCache(); services.AddCookie(cookieAuthenticationOptions=>{ cookieAuthenticationOptions.SessionStore = services.BuildServiceProvider().GetService<ITicketStore>();; })
4.總結
在.NET Core框架提供的鑒權模塊中,首先IOC注冊IAuthenticationService,IAuthenticationSchemeProvider ,IAuthenticationHandlerProvider 服務,然后由IAuthenticationService服務,進行鑒權的核心業務處理,由IAuthenticationSchemeProvider根據scheme負責分配Handler,由IAuthenticationHandlerProvider 構建具體的處理handler ,最終使用具體的handler執行鑒權處理。