【.NET Core项目实战-统一认证平台】开篇及目录索引

一、什么是RPC

RPC是“远程调用(Remote Procedure Call)”的一个名称的缩写,并不是任何规范化的协议,也不是大众都认知的协议标准,我们更多时候使用时都是创建的自定义化(例如Socket,Netty)的消息方式进行调用,相比http协议,我们省掉了不少http中无用的消息内容。因此很多系统内部调用仍然采用自定义化的RPC调用模式进行通信,毕竟速度和性能是内网的关键指标之一,而标准化和语义无关性在外网中举足轻重。所以,为何API网关无法工作在RPC上,因为它没有一个像HTTP/HTTPS那样的通用标准。

二、CzarRpc简介

CzarRpc是作者基于Dotnetty实现的RPC通讯框架,参考了SurgingTars.Net优秀设计,目前正在内部使用中,下面就CzarRpc调用方式做一个简单介绍,测试结构如下:

1、服务接口

新建一个Czar.Rpc.Common类库,首先需要引用Czar.RpcNuget包。

Install-Package Czar.Rpc

然后定义测试接口IHelloRpc.cs,也是目前支持的调用方式。

using Czar.Rpc.Attributes;
using Czar.Rpc.Exceptions;
using Czar.Rpc.Metadata;
using System;
using System.Collections.Generic;
using System.Threading.Tasks; namespace Czar.Rpc.Common
{
/// <summary>
/// 测试Rpc实体
/// </summary>
[BusinessExceptionInterceptor]
[CzarRpc("Demo.Rpc.Hello")]
public interface IHelloRpc: IRpcBaseService
{
string Hello(int no, string name); void HelloHolder(int no, out string name); Task<string> HelloTask(int no, string name); ValueTask<string> HelloValueTask(int no, string name); [CzarOneway]
void HelloOneway(int no, string name); Task TestBusinessExceptionInterceptor(); DemoModel HelloModel(int D1, string D2, DateTime D3); Task<DemoModel> HelloModelAsync(int D1, string D2, DateTime D3); DemoModel HelloSendModel(DemoModel model); DemoModel HelloSendModelParm(string name,DemoModel model); List<DemoModel> HelloSendModelList(List<DemoModel> model);
}
public class DemoModel
{
/// <summary>
/// 测试1
/// </summary>
public int T1 { get; set; } /// <summary>
/// 测试2
/// </summary>
public string T2 { get; set; } /// <summary>
/// 测试3
/// </summary>
public DateTime T3 { get; set; } public ChildModel Child { get; set; }
} public class ChildModel
{
public string C1 { get; set; }
}
}

2.服务端

新建一个控制台程序Czar.Rpc.Server,然后实现服务接口,因为都是测试数据,所以就随意实现了方法。

HelloRpcServer.cs

using Czar.Rpc.Exceptions;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Linq;
using System.Net;
using Czar.Rpc.Common; namespace Demo.Rpc.Server
{
public class HelloRpcServer: IHelloRpc
{
public EndPoint CzarEndPoint { get; set; } public string Hello(int no, string name)
{
string result = $"{no}: Hi, {name}";
Console.WriteLine(result);
return result + " callback";
} public void HelloHolder(int no, out string name)
{
name = no.ToString() + " out";
} public void HelloOneway(int no, string name)
{
/*
耗时操作
*/
Console.WriteLine($"From oneway - {no}: Hi, {name}");
} public Task<string> HelloTask(int no, string name)
{
return Task.FromResult(Hello(no, name));
} public ValueTask<string> HelloValueTask(int no, string name)
{
return new ValueTask<string>(Hello(no, name));
} public Task TestBusinessExceptionInterceptor()
{
throw new BusinessException()
{
CzarCode = "1",
CzarMessage = "test"
};
} public DemoModel HelloModel(int D1, string D2, DateTime D3)
{
return new DemoModel()
{
T1 = D1 + 1,
T2 = D2 + "2",
T3 = D3.AddDays(1)
};
} public async Task<DemoModel> HelloModelAsync(int D1, string D2, DateTime D3)
{
return await Task.FromResult(
new DemoModel()
{
T1 = D1 + 1,
T2 = D2 + "77777",
T3 = D3.AddDays(1)
}
);
} public DemoModel HelloSendModel(DemoModel model)
{
model.T1 = model.T1 + 10;
model.T2 = model.T2 + "11";
model.T3 = model.T3.AddDays(12);
return model;
} public DemoModel HelloSendModelParm(string name, DemoModel model)
{
model.T1 = model.T1 + 10;
model.T2 = model.T2 + "11";
model.T3 = model.T3.AddDays(12);
if (model.Child != null)
{
model.Child.C1 = name+"说:"+ model.Child.C1;
}
return model;
} public List<DemoModel> HelloSendModelList(List<DemoModel> model)
{
return model.Select(t => new DemoModel() { T1=t.T1+10,T2=t.T2+"13",T3=t.T3.AddYears(1),Child=t.Child }).ToList();
}
}
}

