首先说声抱歉,可能是因为假期综合症(其实就是因为懒哈)的原因,已经很长时间没更新博客了,现在也调整的差不多了,准备还是以每周1-2篇的进度来更新博客,并完成本项目所有功能。

言归正传,本重构项目是在我根据实际需求重构,由于还未完全写完,所以也没进行压测,在2月份时,张善友老师给我留言说经过压测发现我重构的Ocelot网关功能性能较差,其中根本原因就是缓存模块,由于重构项目的缓存强依赖Redis缓存,造成性能瓶颈,发现问题后,我也第一时间进行测试,性能影响很大,经过跟张老师请教,可以使用二级缓存来解决性能问题,首先感谢张老师关注并指点迷津,于是就有了这篇文章,如何把现有缓存改成二级缓存并使用。

为了解决redis的强依赖性,首先需要把缓存数据存储到本地,所有请求都优先从本地提取,如果提取不到再从redis提取,如果redis无数据,在从数据库中提取。提取流程如下:

MemoryCache > Redis > db

此种方式减少提取缓存的网络开销,也合理利用了分布式缓存,并最终减少数据库的访问开销。但是使用此种方案也面临了一个问题是如何保证集群环境时每个机器本地缓存数据的一致性,这时我们会想到redis的发布、订阅特性,在数据发生变动时更新redis数据并发布缓存更新通知,由每个集群机器订阅变更事件,然后处理本地缓存记录,最终达到集群缓存的缓存一致性。

但是此方式对于缓存变更非常频繁的业务不适用,比如限流策略(准备还是使用分布式redis缓存实现),但是可以扩展配置单机限流时使用本地缓存实现,如果谁有更好的实现方式,也麻烦告知下集群环境下限流的实现,不胜感激。

改造代码

首先需要分析下目前改造后的Ocelot网关在哪些业务中使用的缓存,然后把使用本地缓存的的业务重构,增加提取数据流程,最后提供网关外部缓存初始化接口,便于与业务系统进行集成。

1

重写缓存方法

找到问题的原因后,就可以重写缓存方法,增加二级缓存支持,默认使用本地的缓存,新建CzarMemoryCache类,来实现IOcelotCache<T>方法,实现代码如下。

