从壹开始先后端分离[.netCore 不按期 ] 36 ║解决JWT自定义中间件受权过时问题

缘起

哈喽,老张的不按期更新的平常又开始了,在我们的先后端分离的.net core 框架中,虽然已经实现了权限验证《框架之五 || Swagger的使用 3.3 JWT权限验证【修改】》,只不过仍是有一些遗留问题,最近有很多的小伙伴发现了这样的一些问题,原本想着直接就在原文修改,可是发现可能怕有的小伙伴看不到,就单发一条推送吧,因此我仍是单写出一篇文章来讲明解决这些问题,但愿对不管是正在开发权限管理系统,仍是平时须要数据库动态绑定权限分配的你有一些启发和思考。今天我们注意解决这三个问题:html

一、过时时间无效;git

二、权限策略是写死的,如何存入数据库;github

三、如何进行无状态权限验证;数据库

以前我也是考虑了一些时间,可是都不是很好的方法,就一直搁浅,正好群里一个大神提供了很好的方法,今天我就不敢用完美来形容了,怕有人批评,哗众取宠,由于是上一个系列,并且也是老问题,这里就不过多的进行文字介绍了,直接上代码。json

投稿做者:这里重点说明下,是参考QQ群里小伙伴 Demon @忐-忑 的相关内容,基本都是他的功劳,我只是一个搬运工😀。后端

 

关于JWT一共三篇 姊妹篇,内容分别从简单到复杂,必定要多看多想:api

      1、Swagger的使用 3.3 JWT权限验证【修改】服务器

      2、解决JWT权限验证过时问题app

      3、JWT完美实现权限与接口的动态分配框架

 预告: 关于复杂的详细的权限验证系列,我会在DDD领域驱动设计以后,开启这个基于微服务的 IdentityServer4 系列讲解,这里先预告一下。

 

1、解决过时问题

在以前的代码里,JWT 虽然已经能够实现验证了,可是却没法达到过时时间,这个也是一个不大不小的问题,之前之因此没法实现这个功能,主要是犯了两个小错误

一、没有真正用到JWT的Bearer验证;

二、使用了自定义的受权,而没有用官方UseAuthentication受权,致使过时时间没有生效;

这里就调整下代码:

 

一、从新设计 IssueJWT 生成 Token 的方法

  /// <summary>
        /// 颁发JWT字符串
        /// </summary>
        /// <param name="tokenModel"></param>
        /// <returns></returns>
        public static string IssueJWT(TokenModelJWT tokenModel)
        {
            var dateTime = DateTime.UtcNow;
//var claims = new Claim[] //{ // new Claim(JwtRegisteredClaimNames.Jti,tokenModel.Uid.ToString()),//Id // new Claim("Role", tokenModel.Role),//角色 // new Claim(JwtRegisteredClaimNames.Iat,$"{new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds()}"),
                  new Claim (JwtRegisteredClaimNames.Exp,$"{new DateTimeOffset(DateTime.Now.AddSeconds(10)).ToUnixTimeSeconds()}")
//}; var claims = new Claim[] { //下边为Claim的默认配置 new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), new Claim(JwtRegisteredClaimNames.Iat, $"{new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds()}"), new Claim(JwtRegisteredClaimNames.Nbf,$"{new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds()}") , //这个就是过时时间,目前是过时100秒,可自定义,注意JWT有本身的缓冲过时时间 new Claim (JwtRegisteredClaimNames.Exp,$"{new DateTimeOffset(DateTime.Now.AddSeconds(100)).ToUnixTimeSeconds()}"), new Claim(JwtRegisteredClaimNames.Iss,"Blog.Core"), new Claim(JwtRegisteredClaimNames.Aud,"wr"), //这个Role是官方UseAuthentication要要验证的Role,咱们就不用手动设置Role这个属性了 new Claim(ClaimTypes.Role,tokenModel.Role), }; //秘钥 var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JwtHelper.secretKey)); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var jwt = new JwtSecurityToken( issuer: "Blog.Core", claims: claims, signingCredentials: creds); var jwtHandler = new JwtSecurityTokenHandler(); var encodedJwt = jwtHandler.WriteToken(jwt); return encodedJwt; }

 

主要的修改,就是Claim[]的声明上,定义了过时时间和Role。

 

二、修改JWT的权限验证服务

     //认证
            services.AddAuthentication(x =>
            {
                x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            })
          .AddJwtBearer(o =>
          {
              o.TokenValidationParameters = new TokenValidationParameters
              {
                  ValidateIssuer = true,//是否验证Issuer
                  ValidateAudience = true,//是否验证Audience 
                  ValidateIssuerSigningKey = true,//是否验证IssuerSigningKey 
                  ValidIssuer = "Blog.Core",
                  ValidAudience = "wr",
                  ValidateLifetime = true,//是否验证超时  当设置exp和nbf时有效 同时启用ClockSkew 
                  IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(JwtHelper.secretKey)),//从appsettings.json拿到的密钥Secret,更多内容往下看 //注意这是缓冲过时时间,总的有效时间等于这个时间加上jwt的过时时间,若是不配置,默认是5分钟
                  ClockSkew = TimeSpan.FromSeconds(30)

              };
          });

 

更新:注意上边的代码在Github中已经有了修改,基本内容都同样,只是位置微调了,说白了,就是把这一块封装了一个实例罢了,不会看不懂:

// 2.1【认证】、官方JWT认证
services.AddAuthentication(x =>
 {
     x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
     x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
 })
 .AddJwtBearer(o =>
 {
     o.TokenValidationParameters = tokenValidationParameters;    
 });