然后启动服务端监听。

class Program
{
static void Main(string[] args)
{
var host = new HostBuilder()
.ConfigureHostConfiguration(i => i.AddJsonFile("CzarConfig.json"))
.ConfigureLogging((hostContext, configLogging) =>
{
configLogging.AddConsole();
})
.UseCodec<JsonCodec>()
.UseLibuvTcpHost()
.UseProxy()
.UseConsoleLifetime()
.Build(); host.RunAsync().Wait();
}
}

启用外部使用CzarConfig.json的配置文件,注意需要设置成始终复制。

{
"CzarHost": {
"Port": 7711, //监听端口
"QuietPeriodSeconds": 2, //退出静默时间 DotNetty特性
"ShutdownTimeoutSeconds": 2, //关闭超时时间 DotNetty特性
"IsSsl": "false", //是否启用 SSL, 客户端需要保持一致
"PfxPath": "cert/datasync.pfx", //证书
"PfxPassword": "123456" //证书密钥
}
}

到此服务器端搭载完成。

3、客户端

新建客户端控制台程序Czar.Rpc.Client,然后配置Rpc调用信息。

{
"CzarHost": {
"ProxyEndPoint": true, //是否启用动态服务地址,就是指定服务端IP
"IsSsl": "false", //是否启用SSL
"PfxPath": "cert/datasync.pfx", //证书
"PfxPassword": "123456", //证书密钥
"ClientConfig": { //客户端配置
"Demo.Rpc.Hello": { //对应服务[CzarRpc("Demo.Rpc.Hello")] 值
"Host": "127.0.0.1", //服务端IP 如果ProxyEndPoint=false 时使用
"Port": 7711, //服务端端口 如果ProxyEndPoint=false 时使用
"Timeout": 10, //调用超时时间
"WriterIdleTimeSeconds";30 //空闲超时时间,默认为30秒,非内网环境建议设置成5分钟内。
}
}
}
}

现在开始启用客户端信息。

