NetCore Ocelot
Ocelot是一个用.NET Core实现并且开源的API网关,功能包括:路由,请求聚合,服务验证,鉴权,限流熔断,并内置了负载均衡器与Service Fabric,Buttefly Tracing集成。这些功能都只需要简单配置就可以完成。
简单的ocelot是一堆asp.net core middleware组成的一个管道,当它拿到请求之后会用一个request builder来构造一个HttpRequestMessage发到下游的真实服务器,等下游的服务返回response之后再由一个middleware将它返回的HttpResponseMessage映射到HttpResponse上.
网关集群
只有一个网关是很危险的,也就是我们通常所将的单点,只要它挂了,所有的服务全挂。这显然无法达到高可用,所以我们也可以部署多台网关。当然这个时候再多台网关前,你还需要一台负载均衡器。
Consul 服务发现
在Ocelot已经支持简单的负载均衡功能,也就是当下游服务存在多个结点的时候,Ocelot能够承担起负载均衡的作用。但是它不提供健康检查,服务的注册也只能通过手动子配置文件里面添加完成。这个时候我们就可以用Consul来做服务发现,它能与Ocelot完美结合
下面实战
ocelot配置信息, 这里ocelot.json是系统生成的,code中实现了多配置文件合并功能
1、ocelot.consul.json
{
"Routes": [
{
"DownstreamPathTemplate": "/api/{url}",
"DownstreamScheme": "https",
"DownstreamHttpMethod": "Get",
"UpstreamHttpMethod": [ "Options", "Get", "Post", "Put", "Delete" ],
"UpstreamPathTemplate": "/Ocelot/{url}",
//"UpstreamHost": "localhost",//404 Not found
"UseServiceDiscovery": true,
"ServiceName": "serviceA",
/*
LeastConnection
RoundRobin
NoLoadBalance
*/
"LoadBalancerOptions": {
"Type": "CustomRandomLoadBalancer"
},
"QoSOptions": {
"ExceptionsAllowedBeforeBreaking": 3,
"DurationOfBreak": 10000, //ms
"TimeoutValue": 10000 //ms,default 90s
},
"RateLimitOptions": {
"ClientWhiteList": [ "NoLimitedAPI" ],
"EnableRateLimiting": true,
"Period": "10s", //1s, 5m, 1h, 1d
"PeriodTimespan": 10, //retry after n second/seconds
"Limit": 3
},
"authenticationoptions": {
"authenticationproviderkey": "authenticationkey",
"allowedscopes": []
},
//"routeclaimsrequirement": {
// "userrole": "admin"
//},
//"DownstreamHttpVersion": "",
//"AddHeadersToRequest": {},
//"AddClaimsToRequest": {},
//"AddQueriesToRequest": {},
//"RequestIdKey": "",
//"FileCacheOptions": {
// "TtlSeconds": 10,
// "Region": "gatewaycacheregion"
//},
//"HttpHandlerOptions": {
// "AllowAutoRedirect": true,
// "UseCookieContainer": false,
// "UseTracing": true,
// "MaxConnectionsPerServer": 100
//},
//"DangerousAcceptAnyServerCertificateValidator": false,
"Priority": 10 //priority
}
]
}
1 { 2 "GlobalConfiguration": { 3 "BaseUrl": "https://localhost:7000", //gateway public address 4 "RequestIdKey": "OcRequestId", 5 "RouteIsCaseSensitive": false, 6 "ServiceDiscoveryProvider": { 7 "Host": "127.0.0.1", 8 "Port": 8500, 9 "Type": "Consul", 10 "ConfigurationKey": "ApiGateway.Ocelot" 11 }, 12 "RateLimitOptions": { 13 "ClientIdHeader": "IgnoreRateLimit", //request header key 14 "QuotaExceededMessage": "The requests has reached the quota.", //The customized prompt 15 "RateLimitCounterPrefix": "ocelotratelimit", 16 "DisableRateLimitHeaders": false, 17 "HttpStatusCode": 666 18 } 19 } 20 }
{ "Routes": [ { "DownstreamPathTemplate": "/api/{url}", "UpstreamPathTemplate": "/Ocelot/{url}", "UpstreamHttpMethod": [ "Options", "Get", "Post", "Put", "Delete" ], "DownstreamHttpMethod": "Get", "AddHeadersToRequest": {}, "UpstreamHeaderTransform": {}, "DownstreamHeaderTransform": {}, "AddClaimsToRequest": {}, "RouteClaimsRequirement": { "userrole": "admin" }, "AddQueriesToRequest": {}, "ChangeDownstreamPathTemplate": {}, "RequestIdKey": null, "FileCacheOptions": { "TtlSeconds": 0, "Region": null }, "RouteIsCaseSensitive": false, "ServiceName": "serviceA", "ServiceNamespace": null, "DownstreamScheme": "https", "QoSOptions": { "ExceptionsAllowedBeforeBreaking": 3, "DurationOfBreak": 10000, "TimeoutValue": 10000 }, "LoadBalancerOptions": { "Type": "CustomRandomLoadBalancer", "Key": null, "Expiry": 0 }, "RateLimitOptions": { "ClientWhitelist": [ "NoLimitedAPI" ], "EnableRateLimiting": true, "Period": "10s", "PeriodTimespan": 10.0, "Limit": 3 }, "AuthenticationOptions": { "AuthenticationProviderKey": "authenticationkey", "AllowedScopes": [] }, "HttpHandlerOptions": { "AllowAutoRedirect": false, "UseCookieContainer": false, "UseTracing": false, "UseProxy": true, "MaxConnectionsPerServer": 2147483647 }, "DownstreamHostAndPorts": [], "UpstreamHost": null, "Key": null, "DelegatingHandlers": [], "Priority": 10, "Timeout": 0, "DangerousAcceptAnyServerCertificateValidator": false, "SecurityOptions": { "IPAllowedList": [], "IPBlockedList": [] }, "DownstreamHttpVersion": null }, { "DownstreamPathTemplate": "/api/service1", "UpstreamPathTemplate": "/Ocelot/service1", "UpstreamHttpMethod": [ "Options", "Get", "Post", "Put", "Delete" ], "DownstreamHttpMethod": "Get", "AddHeadersToRequest": {}, "UpstreamHeaderTransform": {}, "DownstreamHeaderTransform": {}, "AddClaimsToRequest": {}, "RouteClaimsRequirement": {}, "AddQueriesToRequest": {}, "ChangeDownstreamPathTemplate": {}, "RequestIdKey": null, "FileCacheOptions": { "TtlSeconds": 0, "Region": null }, "RouteIsCaseSensitive": false, "ServiceName": "serviceA", "ServiceNamespace": null, "DownstreamScheme": "https", "QoSOptions": { "ExceptionsAllowedBeforeBreaking": 0, "DurationOfBreak": 0, "TimeoutValue": 0 }, "LoadBalancerOptions": { "Type": "CustomRandomLoadBalancer", "Key": null, "Expiry": 0 }, "RateLimitOptions": { "ClientWhitelist": [], "EnableRateLimiting": false, "Period": null, "PeriodTimespan": 0.0, "Limit": 0 }, "AuthenticationOptions": { "AuthenticationProviderKey": "authenticationkey", "AllowedScopes": [] }, "HttpHandlerOptions": { "AllowAutoRedirect": false, "UseCookieContainer": false, "UseTracing": false, "UseProxy": true, "MaxConnectionsPerServer": 2147483647 }, "DownstreamHostAndPorts": [], "UpstreamHost": null, "Key": "service1", "DelegatingHandlers": [], "Priority": 11, "Timeout": 0, "DangerousAcceptAnyServerCertificateValidator": false, "SecurityOptions": { "IPAllowedList": [], "IPBlockedList": [] }, "DownstreamHttpVersion": null }, { "DownstreamPathTemplate": "/api/service2", "UpstreamPathTemplate": "/Ocelot/service2", "UpstreamHttpMethod": [ "Options", "Get", "Post", "Put", "Delete" ], "DownstreamHttpMethod": "Get", "AddHeadersToRequest": {}, "UpstreamHeaderTransform": {}, "DownstreamHeaderTransform": {}, "AddClaimsToRequest": {}, "RouteClaimsRequirement": {}, "AddQueriesToRequest": {}, "ChangeDownstreamPathTemplate": {}, "RequestIdKey": null, "FileCacheOptions": { "TtlSeconds": 0, "Region": null }, "RouteIsCaseSensitive": false, "ServiceName": "serviceA", "ServiceNamespace": null, "DownstreamScheme": "https", "QoSOptions": { "ExceptionsAllowedBeforeBreaking": 0, "DurationOfBreak": 0, "TimeoutValue": 0 }, "LoadBalancerOptions": { "Type": "CustomRandomLoadBalancer", "Key": null, "Expiry": 0 }, "RateLimitOptions": { "ClientWhitelist": [], "EnableRateLimiting": false, "Period": null, "PeriodTimespan": 0.0, "Limit": 0 }, "AuthenticationOptions": { "AuthenticationProviderKey": "authenticationkey", "AllowedScopes": [] }, "HttpHandlerOptions": { "AllowAutoRedirect": false, "UseCookieContainer": false, "UseTracing": false, "UseProxy": true, "MaxConnectionsPerServer": 2147483647 }, "DownstreamHostAndPorts": [], "UpstreamHost": null, "Key": "service2", "DelegatingHandlers": [], "Priority": 12, "Timeout": 0, "DangerousAcceptAnyServerCertificateValidator": false, "SecurityOptions": { "IPAllowedList": [], "IPBlockedList": [] }, "DownstreamHttpVersion": null } ], "DynamicRoutes": [], "Aggregates": [ { "RouteKeys": [ "service1", "service2" ], "RouteKeysConfig": null, "UpstreamPathTemplate": "/api/aggregate", "UpstreamHost": null, "RouteIsCaseSensitive": false, "Aggregator": null, "UpstreamHttpMethod": [ "Get" ], "Priority": 1 } ], "GlobalConfiguration": { "RequestIdKey": "OcRequestId", "ServiceDiscoveryProvider": { "Scheme": null, "Host": "127.0.0.1", "Port": 8500, "Type": "Consul", "Token": null, "ConfigurationKey": "ApiGateway.Ocelot", "PollingInterval": 0, "Namespace": null }, "RateLimitOptions": { "ClientIdHeader": "IgnoreRateLimit", "QuotaExceededMessage": "The requests has reached the quota.", "RateLimitCounterPrefix": "ocelotratelimit", "DisableRateLimitHeaders": false, "HttpStatusCode": 666 }, "QoSOptions": { "ExceptionsAllowedBeforeBreaking": 0, "DurationOfBreak": 0, "TimeoutValue": 0 }, "BaseUrl": "https://localhost:7000", "LoadBalancerOptions": { "Type": null, "Key": null, "Expiry": 0 }, "DownstreamScheme": null, "HttpHandlerOptions": { "AllowAutoRedirect": false, "UseCookieContainer": false, "UseTracing": false, "UseProxy": true, "MaxConnectionsPerServer": 2147483647 }, "DownstreamHttpVersion": null }}
2、Add ocelot
1 builder.Services.AddOcelot() 2 .AddConsul() 3 .AddPolly(); 4 5 app.UseOcelot().Wait();
3、添加自定义load balancer
1 builder.Services.AddOcelot() 2 .AddCustomLoadBalancer((serviceProvider, route, serviceDiscoveryProvider) => new CustomRandomLoadBalancer(serviceDiscoveryProvider.Get)) 3 .AddConsul() 4 .AddPolly();
1 public class CustomRandomLoadBalancer : ILoadBalancer 2 { 3 private readonly Func<Task<List<Service>>> _services; 4 private readonly Object _lock = new object(); 5 private int _index; 6 7 public CustomRandomLoadBalancer(Func<Task<List<Service>>> services) 8 { 9 _services = services; 10 } 11 12 public async Task<Response<ServiceHostAndPort>> Lease(HttpContext httpContext) 13 { 14 var services = await _services(); 15 if (services == null) 16 return new ErrorResponse<ServiceHostAndPort>(new ErrorInvokingLoadBalancerCreator(new Exception("Load balance algorithm error."))); 17 lock (_lock) 18 { 19 if (services.Count == 1) 20 return new OkResponse<ServiceHostAndPort>(services[0].HostAndPort); 21 _index = new Random().Next(services.Count); 22 return new OkResponse<ServiceHostAndPort>(services[_index].HostAndPort); 23 } 24 } 25 26 public void Release(ServiceHostAndPort hostAndPort) 27 { 28 } 29 }
4、添加中间件,设置特定header,避免限流
1 app.Use((httpContext, requestDelegate) => 2 { 3 if (!httpContext.Request.Headers.TryGetValue("IgnoreRateLimit", out Microsoft.Extensions.Primitives.StringValues value)) 4 httpContext.Request.Headers.Add("IgnoreRateLimit", "NoLimitedAPI"); 5 return requestDelegate.Invoke(httpContext); 6 });
添加中间件另种方式
app.UseMiddleware<CustomMiddleware>(3);
1 public class CustomMiddleware 2 { 3 private readonly RequestDelegate _requestDelegate; 4 private Int32 _parameter { get; set; } 5 public CustomMiddleware(RequestDelegate requestDelegate, Int32 parameter) 6 { 7 this._requestDelegate = requestDelegate; 8 this._parameter = parameter; 9 } 10 11 public async Task InvokeAsync(HttpContext httpContext) 12 { 13 var argument = _parameter; 14 await _requestDelegate(httpContext); 15 } 16 }
5、集成identity server4
identity server4 config 配置信息
1 public class Config 2 { 3 4 public static IEnumerable<ApiScope> ApiScopes 5 => new List<ApiScope> 6 { 7 new ApiScope("apiscope1", "scope1"), 8 new ApiScope("apiscope2","scope2") 9 }; 10 11 public static IEnumerable<ApiResource> ApiResources 12 => new List<ApiResource> 13 { 14 new ApiResource("service1","this is service1") 15 { 16 Scopes= { "apiscope1" } 17 }, 18 new ApiResource("service2","this is service2") 19 { 20 Scopes ={ "apiscope2" } 21 } 22 }; 23 24 public static IEnumerable<Client> Clients 25 => new List<Client> 26 { 27 new Client 28 { 29 ClientId = "clientId", 30 ClientSecrets = new [] { new Secret("auth123456".Sha256()) }, 31 AllowedGrantTypes = { GrantType.ClientCredentials }, 32 AllowedScopes = new [] 33 { 34 "apiscope1", 35 "apiscope2", 36 IdentityServerConstants.StandardScopes.OpenId, 37 IdentityServerConstants.StandardScopes.Address, 38 IdentityServerConstants.StandardScopes.Email, 39 IdentityServerConstants.StandardScopes.Phone, 40 IdentityServerConstants.StandardScopes.Profile 41 }, 42 Claims = new List<ClientClaim> 43 { 44 new ClientClaim(IdentityModel.JwtClaimTypes.Role,"admin"), 45 new ClientClaim(IdentityModel.JwtClaimTypes.NickName,"tom"), 46 new ClientClaim("Emai","tom@163.com") 47 } 48 } 49 }; 50 51 public static IEnumerable<IdentityResource> IdentityResources 52 => new List<IdentityResource> 53 { 54 new IdentityResources.OpenId(), 55 new IdentityResources.Profile(), 56 new IdentityResources.Address(), 57 new IdentityResources.Email(), 58 new IdentityResources.Phone() 59 }; 60 }
1 builder.Services.AddIdentityServer() 2 .AddDeveloperSigningCredential() 3 .AddInMemoryIdentityResources(Config.IdentityResources) 4 .AddInMemoryApiResources(Config.ApiResources) 5 .AddInMemoryApiScopes(Config.ApiScopes) 6 .AddInMemoryClients(Config.Clients); 7 8 app.UseIdentityServer();
启动Ids4 dotnet run
6、ocelot中配置支持Ids4 authentication
1 var authenticationProviderKey = "authenticationkey"; 2 builder.Services 3 .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) 4 .AddIdentityServerAuthentication(authenticationProviderKey, options => 5 { 6 options.Authority = "https://localhost:9000"; 7 options.RequireHttpsMetadata = false; 8 options.ApiName = "service1"; 9 options.SupportedTokens = SupportedTokens.Both; 10 options.JwtBearerEvents = new JwtBearerEvents 11 { 12 OnChallenge =ctx => 13 { 14 return Task.CompletedTask; 15 }, 16 OnMessageReceived = ctx => 17 { 18 var token = ctx.Token; 19 return Task.CompletedTask; 20 }, 21 OnTokenValidated = ctx => 22 { 23 var securityToken = ctx.SecurityToken as JwtSecurityToken; 24 var claimIdentities = ctx.Principal?.Identities; 25 if (claimIdentities?.Any(i => i.Claims.Any(c=>c.Value.Equals("admin", StringComparison.OrdinalIgnoreCase))) ?? false) 26 ctx.Success(); 27 else 28 ctx.Fail("token validate failed."); 29 return Task.CompletedTask; 30 }, 31 OnAuthenticationFailed = context => 32 { 33 context.Fail(context.Exception); 34 return Task.CompletedTask; 35 }, 36 OnForbidden = context => 37 { 38 context.Fail("403 forbidden"); 39 return Task.CompletedTask; 40 } 41 }; 42 });
app.UseAuthentication();
7、为了避免恶意攻击避开ocelot直接访问service,在netcore api项目中同样需要添加验证
1 builder.Services.AddAuthentication(options => 2 { 3 options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; 4 options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; 5 }).AddIdentityServerAuthentication(options => 6 { 7 options.Authority = "https://localhost:9000"; 8 options.ApiName = "service1"; 9 options.RequireHttpsMetadata = false; 10 options.SupportedTokens = IdentityServer4.AccessTokenValidation.SupportedTokens.Both; 11 options.Events = new JwtBearerEvents 12 { 13 OnTokenValidated = context => 14 { 15 var token = context.SecurityToken as JwtSecurityToken; 16 var identity = context.Principal?.Identity as ClaimsIdentity; 17 if (!string.IsNullOrEmpty(token?.Issuer)) 18 context.Success(); 19 else 20 context.Fail($"Token invalid"); 21 return Task.CompletedTask; 22 }, 23 OnAuthenticationFailed = context => 24 { 25 context.Fail(context.Exception); 26 return Task.CompletedTask; 27 }, 28 OnForbidden = context => 29 { 30 context.Fail("403 forbidden"); 31 return Task.CompletedTask; 32 }, 33 OnChallenge = context => 34 { 35 var error = context.Error; 36 return Task.CompletedTask; 37 }, 38 OnMessageReceived = context => 39 { 40 var token = context.Token; 41 return Task.CompletedTask; 42 } 43 }; 44 });
配置详解
负载均衡
/* LeastConnection RoundRobin NoLoadBalance */ "LoadBalancerOptions": { "Type": "CustomRandomLoadBalancer" }
熔断,超时
"QoSOptions": { "ExceptionsAllowedBeforeBreaking": 3, "DurationOfBreak": 10000, //ms "TimeoutValue": 10000 //ms,default 90s }
限流
"RateLimitOptions": { "ClientWhiteList": [ "NoLimitedAPI" ], "EnableRateLimiting": true, "Period": "10s", //1s, 5m, 1h, 1d "PeriodTimespan": 10, //retry after n second/seconds "Limit": 3 }
认证
"authenticationoptions": { "authenticationproviderkey": "authenticationkey", "allowedscopes": [] }
Consul服务发现
"ServiceDiscoveryProvider": { "Host": "127.0.0.1", "Port": 8500, "Type": "Consul", "ConfigurationKey": "ApiGateway.Ocelot" }
Ocelot缓存
"FileCacheOptions": { "TtlSeconds": 10, "Region": "gatewaycacheregion" }
最终效果,通过访问ocelot 定义url 负载到具体的服务实例
token
public class TokenController : Controller { [HttpGet("accesstoken")] public async Task<IActionResult> Index() { var request = new ClientCredentialsTokenRequest { Address = "https://localhost:9000/connect/token", ClientId = "clientId", ClientSecret = "auth123456", }; var client = new HttpClient(); var disco = await client.GetDiscoveryDocumentAsync("https://localhost:9000"); if (disco.IsError) { return StatusCode(500); } var tokenResponse = await client.RequestClientCredentialsTokenAsync(request); if(tokenResponse.IsError) { return StatusCode(500); } return Ok(tokenResponse.AccessToken); } }
ocelot api
OK, 时间紧迫,有时间详搞。