//上边用到的 tokenValidationParameters 
 //读取配置文件
 var audienceConfig = Configuration.GetSection("Audience");
 var symmetricKeyAsBase64 = audienceConfig["Secret"];
 var keyByteArray = Encoding.ASCII.GetBytes(symmetricKeyAsBase64);
 var signingKey = new SymmetricSecurityKey(keyByteArray);

 // 令牌验证参数
 var tokenValidationParameters = new TokenValidationParameters
 {
     ValidateIssuerSigningKey = true,
     IssuerSigningKey = signingKey,//仍是从 appsettings.json 拿到的
     ValidateIssuer = true,
     ValidIssuer = audienceConfig["Issuer"],//发行人
     ValidateAudience = true,
     ValidAudience = audienceConfig["Audience"],//订阅人
     ValidateLifetime = true,
     ClockSkew = TimeSpan.Zero,
     RequireExpirationTime = true,
 };

 

其实和以前的方法是同样的,只不过请注意 ClockSkew 属性,默认是5分钟缓冲。

总的Token有效时间 = JwtRegisteredClaimNames.Exp + ClockSkew ;

 

三、启动权限认证配置

在以前的方法中,咱们用到了中间件 app.UseMiddleware<JwtTokenAuth>(); 固然也是能够的,只不过受权的时候写的不全,才致使验证的时候有效时间没办法识别,由于咱们在生成Token的时候,已经配置好了 claim 声明,因此直接调用官方的验证便可。这样的好处是,咱们也不用去判断  Headers 是否包含 Authorization 的操做;

 //app.UseMiddleware<JwtTokenAuth>();//注意此受权方法已经放弃,请使用下边的官方受权方法。这里仅仅是受权方法的替换
 app.UseAuthentication();

虽然这个时候咱们放弃了使用中间件来受权,可是经过你们的学习,已经彻底掌握了中间件的使用了吧,也算是对中间件的一个学习过程,由于在其余地方继续使用其余的中间件。

重要: 

这里使用 app.UseAuthentication(); 的目的是为了替换受权方法,若是你仍须要中间件传值的话,好比把用户信息写入全局,请继续使用中间件!

 

四、重要:正确的Token输入方法

在以前中,我犯了一个想固然的错误,而后就直接是解析的 Token 字符串,获取到数据,这个天然是没有错的,只不过这样就没法正常的使用认证服务中的 AddJwtBearer 方法。那该怎么办呢,很简单,就是之后在 Http请求的时候,带上Bearer(空格)Token,这样的格式,好比:Bearer 96sdfoysgoi79d87g.sd0ug97sdgf15fdg4531dfg

五、测试接口,查看是否有效

这个时候咱们等待130秒,就能够看到已通过期了,若是你没有明白为啥是130秒,请看上文

 

2、把验证策略写到数据库

其实以前我已经在数据库表结构中,配置了用到的数据库表,只不过一直没有用, 

 

├── Module                                // 菜单表
├── ModulePermission                        // 菜单与按钮关系表
├── Permission                              // 按钮表 
├── Role                                    // 角色表
├── RoleModulePermission                    // 按钮跟权限关联表
├── UserRole                                // 用户跟角色关联表
└── sysUserInfo                             // 用户信息表 

 

 目前我采用的是,直接获取当前用户的所有角色信息,赋值给 JWT 的Token,而后经过 UseAuthentication() 进行受权

 

//获取当前用户所有的角色信息(字符串,逗号隔开)
 var userRoles = await sysUserInfoServices.GetUserRoleNameStr(name, pass);
 if (user != null)
 {

     TokenModelJWT tokenModel = new TokenModelJWT();
     tokenModel.Uid = 1;
     tokenModel.Role = user;
}

 

这里先留下一个坑,之后再开发权限管理系统的时候,再单写一个系统吧。

 

3、无状态与有状态验证

一、无状态受权

在第一部分中,咱们不只已经实现了Token的有效期,并且天然而然是实现了受权验证,只须要在知道的Controller 或者方法上增长特性 [Authorize] 就能够实现验证

 

过程是这样的,咱们登录,认证用户信息,成功后,分发Role,而后生成 Token ,这个时候就已经表明当前用户是有有权限的,只不过是无状态的,咱们不知道他的具体是什么角色,可是会被 app.UseAuthentication(); 识别并经过,若是咱们仅仅想给接口增长一个验证,而不要求角色信息,就能够这么操做。

若是想在受权的controller中,让某一个方法可让全部人访问,能够增长  [AllowAnonymous] 特性;

 [HttpGet("{id}")]
 [AllowAnonymous]//不受受权控制,任何人均可访问
 public ActionResult<string> Get(int id)
 {
     return "value";
 }

 

那这个时候你会问,我若是就想要当前用户必须是某一个Role才能访问呢,请往下看。

二、有角色受权

 这个时候咱们就须要增长 Role 信息了,好比这样:

 

注意:在使用 Policy 的时候,之前我写的有问题,请注意修改 

 

  services.AddAuthorization(options =>
  {
      options.AddPolicy("Client", policy => policy.RequireRole("Client").Build());
      options.AddPolicy("Admin", policy => policy.RequireRole("Admin").Build());
      //这个写法是错误的,这个是并列的关系,不是或的关系
      //options.AddPolicy("AdminOrClient", policy => policy.RequireRole("Admin,Client").Build());
      
      //这个才是的关系
      options.AddPolicy("SystemOrAdmin", policy => policy.RequireRole("Admin", "System"));
  });

 

4、权限管理系统Id4

 这个系列我会在DDD领域驱动设计以后,开启这个 IdentityServer4 系列讲解,这里先预告一下。

系列已经开启:

从壹开始 [ Id4 ] 之一║ 受权服务器 IdentityServer4 开篇讲&计划书

 

 

5、Github & Gitee

https://github.com/anjoy8/Blog.Core

https://gitee.com/laozhangIsPhi/Blog.Core