实践剖析.NET Core 如何支持 Cookie 滑动过期和 JWT 混合认证、授权

发表于:2022-1-17 09:29

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:Jeffcky    来源:JeffckyShare

  首先我们实现Cookie认证,然后再次引入JWT,最后在结合二者使用时联系其他我们可能需要注意的事项。
  Cookie认证
  在startup中我们添加cookie认证服务,如下:
  services.AddAuthentication(options => 
  { 
      options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme; 
      options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme; 
  }) 
  .AddCookie(options => 
  { 
      options.ExpireTimeSpan = TimeSpan.FromMinutes(1); 
      options.Cookie.Name = "user-session"; 
      options.SlidingExpiration = true; 
  }); 
  接下来则是使用认证和授权中间件,注意将其置于路由和终结点终结点之间,否则启动也会有明确异常提示。
  app.UseRouting(); 
   
  app.UseAuthentication(); 
   
  app.UseAuthorization(); 
   
  app.UseEndpoints(endpoints => 
  { 
    ...... 
  }); 
  我们给出测试视图页,并要求认证即控制器添加特性。
  [Authorize] 
  public class HomeController : Controller 
  { 
      public IActionResult Index() 
      { 
          return View(); 
      } 
  } 
  当进入首页,未认证默认进入account/login,那么接下来创建该视图。
  public class AccountController : Controller 
  { 
      [AllowAnonymous] 
      public IActionResult Login() 
      { 
        return View(); 
      } 
      ...... 
  } 
  我们启动程序先看看效果:
  如上图,自动跳转至登录页,此时我们点击模拟登录按钮,发起请求去模拟登录。(发起ajax请求代码就占不用篇幅给出了)
  /// <summary> 
  /// 模拟登录 
  /// </summary> 
  /// <returns></returns> 
  [HttpPost] 
  [AllowAnonymous] 
  public async Task<IActionResult> TestLogin() 
  { 
      var claims = new Claim[] 
      { 
        new Claim(ClaimTypes.Name, "Jeffcky"), 
      }; 
   
      var claimsIdentity = new ClaimsIdentity(claims, "Login"); 
   
      await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity)); 
   
      return Ok(); 
  } 
  上述无非就是构建身份以及该身份下所具有的身份属性,类似个人身份证唯一标识个人,身份证上各个信息即表示如上声明。
  同时呢,肯定要调用上下文去登录,在整个会话未过期之前,根据认证方案获取对应处理方式,最后将相关信息进行存储等等,有兴趣的童鞋可以去了解其实现细节哈。
  当我们请求过后,再次访问首页,将看到生成当前会话信息,同时我们将会话过期设置为1分钟,在1分钟内未进行会话,将自动重定向至登录页。
  注意如上标注并没有值,那么这个值可以设置吗?当然可以,在开始配置时我们并未给出,那么这个属性又代表什么含义呢?
  options.Cookie.MaxAge = TimeSpan.FromMinutes(2); 
  那么结合ExpireTimeSpan和MaxAge使用,到底代表什么意思呢?我们暂且撇开滑动过期设置。
  ExpireTimeSpan表示用户身份认证票据的生命周期,它是认证cookie的有效负载,存储的cookie值是一段加密字符串,在每次请求时,web应用程序都会根据请求对其进行解密。
  MaxAge控制着cookie的生命周期,若cookie过期,浏览器将会自动清除,如果没有设置该值,实质上它的生命周期就是ExpireTimeSpan,那么它到底有何意义呢?
  上述我们设置票据的生命周期为1分钟,同时我们控制cookie的生命周期为2分钟,若在2分钟内关闭浏览器或重启web应用程序,此时cookie生命周期并未过期,所以仍将处于会话状态即无需登录,若未设置MaxAge,关闭浏览器或重启后将自动清除其值即需登录,当然一切前提是未手动清除浏览器cookie。
  问题又来了,在配置cookie选项中,还有一个也可以设置过期的属性。
  options.Cookie.Expiration = TimeSpan.FromMinutes(3); 
  当配置ExpireTimeSpan或同时配置MaxAge时,无需设置Expiration,因为会抛出异常。
  JWT认证
  上述已经实现Cookie认证,那么在与第三方进行对接时,我们要使用JWT认证,我们又该如何处理呢?
  首先我们添加JWT认证服务:
  .AddJwtBearer(options => 
  { 
      options.TokenValidationParameters = new TokenValidationParameters 
      { 
        ValidateIssuerSigningKey = true, 
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("1234567890123456")), 
        ValidateIssuer = true, 
        ValidIssuer = "http://localhost:5000", 
        ValidateAudience = true, 
        ValidAudience = "http://localhost:5001", 
        ValidateLifetime = true, 
        ClockSkew = TimeSpan.FromMinutes(5) 
      }; 
  }); 
  将JWT Token置于cookie中,此前文章已有讲解,这里我们直接给出代码,先生成Token。
  private string GenerateToken(Claim[] claims) 
  { 
      var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("1234567890123456")); 
   
      var token = new JwtSecurityToken( 
        issuer: "http://localhost:5000", 
        audience: "http://localhost:5001", 
        claims: claims, 
        notBefore: DateTime.Now, 
        expires: DateTime.Now.AddMinutes(5), 
        signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256) 
      ); 
       return new JwtSecurityTokenHandler().WriteToken(token); 
  } 
  在登录方法中,将其写入响应cookie中,如下这般:
  /// <summary> 
  /// 模拟登录 
  /// </summary> 
  /// <returns></returns> 
  [HttpPost] 
  [AllowAnonymous] 
  public async Task<IActionResult> TestLogin() 
  { 
      var claims = new Claim[] 
      { 
        new Claim(ClaimTypes.Name, "Jeffcky"), 
      }; 
   
      var claimsIdentity = new ClaimsIdentity(claims, "Login"); 
   
      Response.Cookies.Append("x-access-token", GenerateToken(claims), 
        new CookieOptions() 
        { 
          Path = "/", 
          HttpOnly = true 
        }); 
   
      await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity)); 
   
   return Ok(); 
  } 
  去取Bearer Token值,若成功取到这赋值给如下context.Token,所以此时我们需要手动从cookie中取出token并赋值。
  options.Events = new JwtBearerEvents 
  { 
      OnMessageReceived = context => 
      { 
          var accessToken = context.Request.Cookies["x-access-token"]; 
   
          if (!string.IsNullOrEmpty(accessToken)) 
          { 
              context.Token = accessToken; 
          } 
   
          return Task.CompletedTask; 
      } 
  }; 
  一切已就绪,接下来我们写个api接口测试验证看看:
  [Authorize("Bearer")] 
  [Route("api/[controller]/[action]")] 
  [ApiController] 
  public class JwtController : ControllerBase 
  { 
      [HttpGet] 
      public IActionResult Test() 
      { 
        return Ok("test jwt"); 
      } 
  } 
  思考一下,我们通过Postman模拟测试,会返回401吗?结果会是怎样的呢?
  问题不大,主要在于该特性参数为声明指定策略,但我们需要指定认证方案即scheme,修改成如下:
  如此在与第三方对接时,请求返回token,后续将token置于请求头中即可验证通过,同时上述取cookie中token并手动赋值,对于对接第三方则是多余,不过是为了诸多其他原因而已。
  [Authorize(AuthenticationSchemes = "Bearer,Cookies")] 
  注意混合认证方案设置存在顺序,后者将覆盖前者即如上设置,此时将走cookie认证。
  滑动过期思考扩展
  若我们实现基于Cookie滑动过期,同时使用signalr进行数据推送,势必存在问题,因为会一直刷新会话,那么将导致会话永不过期问题,从安全层面角度考虑,我们该如何处理呢?
  我们知道票据生命周期存储在上下文。AuthenticationProperties属性中,所以在配置Cookie选项事件中我们可以进行自定义处理。
  public class CookieAuthenticationEventsExetensions : CookieAuthenticationEvents 
  { 
      private const string TicketIssuedTicks = nameof(TicketIssuedTicks); 
   
      public override async Task SigningIn(CookieSigningInContext context) 
      { 
          context.Properties.SetString( 
            TicketIssuedTicks, 
            DateTimeOffset.UtcNow.Ticks.ToString()); 
   
          await base.SigningIn(context); 
      } 
   
      public override async Task ValidatePrincipal( 
        CookieValidatePrincipalContext context) 
      { 
          var ticketIssuedTicksValue = context 
            .Properties.GetString(TicketIssuedTicks); 
   
          if (ticketIssuedTicksValue is null || 
            !long.TryParse(ticketIssuedTicksValue, out var ticketIssuedTicks)) 
          { 
            await RejectPrincipalAsync(context); 
            return; 
          } 
   
          var ticketIssuedUtc = 
            new DateTimeOffset(ticketIssuedTicks, TimeSpan.FromHours(0)); 
   
          if (DateTimeOffset.UtcNow - ticketIssuedUtc > TimeSpan.FromDays(3)) 
          { 
            await RejectPrincipalAsync(context); 
            return; 
          } 
   
          await base.ValidatePrincipal(context); 
      } 
   
      private static async Task RejectPrincipalAsync( 
        CookieValidatePrincipalContext context) 
      { 
          context.RejectPrincipal(); 
          await context.HttpContext.SignOutAsync(); 
      } 
  } 
  在添加Cookie服务时,有对应事件选项,使用如下:
  options.EventsType = typeof(CookieAuthenticationEventsExetensions); 
  扩展事件实现表示在第一次会话到当前时间截止超过3天,则自动重定向至登录页,最后将上述扩展事件进行注册即可。

  本文内容不用于商业目的,如涉及知识产权问题,请权利人联系51Testing小编(021-64471599-8017),我们将立即处理
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号