class Program
{
public static IServiceProvider service;
public static IConfiguration config;
static async Task Main(string[] args)
{
try
{
var builder = new ConfigurationBuilder();
config = builder.AddJsonFile("CzarConfig.json").Build(); service = new ServiceCollection()
.AddSingleton(config)
.AddLogging(j => j.AddConsole())
.AddLibuvTcpClient(config)
.AddProxy()
.BuildDynamicProxyServiceProvider(); var rpc = service.GetRequiredService<IHelloRpc>();
//使用的内部指定的服务器地址
rpc.CzarEndPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 7711);
var result = string.Empty; string t = "基本调用";
result = rpc.Hello(18, t);
Console.WriteLine(result); result = "无返回结果";
rpc.HelloHolder(1, out result);
Console.WriteLine(result);
result = await rpc.HelloTask(2, "异步任务");
Console.WriteLine(result);
result = "单向";
rpc.HelloOneway(3, "单向调用");
Console.WriteLine(result);
result = await rpc.HelloValueTask(4, "ValueTask任务");
Console.WriteLine(result); var modelResult = rpc.HelloModel(5, "返回实体", DateTime.Now);
Console.WriteLine($"{modelResult.T1} {modelResult.T2} {modelResult.T3.ToLongDateString()}"); var modelResult1 = await rpc.HelloModelAsync(6, "返回Task实体", DateTime.Now);
Console.WriteLine($"{modelResult1.T1} {modelResult1.T2} {modelResult1.T3.ToLongDateString()}"); var mm = new DemoModel()
{
T1 = 7,
T2 = "传实体返回实体",
T3 = DateTime.Now,
Child = new ChildModel()
{
C1 = "子类1"
}
};
var model2 = rpc.HelloSendModel(mm);
Console.WriteLine($"{model2.T1} {model2.T2} {model2.T3.ToLongDateString()} {model2.Child.C1}"); var list = new List<DemoModel>();
var mm1 = new DemoModel()
{
T1 = 8,
T2 = "传List返回List",
T3 = DateTime.Now,
Child = new ChildModel()
{
C1 = "子类2"
}
};
var mm3 = new DemoModel()
{
T1 = 9,
T2 = "传List返回List",
T3 = DateTime.Now,
Child = new ChildModel()
{
C1 = "子类3"
}
};
list.Add(mm1);
list.Add(mm3);
var list3 = rpc.HelloSendModelList(list);
Console.WriteLine($"{list3[0].T1} {list3[0].T2} {list3[0].T3.ToLongDateString()} {list3[0].Child?.C1}"); var mm4 = new DemoModel()
{
T1 = 9,
T2 = "HelloSendModelParm",
T3 = DateTime.Now,
Child = new ChildModel()
{
C1 = "子类4"
}
};
var dd = rpc.HelloSendModelParm("HelloSendModelParm", mm4);
Console.WriteLine($"{dd.T1} {dd.T2} {dd.T3.ToLongDateString()} {dd.Child.C1}"); //异常调用
await rpc.TestBusinessExceptionInterceptor();
}
catch (BusinessException e)
{
Console.WriteLine($"CzarCode:{e.CzarCode} CzarMessage:{e.CzarMessage}");
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
Console.ReadLine();
}
}

现在整个RPC调用搭建完毕,然后分别启动服务器端和客户端,就可以看到屏幕输出内容如下。

客户端输出:

服务器端输出:

至此整个CzarRpc的基本使用已经介绍完毕,感兴趣的朋友可以自行测试。

三、Ocelot增加RPC支持

有了CzarRpc的通讯框架后,现在在Ocelot上实现Rpc功能简直易如反掌,现在开始添加我们的Rpc中间件,也让我们扩展的网关灵活起来。

还记得我介绍网关篇时添加中间件的步骤吗?如果不记得的可以先回去回顾下。

首先如何让网关知道这个后端调用是http还是Rpc呢?这时应该会想到Ocelot路由配置里的DownstreamScheme,可以在这里判断我们定义的是http还是rpc即可。同时我们希望之前定义的所有中间件都生效,最后一步请求时如果配置下端路由rpc,使用rpc调用,否则使用http调用,这样可以重复利用之前所有的中间件功能,减少重复开发。

在之前的开发的自定义限流和自定义授权中间件开发中,我们知道开发完的中间件放到哪里使用,这里就不介绍原理了,直接添加到BuildCzarOcelotPipeline里如下代码。