using Czar.Gateway.Configuration;using Czar.Gateway.RateLimit;using Microsoft.Extensions.Caching.Memory;using Ocelot.Cache;using System;namespace Czar.Gateway.Cache {    /// <summary>    /// 金焰的世界    /// 2019-03-03    /// 使用二级缓存解决集群环境问题    /// </summary>    public class CzarMemoryCache<T> : IOcelotCache<T>    {        private readonly CzarOcelotConfiguration _options;        private readonly IMemoryCache _cache;        public CzarMemoryCache(CzarOcelotConfiguration options,IMemoryCache cache)        {            _options = options;            _cache = cache;        }        public void Add(string key, T value, TimeSpan ttl, string region)        {            key = CzarOcelotHelper.GetKey(_options.RedisOcelotKeyPrefix,region, key);            if (_options.ClusterEnvironment)            {                var msg = value.ToJson();                if (typeof(T) == typeof(CachedResponse))                {//带过期时间的缓存                    _cache.Set(key, value, ttl); //添加本地缓存                    RedisHelper.Set(key, msg); //加入redis缓存                    RedisHelper.Publish(key, msg); //发布                }                else if (typeof(T) == typeof(CzarClientRateLimitCounter?))                {//限流缓存,直接使用redis                    RedisHelper.Set(key, value, (int)ttl.TotalSeconds);                }                else                {//正常缓存,发布                    _cache.Set(key, value, ttl); //添加本地缓存                    RedisHelper.Set(key, msg); //加入redis缓存                    RedisHelper.Publish(key, msg); //发布                }            }            else            {                _cache.Set(key, value, ttl); //添加本地缓存            }        }        public void AddAndDelete(string key, T value, TimeSpan ttl, string region)        {            Add(key, value, ttl, region);        }        public void ClearRegion(string region)        {            if (_options.ClusterEnvironment)            {                var keys = RedisHelper.Keys(region + "*");                RedisHelper.Del(keys);                foreach (var key in keys)                {                    RedisHelper.Publish(key, ""); //发布key值为空,处理时删除即可。                }            }            else            {                _cache.Remove(region);            }        }        public T Get(string key, string region)        {            key = CzarOcelotHelper.GetKey(_options.RedisOcelotKeyPrefix, region, key);            if(region== CzarCacheRegion.CzarClientRateLimitCounterRegion&& _options.ClusterEnvironment)            {//限流且开启了集群支持,默认从redis取                return RedisHelper.Get<T>(key);            }            var result = _cache.Get<T>(key);            if (result == null&& _options.ClusterEnvironment)            {                result= RedisHelper.Get<T>(key);                if (result != null)                {                    if (typeof(T) == typeof(CachedResponse))                    {//查看redis过期时间                        var second = RedisHelper.Ttl(key);                        if (second > 0)                        {                            _cache.Set(key, result, TimeSpan.FromSeconds(second));                        }                    }                    else                    {                        _cache.Set(key, result, TimeSpan.FromSeconds(_options.CzarCacheTime));                    }                }            }            return result;        }    } }

上面就段代码实现了本地缓存和Redis缓存的支持,优先从本地提取,如果在集群环境使用,增加redis缓存支持,但是此种方式不适用缓存变更非常频繁场景,比如客户端限流的实现,所以在代码中把客户端限流的缓存直接使用redis缓存实现。

2

注入实现和订阅

有了实现代码后,发现还缺少添加缓存注入和配置信息修改。首先需要修改配置文件来满足是否开启集群判断,然后需要实现redis的不同部署方式能够通过配置文件配置进行管理,避免硬编码导致的不可用问题。

配置文件CzarOcelotConfiguration.cs修改代码如下:

namespace Czar.Gateway.Configuration{    /// <summary>    /// 金焰的世界    /// 2018-11-11    /// 自定义配置信息    /// </summary>    public class CzarOcelotConfiguration    {        /// <summary>        /// 数据库连接字符串,使用不同数据库时自行修改,默认实现了SQLSERVER        /// </summary>        public string DbConnectionStrings { get; set; }        /// <summary>        /// 金焰的世界        /// 2018-11-12        /// 是否启用定时器,默认不启动        /// </summary>        public bool EnableTimer { get; set; } = false;        /// <summary>        /// 金焰的世界        /// 2018-11.12        /// 定时器周期,单位(毫秒),默认30分总自动更新一次        /// </summary>        public int TimerDelay { get; set; } = 30 * 60 * 1000;        /// <summary>        /// 金焰的世界        /// 2018-11-14        /// Redis连接字符串        /// </summary>        public string RedisConnectionString { get; set; }        /// <summary>        /// 金焰的世界        /// 2019-03-03        /// 配置哨兵或分区时使用        /// </summary>        public string[] RedisSentinelOrPartitionConStr { get; set; }        /// <summary>        /// 金焰的世界        /// 2019-03-03        /// Redis部署方式,默认使用普通方式        /// </summary>        public RedisStoreMode RedisStoreMode { get; set; } = RedisStoreMode.Normal;        /// <summary>        /// 金焰的计界        /// 2019-03-03        /// 做集群缓存同步时使用,会订阅所有正则匹配的事件        /// </summary>        public string RedisOcelotKeyPrefix { get; set; } = "CzarOcelot";        /// <summary>        /// 金焰的世界        /// 2019-03-03        /// 是否启用集群环境,如果非集群环境直接本地缓存+数据库即可        /// </summary>        public bool ClusterEnvironment { get; set; } = false;        /// <summary>        /// 金焰的世界        /// 2018-11-15        /// 是否启用客户端授权,默认不开启        /// </summary>        public bool ClientAuthorization { get; set; } = false;        /// <summary>        /// 金焰的世界        /// 2018-11-15        /// 服务器缓存时间,默认30分钟        /// </summary>        public int CzarCacheTime { get; set; } = 1800;        /// <summary>        /// 金焰的世界        /// 2018-11-15        /// 客户端标识,默认 client_id        /// </summary>        public string ClientKey { get; set; } = "client_id";        /// <summary>        /// 金焰的世界        /// 2018-11-18        /// 是否开启自定义限流,默认不开启        /// </summary>        public bool ClientRateLimit { get; set; } = false;    } }

在配置文件中修改了redis相关配置,支持使用redis的普通模式、集群模式、哨兵模式、分区模式,配置方式可参考csrediscore开源项目。

然后修改ServiceCollectionExtensions.cs代码,注入相关实现和redis客户端。

    builder.Services.AddMemoryCache(); //添加本地缓存#region 启动Redis缓存,并支持普通模式 官方集群模式  哨兵模式 分区模式if (options.ClusterEnvironment)            {//默认使用普通模式var csredis = new CSRedis.CSRedisClient(options.RedisConnectionString);switch (options.RedisStoreMode)                {case RedisStoreMode.Partition:var NodesIndex = options.RedisSentinelOrPartitionConStr;                        Func<string, string> nodeRule = null;                        csredis = new CSRedis.CSRedisClient(nodeRule, options.RedisSentinelOrPartitionConStr);break;case RedisStoreMode.Sentinel:                        csredis = new CSRedis.CSRedisClient(options.RedisConnectionString, options.RedisSentinelOrPartitionConStr);break;                }//初始化 RedisHelper                RedisHelper.Initialization(csredis);            }#endregion            builder.Services.AddSingleton<IOcelotCache<FileConfiguration>, CzarMemoryCache<FileConfiguration>>();            builder.Services.AddSingleton<IOcelotCache<InternalConfiguration>, CzarMemoryCache<InternalConfiguration>>();            builder.Services.AddSingleton<IOcelotCache<CachedResponse>, CzarMemoryCache<CachedResponse>>();            builder.Services.AddSingleton<IInternalConfigurationRepository, RedisInternalConfigurationRepository>();            builder.Services.AddSingleton<IOcelotCache<ClientRoleModel>, CzarMemoryCache<ClientRoleModel>>();            builder.Services.AddSingleton<IOcelotCache<RateLimitRuleModel>, CzarMemoryCache<RateLimitRuleModel>>();            builder.Services.AddSingleton<IOcelotCache<RemoteInvokeMessage>, CzarMemoryCache<RemoteInvokeMessage>>();            builder.Services.AddSingleton<IOcelotCache<CzarClientRateLimitCounter?>, CzarMemoryCache<CzarClientRateLimitCounter?>>();

现在需要实现redis订阅来更新本地的缓存信息,在项目启动时判断是否开启集群模式,如果开启就启动订阅,实现代码如下:

public static async Task<IApplicationBuilder> UseCzarOcelot(this IApplicationBuilder builder, OcelotPipelineConfiguration pipelineConfiguration){//重写创建配置方法var configuration = await CreateConfiguration(builder);    ConfigureDiagnosticListener(builder);    CacheChangeListener(builder);return CreateOcelotPipeline(builder, pipelineConfiguration);}/// <summary>/// 金焰的世界/// 2019-03-03/// 添加缓存数据变更订阅/// </summary>/// <param name="builder"></param>/// <returns></returns>private static void CacheChangeListener(IApplicationBuilder builder){var config= builder.ApplicationServices.GetService<CzarOcelotConfiguration>();var _cache= builder.ApplicationServices.GetService<IMemoryCache>();if (config.ClusterEnvironment)    {//订阅满足条件的所有事件        RedisHelper.PSubscribe(new[] { config.RedisOcelotKeyPrefix + "*" }, message =>              {var key = message.Channel;                  _cache.Remove(key); //直接移除,如果有请求从redis里取//或者直接判断本地缓存是否存在,如果存在更新,可自行实现。              });    }}

使用的是从配置文件提取的正则匹配的所有KEY都进行订阅,由于本地缓存增加了定时过期策略,所以为了实现方便,当发现redis数据发生变化,所有订阅端直接移除本地缓存即可,如果有新的请求直接从redis取,然后再次缓存,防止集群客户端缓存信息不一致。

为了区分不同的缓存实体,便于在原始数据发送变更时进行更新,定义CzarCacheRegion类。

namespace Czar.Gateway.Configuration{    /// <summary>    /// 缓存所属区域    /// </summary>    public class CzarCacheRegion    {        /// <summary>        /// 授权        /// </summary>        public const string AuthenticationRegion = "CacheClientAuthentication";        /// <summary>        /// 路由配置        /// </summary>        public const string FileConfigurationRegion = "CacheFileConfiguration";        /// <summary>        /// 内部配置        /// </summary>        public const string InternalConfigurationRegion = "CacheInternalConfiguration";        /// <summary>        /// 客户端权限        /// </summary>        public const string ClientRoleModelRegion = "CacheClientRoleModel";        /// <summary>        /// 限流规则        /// </summary>        public const string RateLimitRuleModelRegion = "CacheRateLimitRuleModel";        /// <summary>        /// Rpc远程调用        /// </summary>        public const string RemoteInvokeMessageRegion = "CacheRemoteInvokeMessage";        /// <summary>        /// 客户端限流        /// </summary>        public const string CzarClientRateLimitCounterRegion = "CacheCzarClientRateLimitCounter";    } }

现在只需要修改缓存的region为定义的值即可,唯一需要改动的代码就是把之前写死的代码改成如下代码即可。

var enablePrefix = CzarCacheRegion.AuthenticationRegion;

3

开发缓存变更接口

现在整个二级缓存基本完成,但是还遇到一个问题就是外部如何根据数据库变更数据时来修改缓存数据,这时就需要提供外部修改api来实现。

添加CzarCacheController.cs对外部提供缓存更新相关接口,详细代码如下:

using Czar.Gateway.Authentication;using Czar.Gateway.Configuration;using Czar.Gateway.RateLimit;using Czar.Gateway.Rpc;using Microsoft.AspNetCore.Authorization;using Microsoft.AspNetCore.Mvc;using Microsoft.Extensions.Caching.Memory;using Ocelot.Configuration;using Ocelot.Configuration.Creator;using Ocelot.Configuration.Repository;using System;using System.Threading.Tasks;namespace Czar.Gateway.Cache{    /// <summary>    /// 提供外部缓存处理接口    /// </summary>    [Authorize]    [Route("CzarCache")]    public class CzarCacheController : Controller    {        private readonly CzarOcelotConfiguration _options;        private readonly IClientAuthenticationRepository _clientAuthenticationRepository;        private IFileConfigurationRepository _fileConfigurationRepository;        private IInternalConfigurationCreator _internalConfigurationCreator;        private readonly IClientRateLimitRepository _clientRateLimitRepository;        private readonly IRpcRepository _rpcRepository;        private readonly IMemoryCache _cache;        public CzarCacheController(IClientAuthenticationRepository clientAuthenticationRepository, CzarOcelotConfiguration options,          IFileConfigurationRepository fileConfigurationRepository,          IInternalConfigurationCreator internalConfigurationCreator,          IClientRateLimitRepository clientRateLimitRepository,          IRpcRepository rpcRepository,          IMemoryCache cache)        {            _clientAuthenticationRepository = clientAuthenticationRepository;            _options = options;            _fileConfigurationRepository = fileConfigurationRepository;            _internalConfigurationCreator = internalConfigurationCreator;            _clientRateLimitRepository = clientRateLimitRepository;            _rpcRepository = rpcRepository;            _cache = cache;        }        /// <summary>        /// 更新客户端地址访问授权接口        /// </summary>        /// <param name="clientid">客户端ID</param>        /// <param name="path">请求模板</param>        /// <returns></returns>        [HttpPost]        [Route("ClientRule")]        public async Task UpdateClientRuleCache(string clientid, string path)        {            var region = CzarCacheRegion.AuthenticationRegion;            var key = CzarOcelotHelper.ComputeCounterKey(region, clientid, "", path);            key = CzarOcelotHelper.GetKey(_options.RedisOcelotKeyPrefix, region, key);            var result = await _clientAuthenticationRepository.ClientAuthenticationAsync(clientid, path);            var data = new ClientRoleModel() { CacheTime = DateTime.Now, Role = result };            if (_options.ClusterEnvironment)            {                RedisHelper.Set(key, data); //加入redis缓存                RedisHelper.Publish(key, data.ToJson()); //发布事件            }            else            {                _cache.Remove(key);            }        }        /// <summary>        /// 更新网关配置路由信息        /// </summary>        /// <returns></returns>        [HttpPost]        [Route("InternalConfiguration")]        public async Task UpdateInternalConfigurationCache()        {            var key = CzarCacheRegion.InternalConfigurationRegion;            key = CzarOcelotHelper.GetKey(_options.RedisOcelotKeyPrefix, "", key);            var fileconfig = await _fileConfigurationRepository.Get();            var internalConfig = await _internalConfigurationCreator.Create(fileconfig.Data);            var config = (InternalConfiguration)internalConfig.Data;            if (_options.ClusterEnvironment)            {                RedisHelper.Set(key, config); //加入redis缓存                RedisHelper.Publish(key, config.ToJson()); //发布事件            }            else            {                _cache.Remove(key);            }        }        /// <summary>        /// 删除路由配合的缓存信息        /// </summary>        /// <param name="region">区域</param>        /// <param name="downurl">下端路由</param>        /// <returns></returns>        [HttpPost]        [Route("Response")]        public async Task DeleteResponseCache(string region,string downurl)        {            var key = CzarOcelotHelper.GetKey(_options.RedisOcelotKeyPrefix, region, downurl);            if (_options.ClusterEnvironment)            {                await RedisHelper.DelAsync(key);                RedisHelper.Publish(key, "");//发布时间            }            else            {                _cache.Remove(key);            }        }        /// <summary>        /// 更新客户端限流规则缓存        /// </summary>        /// <param name="clientid">客户端ID</param>        /// <param name="path">路由模板</param>        /// <returns></returns>        [HttpPost]        [Route("RateLimitRule")]        public async Task UpdateRateLimitRuleCache(string clientid, string path)        {            var region = CzarCacheRegion.RateLimitRuleModelRegion;            var key = clientid + path;            key = CzarOcelotHelper.GetKey(_options.RedisOcelotKeyPrefix, region, key);            var result = await _clientRateLimitRepository.CheckClientRateLimitAsync(clientid, path);            var data = new RateLimitRuleModel() { RateLimit = result.RateLimit, rateLimitOptions = result.rateLimitOptions };            if (_options.ClusterEnvironment)            {                RedisHelper.Set(key, data); //加入redis缓存                RedisHelper.Publish(key, data.ToJson()); //发布事件            }            else            {                _cache.Remove(key);            }        }        /// <summary>        /// 更新客户端是否开启限流缓存        /// </summary>        /// <param name="path"></param>        /// <returns></returns>        [HttpPost]        [Route("ClientRole")]        public async Task UpdateClientRoleCache(string path)        {            var region = CzarCacheRegion.ClientRoleModelRegion;            var key = path;            key = CzarOcelotHelper.GetKey(_options.RedisOcelotKeyPrefix, region, key);            var result = await _clientRateLimitRepository.CheckReRouteRuleAsync(path);            var data = new ClientRoleModel() { CacheTime = DateTime.Now, Role = result };            if (_options.ClusterEnvironment)            {                RedisHelper.Set(key, data); //加入redis缓存                RedisHelper.Publish(key, data.ToJson()); //发布事件            }            else            {                _cache.Remove(key);            }        }        /// <summary>        /// 更新呢客户端路由白名单缓存        /// </summary>        /// <param name="clientid"></param>        /// <param name="path"></param>        /// <returns></returns>        [HttpPost]        [Route("ClientReRouteWhiteList")]        public async Task UpdateClientReRouteWhiteListCache(string clientid, string path)        {            var region = CzarCacheRegion.ClientReRouteWhiteListRegion;            var key = clientid + path;            key = CzarOcelotHelper.GetKey(_options.RedisOcelotKeyPrefix, region, key);            var result = await _clientRateLimitRepository.CheckClientReRouteWhiteListAsync(clientid, path);            var data = new ClientRoleModel() { CacheTime = DateTime.Now, Role = result };            if (_options.ClusterEnvironment)            {                RedisHelper.Set(key, data); //加入redis缓存                RedisHelper.Publish(key, data.ToJson()); //发布事件            }            else            {                _cache.Remove(key);            }        }        [HttpPost]        [Route("Rpc")]        public async Task UpdateRpcCache(string UpUrl)        {            var region = CzarCacheRegion.RemoteInvokeMessageRegion;            var key = UpUrl;            key = CzarOcelotHelper.GetKey(_options.RedisOcelotKeyPrefix, region, key);            var result = await _rpcRepository.GetRemoteMethodAsync(UpUrl);            if (_options.ClusterEnvironment)            {                RedisHelper.Set(key, result); //加入redis缓存                RedisHelper.Publish(key, result.ToJson()); //发布事件            }            else            {                _cache.Remove(key);            }        }    } }

现在基本实现整个缓存的更新策略,只要配合后台管理界面,在相关缓存原始数据发送变更时,调用对应接口即可完成redis缓存的更新,并自动通知集群的所有本机清理缓存等待重新获取。

接口的调用方式参考之前我写的配置信息接口变更那篇即可。

性能测试

完成了改造后,我们拿改造前网关、改造后网关、原始Ocelot、直接调用API四个环境分别测试性能指标,由于测试环境有效,我直接使用本机环境,然后是Apache ab测试工具测试下相关性能(本测试不一定准确,只作为参考指标),测试的方式是使用100个并发请求10000次,测试结果分别如下。

改造网关性能测试

改造后网关测试

Ocelot默认网关性能

直接调用API性能

本测试仅供参考,因为由于网关和服务端都在本机环境部署,所以使用网关和不使用网关性能差别非常小,如果分开部署可能性别差别会明显写,这不是本篇讨论的重点。

从测试中可以看到,重构的网关改造前和改造后性能有2倍多的提升,且与原生的Ocelot性能非常接近。

最后

本篇主要讲解了如何使用redis的发布订阅来实现二级缓存功能,并提供了缓存的更新相关接口供外部程序调用,避免出现集群环境下无法更新缓存数据导致提取数据不一致情况,但是针对每个客户端独立限流这块集群环境目前还是采用的redis的方式未使用本地缓存,如果有写的不对或有更好方式的,也希望多提宝贵意见。

【.NET Core项目实战-统一认证平台】第十五章 网关篇-使用二级缓存提升性能相关推荐

  1. 【.NET Core项目实战-统一认证平台】第五章 网关篇-自定义缓存Redis

    上篇文章[.NET Core项目实战-统一认证平台]第四章 网关篇-数据库存储配置(2)我们介绍了2种网关配置信息更新的方法和扩展Mysql存储,本篇我们将介绍如何使用Redis来实现网关的所有缓存功 ...

  2. 【.NET Core项目实战-统一认证平台】第七章 网关篇-自定义客户端限流

    上篇文章我介绍了如何在网关上增加自定义客户端授权功能,从设计到编码实现,一步一步详细讲解,相信大家也掌握了自定义中间件的开发技巧了,本篇我们将介绍如何实现自定义客户端的限流功能,来进一步完善网关的基础 ...

  3. 【.NET Core项目实战-统一认证平台】第六章 网关篇-自定义客户端授权

    上篇文章[.NET Core项目实战-统一认证平台]第五章 网关篇-自定义缓存Redis 我们介绍了网关使用Redis进行缓存,并介绍了如何进行缓存实现,缓存信息清理接口的使用.本篇我们将介绍如何实现 ...

  4. 【.NET Core项目实战-统一认证平台】第四章 网关篇-数据库存储配置(2)

    [.NET Core项目实战-统一认证平台]第四章 网关篇-数据库存储配置(2) 原文:[.NET Core项目实战-统一认证平台]第四章 网关篇-数据库存储配置(2) [.NET Core项目实战- ...

  5. 【.NET Core项目实战-统一认证平台】第三章 网关篇-数据库存储配置(1)

    [.NET Core项目实战-统一认证平台]第三章 网关篇-数据库存储配置(1) 原文:[.NET Core项目实战-统一认证平台]第三章 网关篇-数据库存储配置(1) [.NET Core项目实战- ...

  6. 【.NET Core项目实战-统一认证平台】第十一章 授权篇-密码授权模式

    上篇文章介绍了基于Ids4客户端授权的原理及如何实现自定义的客户端授权,并配合网关实现了统一的授权异常返回值和权限配置等相关功能,本篇将介绍密码授权模式,从使用场景.源码剖析到具体实现详细讲解密码授权 ...

  7. 【.NET Core项目实战-统一认证平台】第十三章 授权篇-如何强制有效令牌过期

    上一篇我介绍了JWT的生成验证及流程内容,相信大家也对JWT非常熟悉了,今天将从一个小众的需求出发,介绍如何强制令牌过期的思路和实现过程. .netcore项目实战交流群(637326624),有兴趣 ...

  8. 【.NET Core项目实战-统一认证平台】第十四章 授权篇-自定义授权方式

    上篇文章我介绍了如何强制令牌过期的实现,相信大家对IdentityServer4的验证流程有了更深的了解,本篇我将介绍如何使用自定义的授权方式集成老的业务系统验证,然后根据不同的客户端使用不同的认证方 ...

  9. 【.NET Core项目实战-统一认证平台】第十二章 授权篇-深入理解JWT生成及验证流程...

    上篇文章介绍了基于Ids4密码授权模式,从使用场景.原理分析.自定义帐户体系集成完整的介绍了密码授权模式的内容,并最后给出了三个思考问题,本篇就针对第一个思考问题详细的讲解下Ids4是如何生成acce ...

最新文章

  1. 分享26个关于Java开发视频教程(免费下载)
  2. 数据集成之主数据管理(一)基础概念篇
  3. 特斯拉炫技现场:电驴、行人、快递车,中国的小路难不倒Autopilot自动驾驶
  4. Elasticsearch Grok Pattern内置表达式大全
  5. java调用wcf控件的两种交互
  6. 7个你可能不认识的CSS单位
  7. Spring Boot文档阅读笔记-对Messaging with RabbitMQ解析
  8. canvas笔记-使用canvas画圆及点阵的使用
  9. 学术诚信的重要性_关于学术诚信
  10. ONAP如何将Open-O和ECOMP数百万行代码合并?
  11. 【SICP练习】71 练习2.42
  12. centos7 安装python3.6 及模块安装演示
  13. 转行程序员深漂的这三年 #2
  14. html5怎么插入一段文字,HTML5教程—文字插入进度动画_HTML5教程_文字插入_动画进度_课课家...
  15. 【C语言】17-预处理指令3-文件包含
  16. Python实现图片文字识别
  17. Twincat3之C++
  18. 微信小程序面试题大全
  19. new HashMap(list.size())指定size就能完全避免扩容带来的额外开销了吗?
  20. 《离散数学》每章内容及其重点梳理

热门文章

  1. 如何将Apple Mail建议用于事件和联系人
  2. Vue根据菜单json数据动态按需加载路由Vue-router
  3. mysql导入sql脚本命令
  4. 手机自动化测试:appium源码分析之bootstrap七
  5. 给IT新人的15个建议:苦逼程序员的辛酸反省与总结
  6. Zune 3.0与XNA GS 3.0 Beta
  7. 如何保证执行异步方法时不会遗漏 await 关键字
  8. 里程碑 .Net7再更新,从此彻底碾压Java!
  9. 巧用ActionFilter的AOP特性,为返回的数据增加返回码和消息
  10. 日常使用Git,这些问题你遇到过吗?