有梦想的鱼
写代码我得再认真点,当我最终放下键盘时,我实在不想仍有太多疑惑

基础信息

1.什么是鉴权授权?
  • 鉴权是验证用户是否拥有访问系统的权利,授权是判断用户是否有权限做一些其他操作。

2.传统的Session 和Cookie
  • 主要用于无状态请求下的的用户身份识别,只不过Session将信息存储在服务端,Cookie将信息存储在客户端。

Session

  1. 在客户端第一次进行访问时,服务端会生成一个Session id返回到客户端

  2. 客户端将Session id存储在本地Cookie后续请求都带上这个id

  3. 服务端从接收到的请求中根据Session id在自己存储的信息中去识别客户端信息

Cookie

  1. 在客户端访问服务器时,服务端会在响应中颁发一个Cookie

  2. 客户端会把cookie存储,当再访问服务端时会将cookie和请求一并提交

  3. 服务端会检查cookie识别客户端,并也可以根据需要修改cookie的内容


3.存在的问题

在分布式或集群系统中使用Session

假设现在服务器为了更好的承载和容灾将系统做了分布式和集群,也就是有了N个服务端,那是不是每一个服务端都要具有对每一个客户端的Session或者Cookie的识别能力呢?

其实我们可以使用Session共享的方式用于Session的识别,通常每一个分布式系统都由不同的人负责或者跨网络,做Session共享可能会存在诸多业务因素的影响,就算实现了系统可能也会越来越重?像这种我们可以使用Token校验的方式,每一个客户端登录去向统一的鉴权平台发起,由鉴权平台颁发一个Token,然后后续各个系统的请求都带上这个Token。


4.Token
  • Token是服务端生成的一串字符串,以作客户端进行请求的一个令牌。

image.png

执行步骤

  1. 用户向统一的鉴权系统发起用户名和密码的校验

  2. 校验通过后会颁发一个经过签名的Token,用户就拿着颁发的Token去访问其他三方系统

  3. 三方系统根据对应的加密方式,使用秘钥解密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。

  1. HttpContext.SignInAsync();
  2. HttpContext.AuthenticateAsync();
  3. HttpContext.SignOutAsync();
  4. HttpContext.ChallengeAsync();
  5. 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执行鉴权处理。

posted on 2022-02-20 01:06  有梦想的鱼i  阅读(1751)  评论(6编辑  收藏  举报