public static OcelotRequestDelegate BuildCzarOcelotPipeline(this IOcelotPipelineBuilder builder,
OcelotPipelineConfiguration pipelineConfiguration)
{ // 注册一个全局异常
builder.UseExceptionHandlerMiddleware(); // 如果请求是websocket使用单独的管道
builder.MapWhen(context => context.HttpContext.WebSockets.IsWebSocketRequest,
app =>
{
app.UseDownstreamRouteFinderMiddleware();
app.UseDownstreamRequestInitialiser();
app.UseLoadBalancingMiddleware();
app.UseDownstreamUrlCreatorMiddleware();
app.UseWebSocketsProxyMiddleware();
}); // 添加自定义的错误管道
builder.UseIfNotNull(pipelineConfiguration.PreErrorResponderMiddleware); //使用自定义的输出管道
builder.UseCzarResponderMiddleware(); // 下游路由匹配管道
builder.UseDownstreamRouteFinderMiddleware(); //增加自定义扩展管道
if (pipelineConfiguration.MapWhenOcelotPipeline != null)
{
foreach (var pipeline in pipelineConfiguration.MapWhenOcelotPipeline)
{
builder.MapWhen(pipeline);
}
} // 使用Http头部转换管道
builder.UseHttpHeadersTransformationMiddleware(); // 初始化下游请求管道
builder.UseDownstreamRequestInitialiser(); // 使用自定义限流管道
builder.UseRateLimiting(); //使用请求ID生成管道
builder.UseRequestIdMiddleware(); //使用自定义授权前管道
builder.UseIfNotNull(pipelineConfiguration.PreAuthenticationMiddleware); //根据请求判断是否启用授权来使用管道
if (pipelineConfiguration.AuthenticationMiddleware == null)
{
builder.UseAuthenticationMiddleware();
}
else
{
builder.Use(pipelineConfiguration.AuthenticationMiddleware);
} //添加自定义限流中间件 2018-11-18 金焰的世界
builder.UseCzarClientRateLimitMiddleware(); //添加自定义授权中间件 2018-11-15 金焰的世界
builder.UseAhphAuthenticationMiddleware(); //启用自定义的认证之前中间件
builder.UseIfNotNull(pipelineConfiguration.PreAuthorisationMiddleware); //是否使用自定义的认证中间件
if (pipelineConfiguration.AuthorisationMiddleware == null)
{
builder.UseAuthorisationMiddleware();
}
else
{
builder.Use(pipelineConfiguration.AuthorisationMiddleware);
} // 使用自定义的参数构建中间件
builder.UseIfNotNull(pipelineConfiguration.PreQueryStringBuilderMiddleware); // 使用负载均衡中间件
builder.UseLoadBalancingMiddleware(); // 使用下游地址创建中间件
builder.UseDownstreamUrlCreatorMiddleware(); // 使用缓存中间件
builder.UseOutputCacheMiddleware(); //判断下游的是否启用rpc通信,切换到RPC处理
builder.MapWhen(context => context.DownstreamReRoute.DownstreamScheme.Equals("rpc", StringComparison.OrdinalIgnoreCase), app =>
{
app.UseCzarRpcMiddleware();
}); //使用下游请求中间件
builder.UseCzaHttpRequesterMiddleware(); return builder.Build();
}

这里是在最后请求前判断使用的下游请求方式,如果DownstreamScheme使用的rpc,就使用rpc中间件处理。

Rpc处理的完整逻辑是,如何从http请求中获取想要解析的参数,这里需要设置匹配的优先级,目前设计的优先级为。

1、首先提取路由参数,如果匹配上就是用路由参数名称为key,值为value,按顺序组成第一批参数。

2、提取query参数,如有有值按顺序组成第二批参数。

3、如果非Get请求,提取body内容,如果非空,组成第三批参数

4、从配置库里提取rpc路由调用的服务名称和函数名称,以及是否单向调用。

5、按照获取的数据进行rpc调用并等待返回。

看了上面的设计是不是思路很清晰了呢?

1、rpc路由表设计

CREATE TABLE AhphReRouteRpcConfig
(
RpcId int IDENTITY(1,1) NOT NULL,
ReRouteId int, //路由表主键
ServantName varchar(100) NOT NULL, //调用的服务名称
FuncName varchar(100) NOT NULL, //调用的方法名称
IsOneway bit NOT NULL //是否单向调用
)

2、提取远程调用方法

根据上游路由获取远程调用的配置项目

