zoukankan      html  css  js  c++  java
  • ASP.NET Core 3.1使用JWT认证Token授权 以及刷新Token

     使用JWT尽量使用HTTPS协议 避免挟持

    JWT 有个问题,导致很多开发团队放弃使用它,那就是一旦颁发一个 JWT 令牌,服务端就没办法废弃掉它,除非等到它自身过期。有很多应用默认只允许最新登录的一个客户端正常使用,不允许多端登录,JWT 就没办法做到,因为颁发了新令牌,但是老的令牌在过期前仍然可用。这种情况下,就需要服务端增加相应的逻辑。 

    Jwt刷新系统 登录的时候存两个token  一个刷新token存7天  一个业务token30分钟  每次请求前判断业务token是否过期 或者临近过期  就去用刷新token和业务token请求相应接口刷新 返回业务token 更新在前端 在发送请求;有点麻烦

    或者单系统 不管 直接存1天   1天后到期重新登录 

    刷新Token方案

    方案1.后端存储最后一次token,后端判断toekn,合法超时,刷新token推给前端  比如放在response的heard中

    优点:前端改动小

    缺点:后端实现复杂,需要而外存储。

    方案2.

    前端解码token。拿到过期时间,和当前时间进行判断。如果快过期,主动调用获取新token.

    缺点:前端每次请求需要解码判断

    优点:后端压力小,不需要存储。

    在 Web 应用发展的初期,大部分采用基于 Session 的会话管理方式,逻辑如下。

    • 客户端使用用户名密码进行认证
    • 服务端生成并存储 Session,将 SessionID 通过 Cookie 返回给客户端
    • 客户端访问需要认证的接口时在 Cookie 中携带 SessionID
    • 服务端通过 SessionID 查找 Session 并进行鉴权,返回给客户端需要的数据

    基于 Session 的方式存在多种问题。

    • 服务端需要存储 Session,并且由于 Session 需要经常快速查找,通常存储在内存或内存数据库中,同时在线用户较多时需要占用大量的服务器资源。
    • 当需要扩展时,创建 Session 的服务器可能不是验证 Session 的服务器,所以还需要将所有 Session 单独存储并共享。
    • 由于客户端使用 Cookie 存储 SessionID,在跨域场景下需要进行兼容性处理,同时这种方式也难以防范 CSRF 攻击。

    基于 Token 的会话管理

    鉴于基于 Session 的会话管理方式存在上述多个缺点,无状态的基于 Token 的会话管理方式诞生了,所谓无状态,就是服务端不再存储信息,甚至是不再存储 Session,逻辑如下。

    • 客户端使用用户名密码进行认证
    • 服务端验证用户名密码,通过后生成 Token 返回给客户端
    • 客户端保存 Token,访问需要认证的接口时在 URL 参数或 HTTP Header 中加入 Token
    • 服务端通过解码 Token 进行鉴权,返回给客户端需要的数据

    JWT 优势 & 问题

    JWT 拥有基于 Token 的会话管理方式所拥有的一切优势,不依赖 Cookie,使得其可以防止 CSRF 攻击,也能在禁用 Cookie 的浏览器环境中正常运行。

    而 JWT 的最大优势是服务端不再需要存储 Session,使得服务端认证鉴权业务可以方便扩展,避免存储 Session 所需要引入的 Redis 等组件,降低了系统架构复杂度。但这也是 JWT 最大的劣势,由于有效期存储在 Token 中,JWT Token 一旦签发,就会在有效期内一直可用,无法在服务端废止,当用户进行登出操作,只能依赖客户端删除掉本地存储的 JWT Token,如果需要禁用用户,单纯使用 JWT 就无法做到了。

    基于 JWT 的实践

    既然 JWT 依然存在诸多问题,甚至无法满足一些业务上的需求,但是我们依然可以基于 JWT 在实践中进行一些改进,来形成一个折中的方案,毕竟,在用户会话管理场景下,没有银弹。

    前面讲的 Token,都是 Access Token,也就是访问资源接口时所需要的 Token,还有另外一种 Token,Refresh Token,通常情况下,Refresh Token 的有效期会比较长,而 Access Token 的有效期比较短,当 Access Token 由于过期而失效时,使用 Refresh Token 就可以获取到新的 Access Token,如果 Refresh Token 也失效了,用户就只能重新登录了。

    在 JWT 的实践中,引入 Refresh Token,将会话管理流程改进如下。

    • 客户端使用用户名密码进行认证
    • 服务端生成有效时间较短的 Access Token(例如 10 分钟),和有效时间较长的 Refresh Token(例如 7 天)
    • 客户端访问需要认证的接口时,携带 Access Token
    • 如果 Access Token 没有过期,服务端鉴权后返回给客户端需要的数据
    • 如果携带 Access Token 访问需要认证的接口时鉴权失败(例如返回 401 错误),则客户端使用 Refresh Token 向刷新接口申请新的 Access Token
    • 如果 Refresh Token 没有过期,服务端向客户端下发新的 Access Token
    • 客户端使用新的 Access Token 访问需要认证的接口

    将生成的 Refresh Token 以及过期时间存储在服务端的数据库中,由于 Refresh Token 不会在客户端请求业务接口时验证,只有在申请新的 Access Token 时才会验证,所以将 Refresh Token 存储在数据库中,不会对业务接口的响应时间造成影响,也不需要像 Session 一样一直保持在内存中以应对大量的请求。

    上述的架构,提供了服务端禁用用户 Token 的方式,当用户需要登出或禁用用户时,只需要将服务端的 Refresh Token 禁用或删除,用户就会在 Access Token 过期后,由于无法获取到新的 Access Token 而再也无法访问需要认证的接口。这样的方式虽然会有一定的窗口期(取决于 Access Token 的失效时间),但是结合用户登出时客户端删除 Access Token 的操作,基本上可以适应常规情况下对用户认证鉴权的精度要求。

    总结

    JWT 的使用,提高了开发者开发用户认证鉴权功能的效率,降低了系统架构复杂度,避免了大量的数据库和缓存查询,降低了业务接口的响应延迟。然而 JWT 的这些优点也增加了 Token 管理上的难度,通过引入 Refresh Token,既能继续使用 JWT 所带来的优势,又能使得 Token 管理的精度符合业务的需求。

    使用JWT

    0、引言
    若不清楚什么是JWT的请先了解下什么是JWT。

    1、关于Authentication与Authorization
    我相信在aspnet core中刚接触甚至用了段时间这两个概念的时候都是一头雾水的,傻傻分不清。
    认证(Authentication)和授权(Authorization)在概念上比较的相似,且又有一定的联系,因此很容易混淆。
    认证(Authentication)是指验证用户身份的过程,即当用户要访问受保护的资源时,将其信息(如用户名和密码)发送给服务器并由服务器验证的过程。
    授权(Authorization)是验证一个已通过身份认证的用户是否有权限做某件事情的过程。
    有过RBAC的开发经验者来说这里可以这么通俗的来理解:认证是验证一个用户是否“合法”(一般就是检查数据库中是否有这么个用户),授权是验证这个用户是否有做事情的权限(简单理解成RBAC中的用户权限)。

    2、整个认证流程是怎样的?

     从图中可以看到整个认证、授权的流程,先进行身份验证 ,验证通过后将Token放回给客户端,客户端访问资源的时候请求头中添加Token信息,服务器进行验证并于授权是否能够访问该资源。

    3、开始JWT身份认证

    3.1 引入nuget包:Microsoft.AspNetCore.Authentication.JwtBearer

     3.2 在Startup.cs文件中进行配置

    #region jwt验证
                var jwtConfig = new JwtConfig();
                Configuration.Bind("JwtConfig", jwtConfig);
                services
                    .AddAuthentication(option =>
                    {
                        //认证middleware配置
                        option.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                        option.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
                    })
                    .AddJwtBearer(options =>
                    {
                        options.TokenValidationParameters = new TokenValidationParameters
                        {
                            //Token颁发机构
                            ValidIssuer = jwtConfig.Issuer,
                            //颁发给谁
                            ValidAudience = jwtConfig.Audience,
                            //这里的key要进行加密
                            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtConfig.SecretKey)),
                            //是否验证Token有效期,使用当前时间与Token的Claims中的NotBefore和Expires对比
                            ValidateLifetime = true,
                        };
                    });
                #endregion

    配置中间件

    //启用身份验证功能。必须要在app.UseAuthorization();之前
                app.UseAuthentication();//鉴权,

    JWTHelper

    public class JWTHelper
        {
            public class JwtHelper
            {
    
                /// <summary>
                /// 颁发JWT字符串
                /// </summary>
                /// <param name="tokenModel"></param>
                /// <returns></returns>
                public static string IssueJwt(TokenModelJwt tokenModel)
                {
                    // 自己封装的 appsettign.json 操作类,看下文
                    string iss = Appsettings.app(new string[] { "Audience", "Issuer" });
                    string aud = Appsettings.app(new string[] { "Audience", "Audience" });
                    string secret = Appsettings.app(new string[] { "Audience", "Secret" });
    
                    var claims = new List<Claim>
                  {
                     /*
                     * 特别重要:
                       1、这里将用户的部分信息,比如 uid 存到了Claim 中,如果你想知道如何在其他地方将这个 uid从 Token 中取出来,请看下边的SerializeJwt() 方法,或者在整个解决方案,搜索这个方法,看哪里使用了!
                       2、你也可以研究下 HttpContext.User.Claims ,具体的你可以看看 Policys/PermissionHandler.cs 类中是如何使用的。
                     */                
    
                    new Claim(JwtRegisteredClaimNames.Jti, tokenModel.Uid.ToString()),
                    new Claim(JwtRegisteredClaimNames.Iat, $"{new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds()}"),
                    new Claim(JwtRegisteredClaimNames.Nbf,$"{new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds()}") ,
                    //这个就是过期时间,目前是过期7200秒,可自定义,注意JWT有自己的缓冲过期时间
                    new Claim (JwtRegisteredClaimNames.Exp,$"{new DateTimeOffset(DateTime.Now.AddSeconds(7200)).ToUnixTimeSeconds()}"),
                    new Claim(JwtRegisteredClaimNames.Iss,iss),
                    new Claim(JwtRegisteredClaimNames.Aud,aud),
                    
                    //new Claim(ClaimTypes.Role,tokenModel.Role),//为了解决一个用户多个角色(比如:Admin,System),用下边的方法
                   };
    
                    // 可以将一个用户的多个角色全部赋予;
                    // 作者:DX 提供技术支持;
                    claims.AddRange(tokenModel.Role.Split(',').Select(s => new Claim(ClaimTypes.Role, s)));
    
    
                    //秘钥 (SymmetricSecurityKey 对安全性的要求,密钥的长度太短会报出异常)
                    var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret));
                    var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
                    
                    var jwt = new JwtSecurityToken(
                        issuer: iss,
                        claims: claims,
                        signingCredentials: creds
                        //,expires:DateTime.Now.AddMinutes(1)
                        );
    
                    var jwtHandler = new JwtSecurityTokenHandler();
                    var encodedJwt = jwtHandler.WriteToken(jwt);
    
                    return encodedJwt;
                }
    
                /// <summary>
                /// 解析
                /// </summary>
                /// <param name="jwtStr"></param>
                /// <returns></returns>
                public static TokenModelJwt SerializeJwt(string jwtStr)
                {
                    var jwtHandler = new JwtSecurityTokenHandler();
                    JwtSecurityToken jwtToken = jwtHandler.ReadJwtToken(jwtStr);
                    object role;
                    try
                    {
                        jwtToken.Payload.TryGetValue(ClaimTypes.Role, out role);
                    }
                    catch (Exception e)
                    {
                        Console.WriteLine(e);
                        throw;
                    }
                    var tm = new TokenModelJwt
                    {
                        Uid = (jwtToken.Id).ObjToInt(),
                        Role = role != null ? role.ObjToString() : "",
                    };
                    return tm;
                }
            }
    
            /// <summary>
            /// 令牌
            /// </summary>
            public class TokenModelJwt
            {
                /// <summary>
                /// Id
                /// </summary>
                public long Uid { get; set; }
                /// <summary>
                /// 角色
                /// </summary>
                public string Role { get; set; }
                /// <summary>
                /// 职能
                /// </summary>
                public string Work { get; set; }
    
            }
        }

    然后呢 如果你使用的Swagger

    加上自动在Header带上Token

    Bearer就是在ASP.NET Core 在 Microsoft.AspNetCore.Authentication 下实现了一系列认证, 包含 Cookie, JwtBearer, OAuth, OpenIdConnect 等
    options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme()
                    {
                        Description = "JWT授权(数据将在请求头中进行传输) 直接在下框中输入Bearer {token}(注意两者之间有一个空格)",
                        Name = "Authorization",//jwt默认的参数名称,
                        In = ParameterLocation.Header,//jwt默认存放Autorization信息的位置(header中)
                        Type = SecuritySchemeType.ApiKey, //指定ApiKey
                        BearerFormat = "JWT",//标识承载令牌的格式 该信息主要是出于文档目的
                        Scheme = "bearer"//授权中要使用的HTTP授权方案的名称
                    });
                    //在Heder中添加Token 传递到后台
                    options.AddSecurityRequirement(new OpenApiSecurityRequirement
                    {
                        {
                            new OpenApiSecurityScheme{
                                Reference = new OpenApiReference {
                                            Type = ReferenceType.SecurityScheme,
                                            Id = "Bearer"}
                            },new string[] { }
                        }
                    });

     在对应Controller上或Action上加上Authorize标记

     验证TOken

    options.Events = new JwtBearerEvents
                    {
                        //此处为权限验证失败后触发的事件
                        OnChallenge = context =>
                        {
                            //此处代码为终止.Net Core默认的返回类型和数据结果,这个很重要哦,必须
                            context.HandleResponse();
                            //自定义自己想要返回的数据结果,我这里要返回的是Json对象,通过引用Newtonsoft.Json库进行转换
                            var payload = JsonConvert.SerializeObject(new ApiResult<bool>("Token无效"));
                            //自定义返回的数据类型
                            context.Response.ContentType = "application/json";
                            //自定义返回状态码,默认为401 我这里改成 200
                            context.Response.StatusCode = StatusCodes.Status200OK;
                            //context.Response.StatusCode = StatusCodes.Status401Unauthorized;
                            //输出Json数据结果
                            context.Response.WriteAsync(payload);
                            return Task.FromResult(0);
                        }
                    };
     /// <summary>
            /// 获取Token
            /// </summary>
            /// <param name="req">请求流</param>
            /// <returns></returns>
            public static string GetToken(HttpRequest req)
            {
                string tokenHeader = req.Headers["Authorization"].ToString();
                if (string.IsNullOrEmpty(tokenHeader))
                    throw new Exception("缺少token!");
    
                string pattern = "^Bearer (.*?)$";
                if (!Regex.IsMatch(tokenHeader, pattern))
                    throw new Exception("token格式不对!格式为:Bearer {token}");
    
                string token = Regex.Match(tokenHeader, pattern).Groups[1]?.ToString();
                if (string.IsNullOrEmpty(token))
                    throw new Exception("token不能为空!");
    
                return token;
            }

    好了 基本没问题 最后在如果请求401可以自定义

     

     整的代码如下:

    [AllowAnonymous]
            [HttpGet]
            [Route("api/auth")]
            public IActionResult Get(string userName, string pwd)
            {
                if (CheckAccount(userName, pwd, out string role))
                {
                    //每次登陆动态刷新
                    Const.ValidAudience = userName + pwd + DateTime.Now.ToString();
                    // push the user’s name into a claim, so we can identify the user later on.
                    //这里可以随意加入自定义的参数,key可以自己随便起
                    var claims = new[]
                    {
                        new Claim(JwtRegisteredClaimNames.Nbf,$"{new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds()}") ,
                        new Claim (JwtRegisteredClaimNames.Exp,$"{new DateTimeOffset(DateTime.Now.AddMinutes(30)).ToUnixTimeSeconds()}"),
                        new Claim(ClaimTypes.NameIdentifier, userName),
                        new Claim("Role", role)
                    };
                    //sign the token using a secret key.This secret will be shared between your API and anything that needs to check that the token is legit.
                    var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Const.SecurityKey));
                    var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
                    //.NET Core’s JwtSecurityToken class takes on the heavy lifting and actually creates the token.
                    var token = new JwtSecurityToken(
                        //颁发者
                        issuer: Const.Domain,
                        //接收者
                        audience: Const.ValidAudience,
                        //过期时间
                        expires: DateTime.Now.AddMinutes(30),
                        //签名证书
                        signingCredentials: creds,
                        //自定义参数
                        claims: claims
                        );
    
                    return Ok(new
                    {
                        token = new JwtSecurityTokenHandler().WriteToken(token)
                    });
                }
                else
                {
                    return BadRequest(new { message = "username or password is incorrect." });
                }
            }

    3.3 然后改造一下StartUp.cs

      我们仅仅需要关心改动的地方,也就是AddJwtBearer这个验证token的方法,我们不用原先的固定值的校验方式,而提供一个代理方法进行运行时执行校验

    .AddJwtBearer(options =>
    
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateLifetime = true,//是否验证失效时间
        ClockSkew = TimeSpan.FromSeconds(30),
        ValidateAudience = true,//是否验证Audience
        //ValidAudience = Const.GetValidudience(),//Audience
        //这里采用动态验证的方式,在重新登陆时,刷新token,旧token就强制失效了
        AudienceValidator = (m, n, z) =>
        {
            return m != null && m.FirstOrDefault().Equals(Const.ValidAudience);
        },
        ValidateIssuer = true,//是否验证Issuer
        ValidIssuer = Const.Domain,//Issuer,这两项和前面签发jwt的设置一致
        ValidateIssuerSigningKey = true,//是否验证SecurityKey
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Const.SecurityKey))//拿到SecurityKey
    };

    这里逻辑是这样的:因为重新登陆将原来的变量更改了,所以这里校验的时候也一并修改成了新的变量值,那么旧的token当然就不匹配了,也就是旧的token被强制失效了。

     

     

     参考地址https://www.cnblogs.com/7tiny/p/11012035.html   https://www.cnblogs.com/cokeking/p/10969579.html

  • 相关阅读:
    获取Android状态栏高度的屡试不爽的方法
    在线音乐API的研究 (Part 2.1)
    Zabbix
    利用 Puppet 实现自动化管理配置 Linux 计算机集群
    django的admin后台管理如何更改为中文
    windows系统安装python3.6.3和python3.7.0
    微课程--Android--高级控件之二--标题栏
    微课程--Android--高级控件之一ListView
    微课程--Android--Fragement
    微课程--Android--界面布局总结
  • 原文地址:https://www.cnblogs.com/netlock/p/13362019.html
Copyright © 2011-2022 走看看