基础信息
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执行鉴权处理。