public interface IRpcRepository
{
/// <summary>
/// 根据模板地址获取RPC请求方法
/// </summary>
/// <param name="UpUrl">上游模板</param>
/// <returns></returns>
Task<RemoteInvokeMessage> GetRemoteMethodAsync(string UpUrl);
} public class SqlServerRpcRepository : IRpcRepository
{
private readonly CzarOcelotConfiguration _option;
public SqlServerRpcRepository(CzarOcelotConfiguration option)
{
_option = option;
} /// <summary>
/// 获取RPC调用方法
/// </summary>
/// <param name="UpUrl"></param>
/// <returns></returns>
public async Task<RemoteInvokeMessage> GetRemoteMethodAsync(string UpUrl)
{
using (var connection = new SqlConnection(_option.DbConnectionStrings))
{
string sql = @"select T4.* from AhphGlobalConfiguration t1 inner join AhphConfigReRoutes T2 on
T1.AhphId=t2.AhphId inner join AhphReRoute T3 on T2.ReRouteId=T3.ReRouteId
INNER JOIN AhphReRouteRpcConfig T4 ON T3.ReRouteId=T4.ReRouteId
where IsDefault=1 and T1.InfoStatus=1 AND T3.InfoStatus=1 AND UpstreamPathTemplate=@URL";
var result = await connection.QueryFirstOrDefaultAsync<RemoteInvokeMessage>(sql, new { URL = UpUrl });
return result;
}
}
}

3、重写返回结果

由于rpc调用后是返回的Json封装的信息,需要解析成对应的HttpContent。

using System.IO;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks; namespace Czar.Gateway.Rpc
{
public class RpcHttpContent : HttpContent
{
private string result; public RpcHttpContent(string result)
{
this.result = result;
} public RpcHttpContent(object result)
{
this.result = Newtonsoft.Json.JsonConvert.SerializeObject(result);
} protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context)
{
var writer = new StreamWriter(stream);
await writer.WriteAsync(result);
await writer.FlushAsync();
} protected override bool TryComputeLength(out long length)
{
length = result.Length;
return true;
}
}
}

4、rpc中间件逻辑处理

有了前面的准备信息,现在基本可以完成逻辑代码的开发了,详细的中间件代码如下。

using Czar.Gateway.Errors;
using Czar.Rpc.Clients;
using Ocelot.Logging;
using Ocelot.Middleware;
using Ocelot.Responses;
using System.Collections.Generic;
using System.Net;
using System.Threading.Tasks; namespace Czar.Gateway.Rpc.Middleware
{
public class CzarRpcMiddleware : OcelotMiddleware
{
private readonly OcelotRequestDelegate _next;
private readonly IRpcClientFactory _clientFactory;
private readonly ICzarRpcProcessor _czarRpcProcessor;
public CzarRpcMiddleware(OcelotRequestDelegate next, IRpcClientFactory clientFactory,
IOcelotLoggerFactory loggerFactory, ICzarRpcProcessor czarRpcProcessor) : base(loggerFactory.CreateLogger<CzarRpcMiddleware>())
{
_next = next;
_clientFactory = clientFactory;
_czarRpcProcessor = czarRpcProcessor;
} public async Task Invoke(DownstreamContext context)
{
var httpStatusCode = HttpStatusCode.OK;
var _param = new List<object>();
//1、提取路由参数
var tmpInfo = context.TemplatePlaceholderNameAndValues;
if (tmpInfo != null && tmpInfo.Count > 0)
{
foreach (var tmp in tmpInfo)
{
_param.Add(tmp.Value);
}
}
//2、提取query参数
foreach (var _q in context.HttpContext.Request.Query)
{
_param.Add(_q.Value.ToString());
}
//3、从body里提取内容
if (context.HttpContext.Request.Method.ToUpper() != "GET")
{
context.DownstreamRequest.Scheme = "http";
var requert = context.DownstreamRequest.ToHttpRequestMessage();
if (requert.Content!=null)
{
var json = "{}";
json = await requert.Content.ReadAsStringAsync();
_param.Add(json);
}
}
//从缓存里提取
var req = await _czarRpcProcessor.GetRemoteMethodAsync(context.DownstreamReRoute.UpstreamPathTemplate.OriginalValue);
if (req != null)
{
req.Parameters = _param.ToArray();
var result = await _clientFactory.SendAsync(req, GetEndPoint(context.DownstreamRequest.Host, context.DownstreamRequest.Port));
OkResponse<RpcHttpContent> httpResponse;
if (result.CzarCode == Czar.Rpc.Utilitys.RpcStatusCode.Success)
{
httpResponse = new OkResponse<RpcHttpContent>(new RpcHttpContent(result.CzarResult?.ToString()));
}
else
{
httpResponse = new OkResponse<RpcHttpContent>(new RpcHttpContent(result));
}
context.HttpContext.Response.ContentType = "application/json";
context.DownstreamResponse = new DownstreamResponse(httpResponse.Data, httpStatusCode, httpResponse.Data.Headers, "OK");
}
else
{//输出错误
var error = new InternalServerError($"请求路由 {context.HttpContext.Request.Path}未配置后端转发");
Logger.LogWarning($"{error}");
SetPipelineError(context, error);
}
}
private EndPoint GetEndPoint(string ipaddress, int port)
{
if (IPAddress.TryParse(ipaddress, out IPAddress ip))
{
return new IPEndPoint(ip, port);
}
else
{
return new DnsEndPoint(ipaddress, port);
}
}
}
}

