• 第四十五节:复习Session/Jwt原理、Jwt实操、Swagger中配置Jwt、Jwt撤回方案、双token方案


    一. 复习

    1. 旧的Session校验机制

      (https://www.cnblogs.com/yaopengfei/p/10435032.html)

    2. Session原理

    (https://www.cnblogs.com/yaopengfei/p/8057176.html)

    3. Jwt原理

    (重点参考:https://www.cnblogs.com/yaopengfei/p/12162507.html)

      样式:"xxxxxxxxxxxx.xxxxxxxxxxxxx.xxxxxxxxxxxxxxxx"由三部分组成.

    (1).Header头部:{\"alg\":\"HS256\",\"typ\":\"JWT\"}基本组成,也可以自己添加别的内容,然后对最后的内容进行Base64编码.

    (2).Payload负载:iss、sub、aud、exp、nbf、iat、jti基本参数,也可以自己添加别的内容,然后对最后的内容进行Base64编码.

    (3).Signature签名:将Base64后的Header和Payload通过.组合起来,然后利用【Hmacsha256+密钥】进行加密, 形成的字符串作为第三部分。

    二. 实操

    1. 加密和解密测试

      这里基于 【System.IdentityModel.Tokens.Jwt】程序集测试

      下面代码,解密的时候不验证 aud 和 iss, ClockSkew = TimeSpan.Zero  代表校验过期时间的偏移量,即验证过期时间:(expires+该值),该值默认为5min,这里设置为0,表示生成token时的expries即为过期时间

     /// <summary>
            /// 测试加密和解密
            /// </summary>
            /// <returns></returns>
            [HttpPost]
            public string TestJwAndJm()
            {
                string secretKey = configuration["SecretKey"];
                string token;
                //加密
                {
                    var tokenHandler = new JwtSecurityTokenHandler();
                    var key = Encoding.Default.GetBytes(secretKey);
                    var tokenDescriptor = new SecurityTokenDescriptor()
                    {
                        Subject = new ClaimsIdentity(new Claim[] {
                             new Claim("userId","00000000001"),
                             new Claim("userAccount","admin")
                        }),
                        Expires = DateTime.UtcNow.AddSeconds(10),
                        SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
                    };
                    token = tokenHandler.WriteToken(tokenHandler.CreateToken(tokenDescriptor));           //将组装好的格式生成加密后的jwt字符串
                    Console.WriteLine("加密生成的token为:" + token);
                }
                //解密
                bool result;
                {
                    var tokenHandler = new JwtSecurityTokenHandler();
                    var key = Encoding.Default.GetBytes(secretKey);
                    var validationParameters = new TokenValidationParameters
                    {
                        ValidateAudience = false, //表示不验证aud
                        ValidateIssuer = false,   //表示不验证iss
                        IssuerSigningKey = new SymmetricSecurityKey(key),
                        ClockSkew = TimeSpan.Zero   //代表校验过期时间的偏移量,即验证过期时间:(expires+该值),该值默认为5min,这里设置为0,表示生成token时的expries即为过期时间
                    };
                    SecurityToken validatedToken;   //解密后的对象
                    try
                    {
                        ClaimsPrincipal claimsPrincipal = tokenHandler.ValidateToken(token, validationParameters, out validatedToken);
                        result = true;
                        //获取payload中的数据
                        var jwtPayload = ((JwtSecurityToken)validatedToken).Payload.SerializeToJson();
                        Console.WriteLine("解密后的内容为:" + jwtPayload);
                    }
                    catch (SecurityTokenExpiredException)
                    {
                        //表示过期
                        result = false;
                    }
                    catch (SecurityTokenException)
                    {
                        //表示token错误
                        result = false;
                    }
                }
    
                return $"token:{token}, result:{result}";
            }

    2. 在webapi中测试

      这里基于【Microsoft.AspNetCore.Authentication.JwtBearer】程序集测试

     (1). 编写获取token的接口 GetToken()

     /// <summary>
            /// 获取Token
            /// </summary>
            /// <returns></returns>
            [HttpPost]
            public String GetToken()
            {
                string secretKey = configuration["SecretKey"];
                string token;
                //加密
                {
                    var tokenHandler = new JwtSecurityTokenHandler();
                    var key = Encoding.Default.GetBytes(secretKey);
                    var tokenDescriptor = new SecurityTokenDescriptor()
                    {
                        Subject = new ClaimsIdentity(new Claim[] {
                             new Claim("userId","00000000001"),
                             new Claim("userAccount","admin")
                        }),
                        Expires = DateTime.UtcNow.AddMinutes(5),
                        SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
                    };
                    token = tokenHandler.WriteToken(tokenHandler.CreateToken(tokenDescriptor));           //将组装好的格式生成加密后的jwt字符串
                }
                return token;
    
            }

     (2). 通过services.AddAuthentication("Bearer").AddJwtBearer() 注册jwt校验

    //注册jwt校验
    builder.Services.AddAuthentication("Bearer").AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = false,//是否验证Issuer
            ValidateAudience = false,//是否验证Audience
            ClockSkew = TimeSpan.Zero,//校验时间是否过期时,设置的时钟偏移量(默认是5min,这里设置为0,即用的是产生token时设置的过期时间)
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.Default.GetBytes(builder.Configuration["SecretKey"])),//拿到SecurityKey
        };
    });

     (3). 开启认证中间件,app.UseAuthentication();

     

     注意:必须在授权中间件UseAuthorization上面

     (4). 编写两个接口 GetMsg1() GetMsg2(), 其中GetMsg1接口上添加 [Authorize] 表示开启jwt校验, GetMsg2不开启

            /// <summary>
            /// 测试Jwt校验
            /// </summary>
            /// <returns></returns>
            [Authorize]
            [HttpPost]
            public string GetMsg1()
            {
                return "请求成功";
            }
    
            [HttpPost]
            public string GetMsg2()
            {
                return "请求成功";
            } 

      测试1:分别请求GetMsg1和GetMsg2接口,其中GetMsg1接口报401没有权限, 402接口则正常请求成功

      测试2:

         A. 先请求GetToken接口获取token

         B. 通过postMan请求GetMsg1接口, 并且配置 Bearer Token, 请求成功.  详见:doc中的截图

     

    3. swagger中配置jwt

      背景:开启jwt校验后,swagger中无法请求接口了

      解决方案:

       (1). 在AddSwaggerGen中添加开启输入jwt校验的代码

    //给swagger中配置开启jwt输入
    builder.Services.AddSwaggerGen(c =>
    {
        var scheme = new OpenApiSecurityScheme()
        {
            Description = "Bearer认证, 即:说白了就是在Header中传递参数的时候('Authorization', 'Bearer ' + token),在值的前面加了一个Bearer和空格,然后在解析的时候需要隔离拿出来token值.",
            Reference = new OpenApiReference
            {
                Type = ReferenceType.SecurityScheme,
                Id = "Authorization"
            },
            Scheme = "oauth2",
            Name = "Authorization",
            In = ParameterLocation.Header,
            Type = SecuritySchemeType.ApiKey,
        };
        c.AddSecurityDefinition("Authorization", scheme);
        var requirement = new OpenApiSecurityRequirement();
        requirement[scheme] = new List<string>();
        c.AddSecurityRequirement(requirement);
    });

       (2). 运行后,进入swagger页面,右上角会出现一个Authorize的按钮, 点击后进入, 输入token

       (3). 重新请求getMsg1接口,就会自动携带token进行请求,该接口请求成功

     

    三. Jwt撤回问题

    1.需求

       比如用户被删除了、禁用了; jwt被盗用了; 单设备登录等场景, 由于jwt是有有效期的, 所以经常会出现了前面的场景后, token还没失效,也就是说还能正常使用, 那么我现在的, 需求就是当出现这些场景,可以手动的控制jwt过期问题

    2.解决方案1【不做探讨】

      把所有生成的jwt都在服务器上存一份(可以存放到redis里), 然后每次比较都要先查询一下redis里是否存在请求传过来的jwt,然后再进行准确性校验,另外可以手动控制删除redis中想让失效的jwt。

    3 .解决方案2【推荐】

       在用户表中增加一个整数类型的列JWTVersion,代表最后一次发放出去的令牌的版本号;每次登录成功,发放令牌的时候,都让JWTVersion的值自增,同时将JWTVersion的值也放到JWT令牌的负载payLoad中; 当执行禁用用户,撤回用户的令牌等操作的时候,把这个用户对应的JWTVersion列的值自增即可; 当服务器端收到客户端提交的JWT令牌后,先把JWT令牌中的JWTVersion值和数据库中JWTVersion的值做一下比较,如果JWT令牌中JWTVersion的值小于数据库中JWTVersion的值,就说明这个JWT令牌过期了。

     【补充:这套方案也适用于同一个账号不能同时访问系统的需求!!!】

    4. 版本1:直接从DB中拿jwtVersion获取

    思路:

     (1). 编写登录接口CheckLogin, 登录成功后, 将jwtVerson++, 并将其存放到jwt的payload负载中

     /// <summary>
            /// 登录接口
            /// </summary>
            /// <param name="user"></param>
            /// <returns></returns>
            [HttpPost]
            public async Task<IActionResult> CheckLogin(UserModel user)
            {
    
                //1.校验登录
                var userData = dbContext.Set<UserInfo>().Where(u => u.userAccount == user.userAccount && u.userPwd == user.userPwd).FirstOrDefault();
                if (userData != null)
                {
                    //1.1登录成功jwtVersion需要自增1
                    userData.jwtVersion++;
    
                    string secretKey = configuration["SecretKey"];
                    //1.2加密
                    //额外的header参数也可以不设置
                    var extraHeaders = new Dictionary<string, object>
                        {
                             {"myName", "limaru" },
                        };
                    //过期时间(可以不设置,下面表示签名后 20分钟过期)
                    double exp = (DateTime.UtcNow.AddMinutes(20) - new DateTime(1970, 1, 1)).TotalSeconds;
                    var payload = new Dictionary<string, object>
                        {
                             {"userId", userData.id},
                             {"userAccount", userData.userAccount },
                             {"jwtVersion", userData.jwtVersion.ToString() },
                             {"exp",exp }
                        };
    
                    //1.3 进行JWT签名
                    var token = JWTHelp.JWTJiaM(payload, secretKey, extraHeaders);
    
                    //1.4 保存数据库
                    _ = await dbContext.SaveChangesAsync();
    
                    return Ok(new { status = "ok", msg = "登录成功", token });
    
                }
                else
                {
                    return Ok(new { status = "error", msg = "登录失败" });
                }
    
            }

     (2).编写过滤器:CheckJwt

         A. 先校验是否有Skip标签   (为了保证完整性,该案例并无作用)

         B. 校验JWT自身的准确性(非空、过期、错误等)

         C. JWT自身校验通过后, 从jwt解密字符串中拿到jwtVersion、UserId ,根据UserId去数据库中查询JwtVersion, 如果为空, 校验直接不通过

         D. 如果DB中的JwtVersion不为空,且客户端的版本 >= DB中的版本号,则表示校验通过; 反之检验失败

    代码分享:

     /// <summary>
        /// 版本1--纯DB操作
        /// </summary>
        public class CheckJwt : IAsyncActionFilter
        {
            private readonly IConfiguration configuration;
    
            private readonly Core6xDBContext dbContext;
            public CheckJwt(IConfiguration configuration, Core6xDBContext dbContext)
            {
                this.configuration = configuration;
                this.dbContext = dbContext;
            }
    
            public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
            {
    
                //1. 先判断是否有skip跳过标签
                var isSkip = context.ActionDescriptor.EndpointMetadata.Any(x => x.GetType() == typeof(SkipAttribute));
                if (isSkip)
                {
                    await next();
                    return;
                }
    
                //2. 校验jwt自身的准确性
                var token = context.HttpContext.Request.Headers["auth"].ToString();    //ajax请求传过来
                if (token == "null" || string.IsNullOrEmpty(token))
                {
                    context.Result = new JsonResult(new { status = "error", msg = "非法请求,参数为空" });
                    return;
                }
                var result = JWTHelp.JWTJieM(token, configuration["SecretKey"]);
                if (result == "expired")
                {
                    context.Result = new JsonResult(new { status = "error", msg = "非法请求,参数已经过期" });
                    return;
                }
                else if (result == "invalid")
                {
                    context.Result = new JsonResult(new { status = "error", msg = "非法请求,未通过校验" });
                    return;
                }
                else if (result == "error")
                {
                    context.Result = new JsonResult(new { status = "error", msg = "非法请求,未通过校验" });
                    return;
                }
                else
                {
                    //3. 表示校验通过,接下来校验jwtVersion的准确性
                    var jwtModel = JsonConvert.DeserializeObject<JwtModel>(result);
    
                    //3.1 获取数据库中该用户的jwtVersion
                    long? myJwtVersion = dbContext.Set<UserInfo>().Where(u => u.id == jwtModel.userId).Select(u => u.jwtVersion).FirstOrDefault();
                    if (myJwtVersion==null)
                    {
                        context.Result = new JsonResult(new { status = "error", msg = "该用户不存在" });
                        return;
                    }
    
                    //3.2 客户端提交的版本 大于等于 DB的版本号, 验证通过 (客户端jwtVerson小于DB中的版本号, 则说明过期了)
    
                    if (long.Parse(jwtModel.jwtVersion) >= myJwtVersion)
                    {
                        //表示校验通过,执行action中的业务
                        context.RouteData.Values.Add("auth", result);
                        await next();
                    }
                    else
                    {
                        context.Result = new JsonResult(new { status = "error", msg = "jwt版本号错误" });
                        return;
                    }
    
                }
    
    
            }
    View Code

     测试:

        访问CheckLogin获取token, 然后携带token访问GetMsg接口,此时请求成功; 然后去DB中找到这个用户将jwtVersion增加1后,再次访问GetMsg接口,则访问不通过,提示jwtVerson版本号错误

            /// <summary>
            /// 版本1的验证
            /// </summary>
            /// <returns></returns>
            [HttpPost]
            [TypeFilter(typeof(CheckJwt))]
            public string GetMsg()
            {
                return "恭喜你,访问成功了";
    
            }

    如下图

     剖析:

        上述方案在CheckJwt中过滤器中每次都要访问DB,性能很差, 可以考虑引入内存缓存来处理这个问题,提供性能.

         

    5 版本2:引入内存缓存进行优化 【推荐】

    思路:

        使用IMemoryCache中的GetOrCreate方法,表示缓存存在,直接从缓存中读取内容并返回;缓存不存在,执行数据库读取操作→写入缓存→返回内容, 从而缓解了DB的压力

    代码分享:

     /// <summary>
        /// 版本2--引入内存缓存
        /// </summary>
        public class CheckJwt2 : IAsyncActionFilter
        {
            private readonly IConfiguration configuration;
    
            private readonly Core6xDBContext dbContext;
    
            private readonly IMemoryCache cache;
            public CheckJwt2(IConfiguration configuration, Core6xDBContext dbContext, IMemoryCache cache)
            {
                this.configuration = configuration;
                this.dbContext = dbContext;
                this.cache = cache;
            }
    
            public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
            {
    
                //1. 先判断是否有skip跳过标签
                var isSkip = context.ActionDescriptor.EndpointMetadata.Any(x => x.GetType() == typeof(SkipAttribute));
                if (isSkip)
                {
                    await next();
                    return;
                }
    
                //2. 校验jwt自身的准确性
                var token = context.HttpContext.Request.Headers["auth"].ToString();    //ajax请求传过来
                if (token == "null" || string.IsNullOrEmpty(token))
                {
                    context.Result = new JsonResult(new { status = "error", msg = "非法请求,参数为空" });
                    return;
                }
                var result = JWTHelp.JWTJieM(token, configuration["SecretKey"]);
                if (result == "expired")
                {
                    context.Result = new JsonResult(new { status = "error", msg = "非法请求,参数已经过期" });
                    return;
                }
                else if (result == "invalid")
                {
                    context.Result = new JsonResult(new { status = "error", msg = "非法请求,未通过校验" });
                    return;
                }
                else if (result == "error")
                {
                    context.Result = new JsonResult(new { status = "error", msg = "非法请求,未通过校验" });
                    return;
                }
                else
                {
                    //3. 表示校验通过,接下来校验jwtVersion的准确性
                    var jwtModel = JsonConvert.DeserializeObject<JwtModel>(result);
    
                    //3.1 获取数据库中该用户的jwtVersion 【此处引入缓存】
                    string cacheKey = $"JwtVersionCheck_{jwtModel.userId}";
                    //GetOrCreate用法:缓存存在,直接从缓存中读取内容并返回;缓存不存在,执行数据库读取操作→写入缓存→返回内容
                    long? myJwtVersion = cache.GetOrCreate(cacheKey, opt =>
                    {
                        opt.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(40);  //设置绝对过期时间为20s
                        return dbContext.Set<UserInfo>().Where(u => u.id == jwtModel.userId).Select(u => u.jwtVersion).FirstOrDefault();
                    });
                    if (myJwtVersion == null)
                    {
                        context.Result = new JsonResult(new { status = "error", msg = "该用户不存在" });
                        return;
                    }
    
                    //3.2 客户端提交的版本 大于等于 DB的版本号, 验证通过 (客户端jwtVerson小于DB中的版本号, 则说明过期了)
                    if (long.Parse(jwtModel.jwtVersion) >= myJwtVersion)
                    {
                        //表示校验通过,执行action中的业务
                        context.RouteData.Values.Add("auth", result);
                        await next();
                    }
                    else
                    {
                        context.Result = new JsonResult(new { status = "error", msg = "jwt版本号错误" });
                        return;
                    }
    
                }
    
    
            }
        }
    View Code

     测试:

           /// <summary>
            /// 版本2的验证
            /// </summary>
            /// <returns></returns>
            [HttpPost]
            [TypeFilter(typeof(CheckJwt2))]
            public string GetMsg2()
            {
                return "恭喜你,访问成功了";
    
            }

        访问CheckLogin获取token, 然后携带token访问GetMsg2接口,此时请求成功; 然后去DB中找到这个用户将jwtVersion增加1后,再次访问GetMsg2接口,由于缓存的过期时间为20s,还没有过期,

        读取的JwtVersion是修改前的值,所以还是能访问通过;等待20s过后,再次访问GetMsg2接口,则访问不通过,提示jwtVerson版本号错误

     剖析:

        (1).虽然引入内存缓存可以缓解DB的压力,但是有利就有弊,在缓存没有过期的这段时间里,手动增加DB中的JwtVersion,是无法让客户端jwt失效的,当然通常缓存过期时间设置的不长,该问题也无可厚非,关系不大的.

        (2).集群环境中,由于内存缓存等导致的并发问题,假如集群的A服务器中缓存保存的还是版本为5的数据,但客户端提交过来的可能已经是版本号为6的数据。     因此只要是客户端提交的版本号>=服务器上取出来(可能是从Db,也可能是从缓存)的版本号,那么也是可以的

    6 版本3:使用Redis存储JwtVersion【非常推荐】

    思路:

     将JwtVerson存放到Redis里, 即可以环境DB压力,也可以解决集群环境下内存缓存的局限性

     步骤:

       (1). 基于【CSRedisCore3.8.3】程序集进行redis访问,这里采用简单粗暴的写法  (详细可以参考:https://www.cnblogs.com/yaopengfei/p/14211883.html)

       (2). 在登录方法CheckLogin3中将 对该用户jwtVersion对应的key进行自增1, 然后将自增后的值写入payLoad中

     /// <summary>
            /// 登录接口--版本3使用
            /// </summary>
            /// <param name="user"></param>
            /// <returns></returns>
            [HttpPost]
            public async Task<IActionResult> CheckLogin3(UserModel user)
            {
    
                //1.校验登录
                var userData = dbContext.Set<UserInfo>().Where(u => u.userAccount == user.userAccount && u.userPwd == user.userPwd).FirstOrDefault();
                if (userData != null)
                {
                    //1.1登录成功redis里的jwtVersion需要自增1
                    var rds = new CSRedis.CSRedisClient(configuration["RedisStr"]);
                    string userKey = $"userInfo_{userData.id}";
                    var jwtVersion = rds.IncrBy(userKey);  //获取自增后的jwtVersion
    
    
                    string secretKey = configuration["SecretKey"];
                    //1.2加密
                    //额外的header参数也可以不设置
                    var extraHeaders = new Dictionary<string, object>
                        {
                             {"myName", "limaru" },
                        };
                    //过期时间(可以不设置,下面表示签名后 20分钟过期)
                    double exp = (DateTime.UtcNow.AddMinutes(20) - new DateTime(1970, 1, 1)).TotalSeconds;
                    var payload = new Dictionary<string, object>
                        {
                             {"userId", userData.id},
                             {"userAccount", userData.userAccount },
                             {"jwtVersion", jwtVersion.ToString() },
                             {"exp",exp }
                        };
    
                    //1.3 进行JWT签名
                    var token = JWTHelp.JWTJiaM(payload, secretKey, extraHeaders);
    
                    //1.4 保存数据库
                    _ = await dbContext.SaveChangesAsync();
    
                    return Ok(new { status = "ok", msg = "登录成功", token });
                }
                else
                {
                    return Ok(new { status = "error", msg = "登录失败" });
                }
            }

       (3). 编写过滤器CheckJwt3, 与版本1的逻辑相同,只不过改为从redis中读取jwtVersion了

    代码分享:

    /// <summary>
        /// 版本3--基于Redis存储jwtVersion
        /// </summary>
        public class CheckJwt3 : IAsyncActionFilter
        {
            private readonly IConfiguration configuration;
    
            private readonly Core6xDBContext dbContext;
            public CheckJwt3(IConfiguration configuration, Core6xDBContext dbContext)
            {
                this.configuration = configuration;
                this.dbContext = dbContext;
            }
    
            public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
            {
    
                //1. 先判断是否有skip跳过标签
                var isSkip = context.ActionDescriptor.EndpointMetadata.Any(x => x.GetType() == typeof(SkipAttribute));
                if (isSkip)
                {
                    await next();
                    return;
                }
    
                //2. 校验jwt自身的准确性
                var token = context.HttpContext.Request.Headers["auth"].ToString();    //ajax请求传过来
                if (token == "null" || string.IsNullOrEmpty(token))
                {
                    context.Result = new JsonResult(new { status = "error", msg = "非法请求,参数为空" });
                    return;
                }
                var result = JWTHelp.JWTJieM(token, configuration["SecretKey"]);
                if (result == "expired")
                {
                    context.Result = new JsonResult(new { status = "error", msg = "非法请求,参数已经过期" });
                    return;
                }
                else if (result == "invalid")
                {
                    context.Result = new JsonResult(new { status = "error", msg = "非法请求,未通过校验" });
                    return;
                }
                else if (result == "error")
                {
                    context.Result = new JsonResult(new { status = "error", msg = "非法请求,未通过校验" });
                    return;
                }
                else
                {
                    //3. 表示校验通过,接下来校验jwtVersion的准确性
                    var jwtModel = JsonConvert.DeserializeObject<JwtModel>(result);
    
                    //3.1 获取Redis该用户的jwtVersion
                    var rds = new CSRedis.CSRedisClient(configuration["RedisStr"]);
                    string userKey = $"userInfo_{jwtModel.userId}";
                    long? myJwtVersion = rds.Get<long?>(userKey);
    
                    if (myJwtVersion==null)
                    {
                        context.Result = new JsonResult(new { status = "error", msg = "该用户不存在" });
                        return;
                    }
    
                    //3.2 客户端提交的版本 大于等于 Redis的版本号, 验证通过 (客户端jwtVerson小于Redis中的版本号, 则说明过期了)
    
                    if (long.Parse(jwtModel.jwtVersion) >= myJwtVersion)
                    {
                        //表示校验通过,执行action中的业务
                        context.RouteData.Values.Add("auth", result);
                        await next();
                    }
                    else
                    {
                        context.Result = new JsonResult(new { status = "error", msg = "jwt版本号错误" });
                        return;
                    }
    
                }
    
    
            }
        }
    View Code

     测试:

         访问CheckLogin3获取token, 然后携带token访问GetMsg3接口,此时请求成功; 然后去Redis中找到这个用户将jwtVersion增加1后,再次访问GetMsg3接口,则访问不通过,提示jwtVerson版本号错误

     剖析:

        既缓解了DB的压力,同时还能解决集群问题

    四. 双token方案

      详见:

         https://www.cnblogs.com/yaopengfei/p/12449213.html

    !

    • 作       者 : Yaopengfei(姚鹏飞)
    • 博客地址 : http://www.cnblogs.com/yaopengfei/
    • 声     明1 : 如有错误,欢迎讨论,请勿谩骂^_^。
    • 声     明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。
     
  • 相关阅读:
    关于游戏
    学习lucene5.5.4的笔记
    lucene中文学习地址推荐
    lucene的使用与优化
    进一步了解this和super
    被遗忘的设计模式——空对象模式(Null Object Pattern)
    Java 空对象设计模式(Null Object Pattern) 讲解
    java的动态代理机制详解
    为什么要使用代理模式
    大O 表示法
  • 原文地址:https://www.cnblogs.com/yaopengfei/p/16328194.html
Copyright © 2020-2023  润新知