5、启动Rpc客户端配置

目前Rpc的客户端配置我们还没启动,只需要在AddCzarOcelot中添加相关注入即可。

var service = builder.First(x => x.ServiceType == typeof(IConfiguration));
var configuration = (IConfiguration)service.ImplementationInstance;
//Rpc应用
builder.AddSingleton<ICzarRpcProcessor, CzarRpcProcessor>();
builder.AddSingleton<IRpcRepository, SqlServerRpcRepository>();
builder.AddLibuvTcpClient(configuration);

6、配置客户端

最后别忘了配置Rpc客户端信息是否启用证书信息,为了配置信息的内容。

{
"CzarHost": {
"ProxyEndPoint": true,
"IsSsl": "false",
"PfxPath": "cert/datasync.pfx",
"PfxPassword": "bl123456",
"ClientConfig": {
"Demo.Rpc.Hello": {
"Host": "127.0.0.1",
"Port": 7711,
"Timeout": 20
}
}
}
}

现在让网关集成Rpc功能全部配置完毕。

四、网关Rpc功能测试

本次测试我在原有的网关基础上,增加不同类型的Rpc调用,就按照不同维度测试Rpc调用功能,本次测试案例是建立在Czar.Rpc 服务端基础上,正好可以测试。

1、测试路由参数

请求路径/hello/{no}/{name},调用的服务端方法Hello,传入的两个参数分别是no ,name

可以在服务器端添加断点调试,发现确实接收到请求信息,并正常返回,下面是PostMan测试结果。

2、使用Query方式传递参数

请求路径/rpc/query,调用的服务端方法还是Hello,参数分别是no ,name

3、使用Post方式传递Json

请求路径/rpc/body,调用的服务器方法是HelloSendModel

4、混合参数使用

请求的路径/rpc/bodyparm/{name},调用的服务器端方法是HelloSendModelParm

所有的返回结果可自行调试测试,发现都能达到预期结果。

同时此网关还是支持默认的http请求的,这里就不一一测试了。

五、总结

本篇我介绍了什么是Rpc,以及Czar.Rpc的基本使用,然后使用Czar.Rpc框架集成到我们基于Ocelot扩展网关中,并实现了不能方式的Rpc调用,可以在几乎不改变现有流程的情况下很快速的集成进去,这也是Ocelot开发框架的魅力所在。

如果在使用过程中有什么问题或建议,可以在.NET Core项目实战交流群(637326624)中联系作者。

最后本文涉及的所有的源代码可在https://github.com/jinyancao/czar.gateway中下载预览。

【.NET Core项目实战-统一认证平台】第十六章 网关篇-Ocelot集成RPC服务的更多相关文章

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

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

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

    [.NET Core项目实战-统一认证平台]开篇及目录索引 上篇文章我们介绍了如何扩展Ocelot网关,并实现数据库存储,然后测试了网关的路由功能,一切都是那么顺利,但是有一个问题未解决,就是如果网关 ...

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

    [.NET Core项目实战-统一认证平台]开篇及目录索引 上篇文章我介绍了如何在网关上增加自定义客户端授权功能,从设计到编码实现,一步一步详细讲解,相信大家也掌握了自定义中间件的开发技巧了,本篇我们 ...

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

    [.NET Core项目实战-统一认证平台]开篇及目录索引 本篇将介绍如何扩展Ocelot中间件实现自定义网关,并使用2种不同数据库来演示Ocelot配置信息存储和动态更新功能,内容也是从实际设计出发 ...

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

    [.NET Core项目实战-统一认证平台]开篇及目录索引 上篇文章我们介绍了2种网关配置信息更新的方法和扩展Mysql存储,本篇我们将介绍如何使用Redis来实现网关的所有缓存功能,用到的文档及源码 ...

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

    [.NET Core项目实战-统一认证平台]开篇及目录索引 上一篇我介绍了JWT的生成验证及流程内容,相信大家也对JWT非常熟悉了,今天将从一个小众的需求出发,介绍如何强制令牌过期的思路和实现过程. ...

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

    [.NET Core项目实战-统一认证平台]开篇及目录索引 上篇文章介绍了基于Ids4客户端授权的原理及如何实现自定义的客户端授权,并配合网关实现了统一的授权异常返回值和权限配置等相关功能,本篇将介绍 ...

  8. 【.NET Core项目实战-统一认证平台】第十章 授权篇-客户端授权

    [.NET Core项目实战-统一认证平台]开篇及目录索引 上篇文章介绍了如何使用Dapper持久化IdentityServer4(以下简称ids4)的信息,并实现了sqlserver和mysql两种 ...

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

    [.NET Core项目实战-统一认证平台]开篇及目录索引 一.背景 首先说声抱歉,可能是因为假期综合症(其实就是因为懒哈)的原因,已经很长时间没更新博客了,现在也调整的差不多了,准备还是以每周1-2 ...

随机推荐

  1. 初始化git库并配置自动部署

    1.初始化库 git init --bare wap.git 2.配置wap.git/config文件 [core] repositoryformatversion = 0 filemode = tr ...

  2. 动态设置bootstrapswitch状态

    checkbox的html <input type="checkbox" name="mySwitch" id="mySwitch"& ...

  3. mybatis逆向工程的注意事项,以及数据库表

    1.选择性更新,如果有新参数就更换成新参数,如果参数是null就不更新,还是原来的参数 2.mybatis使用逆向工程,数据库建表的字段user_id必须用下滑线隔开,这样生成的对象private L ...

  4. JS DOM与BOM

    DOM知识点 [DOM(文档对象模型)是 HTML 和 XML 的应用程序接口(API).DOM代表着被加载到浏览器窗口里的当前网页:浏览器向我们提供了当前网页的地图(或者说模型),而我们可以通过js ...

  5. oracle11G 用户密码180天修改概要文件过程

    oracle11G 用户密码180天修改概要文件过程 原因 创建用户的时候不指定概要文件的默认的概要文件是default, 而默认的概要文件中的设置如下,注意斜体部分 PROFILE RESOURCE ...

  6. 1.4 The usage of plug-in

    Once upon a time, we once thought naively that Android plug-in was intended to add new features or a ...

  7. Java synchronized和 Lock 的区别与用法

    在分布式开发中,锁是线程控制的重要途径.Java为此也提供了2种锁机制,synchronized和lock.做为Java爱好者,自然少不了对比一下这2种机制,也能从中学到些分布式开发需要注意的地方.  ...

  8. [Swift]LeetCode818. 赛车 | Race Car

    Your car starts at position 0 and speed +1 on an infinite number line.  (Your car can go into negati ...

  9. [Swift]LeetCode1017. 负二进制转换 | Convert to Base -2

    Given a number N, return a string consisting of "0"s and "1"s that represents it ...

  10. 机器学习入门16 - 多类别神经网络 (Multi-Class Neural Networks)

    原文链接:https://developers.google.com/machine-learning/crash-course/multi-class-neural-networks/ 多类别分类, ...