Consul

简介

Consul是一个基于CP的轻量级分布式高可用的系统,提供服务发现、健康检查、K-V存储、多数据中心等功能,不需要再依赖其他组件(Zk、Eureka、Etcd等)。

服务发现:Consul可以提供一个服务,比如api或者MySQL之类的,其他客户端可以使用Consul发现一个指定的服务提供者,并通过DNS和HTTP应用程序可以很容易的找到所依赖的服务。
健康检查:Consul客户端提供相应的健康检查接口,Consul服务端通过调用健康检查接口检测客户端是否正常
K-V存储:客户端可以使用Consul层级的Key/Value存储,比如动态配置,功能标记,协调,领袖选举等等
多数据中心:Consul支持开箱即用的多数据中心

安装,下载地址如下:
https://www.consul.io/downloads.html

解压安装包在consul.exe目录下打开终端命令,通过consul.exe agent -dev启动
concul,访问8500
在这里插入图片描述

在asp.netcore中的使用

在这里插入图片描述

下载程序包 consul
在这里插入图片描述
新建一个consul帮助类
扩展方法
用于注册服务

//在程序的入口函数中添加如下代码 使其支持命令行参数
public static void Main(string[] args)
        {
            new ConfigurationBuilder().SetBasePath(Directory.GetCurrentDirectory()).AddCommandLine(args).Build(); //支持命令行参数
            CreateHostBuilder(args).Build().Run();
        }
using Consul;
using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace GodFoxVideoWeb.Unity
{
    public static class ConsulRi
    {
        public static void ConsulRegist(this IConfiguration configuration)
        {
            ConsulClient client = new ConsulClient(c =>
            {
                c.Address = new Uri("http://localhost:8500/");
                c.Datacenter = "dc1";
            });

            string ip = configuration["ip"];
            int port = int.Parse(configuration["port"]);//命令行参数必须传入
            //int weight = string.IsNullOrWhiteSpace(configuration["weight"]) ? 1 : int.Parse(configuration["weight"]);

            client.Agent.ServiceRegister(new AgentServiceRegistration()
            {
                ID = "service" + Guid.NewGuid(),//唯一的---科比 奥尼尔
                Name = "Xzp",//组名称-Group   湖人 表示一组服务,一个服务集群并取一个唯一表示组名
                Address = ip,//其实应该写ip地址
                Port = port,//不同实例
                //Tags = new string[] { weight.ToString() },//标签
                //Check = new AgentServiceCheck()
                //{
                //    Interval = TimeSpan.FromSeconds(12),//间隔12s一次
                //    HTTP = $"http://{ip}:{port}/Api/Health/Index",
                //    Timeout = TimeSpan.FromSeconds(5),//检测等待时间
                //    DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(60)//失败后多久移除
                //}
            });
            //命令行参数获取
           // Console.WriteLine($"{ip}:{port}--weight:{weight}");
        }
    }
}

然后在startup中configure调用
即程序启动加载时将服务注册到consul中

//执行且只执行一次的,去注册
   this.Configuration.ConsulRegist();

这里我们通过命令行启动3个服务,注册一组服务到consul中
在这里插入图片描述

启动成功后
在consul就可以看到注册的服务了
在这里插入图片描述
在这里插入图片描述

如何获取到服务地址呢?

首先新建一个webapi项目
在这里插入图片描述
再同样引入 consul程序包

使用

using Consul;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Models.Entity;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;

namespace ceishi.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class HomeController : ControllerBase
    {
        [HttpGet("{id}")]
        public IActionResult Get(int id)
        {
            //string url = "https://localhost:44372/products/933";
            //string data = InvokeApi(url);
            //List<Product> list = Newtonsoft.Json.JsonConvert.DeserializeObject<List<Product>>(data);
            //return Ok(list);
            ConsulClient client = new ConsulClient(c =>
            {
                c.Address = new Uri("http://localhost:8500/");
                c.Datacenter = "dc1";
            });
            var response = client.Agent.Services().Result.Response; //可以获取到所有服务 包括ip port 每个服务的唯一表示 Guid  还有所对应的组名 group
            return Ok(response);
        }
        //public static string InvokeApi(string url)
        //{
        //    using (HttpClient http = new HttpClient())
        //    {
        //        HttpRequestMessage message = new HttpRequestMessage();
        //        message.Method = HttpMethod.Get;
        //        message.RequestUri = new Uri(url);
        //        var result = http.SendAsync(message).Result;
        //        string content = result.Content.ReadAsStringAsync().Result;
        //        return content;
        //    }
        //}
    }
}

返回的结果如下

{
  "service0524539e-7b85-422c-9488-79c3b56189ae": {
    "id": "service0524539e-7b85-422c-9488-79c3b56189ae",
    "service": "Xzp",
    "tags": [],
    "port": 8003,
    "address": "127.0.0.1",
    "enableTagOverride": false,
    "meta": {}
  },
  "service38b5fd2e-acac-48e5-bfa2-4f152d65879c": {
    "id": "service38b5fd2e-acac-48e5-bfa2-4f152d65879c",
    "service": "Xzp",
    "tags": [],
    "port": 8001,
    "address": "127.0.0.1",
    "enableTagOverride": false,
    "meta": {}
  },
  "service7c95128a-de2e-4776-bb83-621996042c28": {
    "id": "service7c95128a-de2e-4776-bb83-621996042c28",
    "service": "Xzp",
    "tags": [],
    "port": 8002,
    "address": "127.0.0.1",
    "enableTagOverride": false,
    "meta": {}
  }
}

也可以通过如下方法获取指定一组的服务实例

using Consul;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Models.Entity;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;

namespace ceishi.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class HomeController : ControllerBase
    {
        [HttpGet("{id}")]
        public IActionResult Get(int id)
        {
            //string url = "https://localhost:44372/products/933";
            //string data = InvokeApi(url);
            //List<Product> list = Newtonsoft.Json.JsonConvert.DeserializeObject<List<Product>>(data);
            //return Ok(list);

            
            ConsulClient client = new ConsulClient(c =>
            {
                c.Address = new Uri("http://localhost:8500/");
                c.Datacenter = "dc1";
            });

            string url = "http://Xzp/api/users/all"; //获取到指定的一组服务 //其实 consul就是进行了  DNS 域名解析
            var response = client.Agent.Services().Result.Response; //可以获取到所有服务 包括ip port 每个服务的唯一表示 Guid  还有所对应的组名 group
            Uri uri = new Uri(url);
            string groupName = uri.Host; //这里表示截取到 中间的域名 Xzp

            Array serviceDictionary = response.Where(s => s.Value.Service.Equals(groupName, StringComparison.OrdinalIgnoreCase)).ToArray();//找出指定的一组 服务实例
            return Ok(serviceDictionary);
        }
        //public static string InvokeApi(string url)
        //{
        //    using (HttpClient http = new HttpClient())
        //    {
        //        HttpRequestMessage message = new HttpRequestMessage();
        //        message.Method = HttpMethod.Get;
        //        message.RequestUri = new Uri(url);
        //        var result = http.SendAsync(message).Result;
        //        string content = result.Content.ReadAsStringAsync().Result;
        //        return content;
        //    }
        //}
    }
}

结果

[
  {
    "key": "service0524539e-7b85-422c-9488-79c3b56189ae",
    "value": {
      "id": "service0524539e-7b85-422c-9488-79c3b56189ae",
      "service": "Xzp",
      "tags": [],
      "port": 8003,
      "address": "127.0.0.1",
      "enableTagOverride": false,
      "meta": {}
    }
  },
  {
    "key": "service38b5fd2e-acac-48e5-bfa2-4f152d65879c",
    "value": {
      "id": "service38b5fd2e-acac-48e5-bfa2-4f152d65879c",
      "service": "Xzp",
      "tags": [],
      "port": 8001,
      "address": "127.0.0.1",
      "enableTagOverride": false,
      "meta": {}
    }
  },
  {
    "key": "service7c95128a-de2e-4776-bb83-621996042c28",
    "value": {
      "id": "service7c95128a-de2e-4776-bb83-621996042c28",
      "service": "Xzp",
      "tags": [],
      "port": 8002,
      "address": "127.0.0.1",
      "enableTagOverride": false,
      "meta": {}
    }
  }
]

健康检查

在注册服务实例上添加属性 Chek

client.Agent.ServiceRegister(new AgentServiceRegistration()
            {
                ID = "service" + Guid.NewGuid(),//唯一的---科比 奥尼尔
                Name = "Xzp",//组名称-Group   湖人
                Address = ip,//其实应该写ip地址
                Port = port,//不同实例
                //Tags = new string[] { weight.ToString() },//标签
                Check = new AgentServiceCheck() //健康检查 代理  
                {
                    Interval = TimeSpan.FromSeconds(12),//间隔12s请求一次
                    HTTP = $"http://{ip}:{port}/products/userId",
                    Timeout = TimeSpan.FromSeconds(5),//检测等待时间
                    DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(60)//失败后多久移除 //最小值60s
                }
            });

表示健康检查开启成功
在这里插入图片描述

当你配置了Chek 后 consul会代理请求api地址如果服务器出现异常则会在你规定的时间内清除该服务注册实例

负载均衡策略

consul 只负责返回全部服务实例,要怎么调用还是由客服端决定的。

 AgentService agentService = null; //new一个空的服务实例
                //{
                //    //平均策略  多个实例,平均分配---随机就是平均
                //    agentService = serviceDictionary[new Random(DateTime.Now.Millisecond + iSeed++).Next(0, serviceDictionary.Length)].Value;  //说白了就是用到了随机函数 随机获取服务实例
                //}
                //{
                //    //轮询策略
                //    agentService = serviceDictionary[iSeed++ % serviceDictionary.Length].Value;
                //}
                {
                    //能不能根据服务器的情况来分配---权重---不同的实例权重不同--配置权重
                    List<KeyValuePair<string, AgentService>> pairsList = new List<KeyValuePair<string, AgentService>>();
                    foreach (var pair in serviceDictionary)
                    {
                        int count = int.Parse(pair.Value.Tags?[0]);
                        for (int i = 0; i < count; i++)
                        {
                            pairsList.Add(pair);
                        }
                    }
                    agentService = pairsList.ToArray()[new Random(iSeed++).Next(0, pairsList.Count())].Value;

                }

private static int iSeed = 0;

在这里插入图片描述

负载均衡–权重

在consul实例中 添加 Tags属性
在命令行启动时 添加一个 --weight=xx 参数

int weight = string.IsNullOrWhiteSpace(configuration["weight"]) ? 1 : int.Parse(configuration["weight"]); //获取到参数
client.Agent.ServiceRegister(new AgentServiceRegistration()
            {
                ID = "service" + Guid.NewGuid(),//唯一的---科比 奥尼尔
                Name = "Xzp",//组名称-Group   湖人
                Address = ip,//其实应该写ip地址
                Port = port,//不同实例
                Tags = new string[] { weight.ToString() },//标签
                Check = new AgentServiceCheck() //健康检查 代理  
                {
                    Interval = TimeSpan.FromSeconds(12),//间隔12s请求一次
                    HTTP = $"http://{ip}:{port}/products/userId",
                    Timeout = TimeSpan.FromSeconds(5),//检测等待时间
                    DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(60)//失败后多久移除 //最小值60s
                }
            });

Consul总结

consul到底有什么用呢?简单来说就是
服务的注册与发现
健康检查
consul并没有实现负载均衡策略,还是在客户端自己实现随机调用的

Gateway 网关

网关的作用是什么呢

(1)网关层对外部和内部进行了隔离,保障了后台服务的安全性。

(2)对外访问控制由网络层面转换成了运维层面,减少变更的流程和错误成本。

(3)减少客户端与服务的耦合,服务可以独立运行,并通过网关层来做映射。

(4)通过网关层聚合,减少外部访问的频次,提升访问效率。

(5)节约后端服务开发成本,减少上线风险。

(6)为服务熔断,灰度发布,线上测试提供简单方案。

(7)便于进行应用层面的扩展。

就是将服务于客户端隔离,避免客户端直接调用,为了客户端提供统一入口 保证服务安全性

在这里插入图片描述

Ocelot

Ocelot是一个基于 .net core的开源WebAPI服务网关项目,它的功能非常强大,包括了路由、请求聚合、服务发现、认证鉴权、限流、负载均衡等功能。而这些功能都可以直接通过修改json配置文件即可使用,非常方便。Ocelot是系统中对外暴露的一个请求入口,所有外部接口都必须通过这个网关才能向下游API发出请求,就如地铁中的安检系统,所有人都必须经过安检才能乘坐地铁。

Ocelot官网:http://threemammals.com/ocelot

具体使用

首先新建一个webapi 项目
在这里插入图片描述
安装程序包
引入Ocelot
然后在startup中使用

//ConfigureServices中

public void ConfigureServices(IServiceCollection services)
        {
            services.AddOcelot()
                .AddConsul()
                .AddPolly();//如何处理
            //services.AddControllers();
        }
//Configure中

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            app.UseOcelot();//去掉默认管道,使用Ocelot管道处理请求


            //if (env.IsDevelopment())
            //{
            //    app.UseDeveloperExceptionPage();
            //}

            //app.UseHttpsRedirection();

            //app.UseRouting();

            //app.UseAuthorization();

            //app.UseEndpoints(endpoints =>
            //{
            //    endpoints.MapControllers();
            //});
        }
    }

新建一个配置文件 configuration.json

{    
    "ReRoutes": [],    
    "GlobalConfiguration": {}
}

在Program 加载配置文件

public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
             .ConfigureAppConfiguration(conf =>
             {
                 conf.AddJsonFile("configuration.json", optional: false, reloadOnChange: true);
             })
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });

Ocelot配置参数详解

我们先来看GlobalConfiguration,它是一个全局配置项,通常我们都要在这个配置项中添加一个属性BaseUrl,BaseUrl就是Ocelot服务对外暴露的Url。

"GlobalConfiguration": {"BaseUrl": "http://localhost:4727"}

ReRoutes是一个数组,其中的每一个元素代表了一个路由,而一个路由所包含的所有可配置参

{    
    "DownstreamPathTemplate": "/",    
    "UpstreamPathTemplate": "/",    
    "UpstreamHttpMethod": 
    [        
        "Get"
    ],    
    "AddHeadersToRequest": {},    
    "AddClaimsToRequest": {},    
    "RouteClaimsRequirement": {},    
    "AddQueriesToRequest": {},    
    "RequestIdKey": "",    
    "FileCacheOptions": 
    {        
        "TtlSeconds": 0,        
        "Region": ""
    },    
    "ReRouteIsCaseSensitive": false,    
    "ServiceName": "",    
    "DownstreamScheme": "http",    
    "DownstreamHostAndPorts": 
    [
        {            
        "Host": "localhost",            
        "Port": 8001,
        }
    ],    
    "QoSOptions": 
    {        
        "ExceptionsAllowedBeforeBreaking": 0,        
        "DurationOfBreak": 0,        
        "TimeoutValue": 0
    },    
    "LoadBalancer": "",    
    "RateLimitOptions": 
    {        
        "ClientWhitelist": [],        
        "EnableRateLimiting": false,        
        "Period": "",        
        "PeriodTimespan": 0,        
        "Limit": 0
    },    
    "AuthenticationOptions": 
    {        
        "AuthenticationProviderKey": "",        
        "AllowedScopes": []
    },    
    "HttpHandlerOptions": 
    {        
        "AllowAutoRedirect": true,        
        "UseCookieContainer": true,        
        "UseTracing": true
    },    
    "UseServiceDiscovery": false
}

Downstream 下游服务配置

UpStream 上游服务配置

Aggregates 服务聚合配置

ServiceName, LoadBalancer, UseServiceDiscovery 服务发现配置

AuthenticationOptions 服务认证配置

RouteClaimsRequirement Claims 鉴权配置

RateLimitOptions 限流配置

FileCacheOptions 缓存配置

QosOptions 服务质量与熔断配置

DownstreamHeaderTransform 头信息转发配置

案例一 路由

路由是Ocelot最基本的功能。Ocelot接收到来自上游服务的请求,经过验证后,将请求转发到下游服务,因此,我们首先要配置路由当中上下游服务参数。

{
  "ReRoutes": [
    {
      "DownstreamPathTemplate": "/{url}", //服务地址--url变量
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 8001 //服务端口
        } 
      ],
      "UpstreamPathTemplate": "/ocelot/{url}", //网关地址--url变量   //冲突的还可以加权重Priority
      "UpstreamHttpMethod": [ "Get", "Post" ]
    }
  ]

当上游服务向地址http://localhost:8010/ocelot/products发出请求时,Ocelot会将请求转发到下游服务http://localhost:8001/products

注意如果你用的Ocelot16.x以上的版本,需要将ReRoutes改成Routes
因为Ocelot 16.x以上版本修改了配置信息

如果希望Ocelot能够转发所有的请求,则可以配置如下:

{    
    "DownstreamPathTemplate": "/{url}",    
    "DownstreamScheme": "http",    
    "DownstreamHostAndPorts": 
    [
      {
        "Host": "localhost",                
        "Port": 8001,
      }
    ],    
    "UpstreamPathTemplate": "/{url}",    
    "UpstreamHttpMethod": ["Get","Post"]
}

如果希望Ocelot只转发来自某个特定上游服务Host的请求,则可以配置如下:

{    
    "DownstreamPathTemplate": "/{url}",    
    "DownstreamScheme": "http",    
    "DownstreamHostAndPorts": [
                   {                
                     "Host": "localhost",                
                     "Port": 8001,
                   }
                  ],    
    "UpstreamPathTemplate": "/{url}",    
    "UpstreamHttpMethod": ["Get"],    
    "UpstreamHost": "localhost:4023"
}
配置多个地址多实例转发
{
  "Routes": [
    {
      "DownstreamPathTemplate": "/{url}", //服务地址--url变量
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 8001 //服务端口
        } //可以多个,自行负载均衡
      ],
      "UpstreamPathTemplate": "/ocelot1/{url}", //网关地址--url变量   //冲突的还可以加权重Priority
      "UpstreamHttpMethod": [ "Get", "Post" ]
    },
    {
      "DownstreamPathTemplate": "/{url}", //服务地址--url变量
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 8002 //服务端口
        }
      ],
      "UpstreamPathTemplate": "/ocelot2/{url}", //网关地址--url变量
      "UpstreamHttpMethod": [ "Get", "Post" ]
    },
    {
      "DownstreamPathTemplate": "/{url}", //服务地址--url变量
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 8003 //服务端口
        }
      ],
      "UpstreamPathTemplate": "/ocelot3/{url}", //网关地址--url变量
      "UpstreamHttpMethod": [ "Get", "Post" ]
    }
  ]
}

可以通过
http://localhost:8010/ocelot1/products
http://localhost:8010/ocelot3/products
http://localhost:8010/ocelot2/products
访问不同的服务实例

单个地址的多个服务转发实现负载均衡
{
  "Routes": [
    {
      "DownstreamPathTemplate": "/{url}", //服务地址--url变量
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 8001 //服务端口
        } //可以多个,自行负载均衡
        ,
        {
          "Host": "localhost",
          "Port": 8002 //服务端口
        },
        {
          "Host": "localhost",
          "Port": 8003 //服务端口
        }
      ],
      "UpstreamPathTemplate": "/ocelot/{url}", //网关地址--url变量   //冲突的还可以加权重Priority
      "UpstreamHttpMethod": [ "Get", "Post" ],
      "LoadBalancerOptions": {
        "Type": "RoundRobin" //轮询      LeastConnection-最少连接数的服务器   NoLoadBalance不负载均衡
      }
    }
  ]
}
请求聚合

Ocelot允许您指定组成多个普通路由的聚合路由,并将其响应映射到一个对象中。通常在这里,您的客户端正在向服务器发出多个请求,而该服务器可能只是一个。此功能使您可以开始使用Ocelot为前端类型的体系结构实现后端。

高级注册您自己的聚合器
{
    "Routes": [
        {
            "DownstreamPathTemplate": "/",
            "UpstreamPathTemplate": "/laura",
            "UpstreamHttpMethod": [
                "Get"
            ],
            "DownstreamScheme": "http",
            "DownstreamHostAndPorts": [
                {
                    "Host": "localhost",
                    "Port": 51881
                }
            ],
            "Key": "Laura"
        },
        {
            "DownstreamPathTemplate": "/",
            "UpstreamPathTemplate": "/tom",
            "UpstreamHttpMethod": [
                "Get"
            ],
            "DownstreamScheme": "http",
            "DownstreamHostAndPorts": [
                {
                    "Host": "localhost",
                    "Port": 51882
                }
            ],
            "Key": "Tom"
        }
    ],
    "Aggregates": [
        {
            "RouteKeys": [
                "Tom",
                "Laura"
            ],
            "UpstreamPathTemplate": "/",
            "Aggregator": "FakeDefinedAggregator"
        }
    ]
}

在这里,我们添加了一个称为FakeDefinedAggregator的聚合器。当Ocelot试图聚合此路由时,它将寻找该聚合器。
为了使聚合器可用,我们必须将FakeDefinedAggregator添加到OcelotBuilder中,如下所示。

services
    .AddOcelot()
    .AddSingletonDefinedAggregator<FakeDefinedAggregator>();

现在,当Ocelot尝试汇总上方的路由时,它将在容器中找到FakeDefinedAggregator并将其用于汇总路由。由于FakeDefinedAggregator已在容器中注册,因此您可以将所需的所有依赖项添加到容器中,如下所示。

services.AddSingleton<FooDependency>();

services
    .AddOcelot()
    .AddSingletonDefinedAggregator<FooAggregator>();

除了此Ocelot,您还可以添加如下所示的瞬时聚合器。

services
    .AddOcelot()
    .AddTransientDefinedAggregator<FakeDefinedAggregator>();

为了制作一个聚合器,您必须实现此接口。

public interface IDefinedAggregator
{
    Task<DownstreamResponse> Aggregate(List<HttpContext> responses);
}

使用此功能,您几乎可以做任何事情,因为HttpContext对象包含所有聚合请求的结果。请注意,如果HttpClient向集合中的Route发出请求时抛出异常,则不会获得HttpContext,但是会获得成功。如果确实抛出异常,则将记录该异常。

服务发现Ocelot+Consul

关于服务发现,我的个人理解是在这个微服务时代,当下游服务太多的时候,我们就需要找一个专门的工具记录这些服务的地址和端口等信息,这样会更加便于对服务的管理,而当上游服务向这个专门记录的工具查询某个服务信息的过程,就是服务发现。

举个例子,以前我要找的人也就只有Willing和Jack,所以我只要自己用本子(数据库)记住他们两个的位置就可以了,那随着公司发展,部门的人越来越多,他们经常会调换位置,还有入职离职的人员,这就导致我本子记录的信息没有更新,所以我找来了HR部门(Consul)帮忙统一管理,所有人有信息更新都要到HR部门那里进行登记(服务注册),然后当我(上游服务)想找人做某件事情(发出请求)的时候,我先到HR那里查询可以帮我完成这个任务的人员在哪里(服务发现),得到这些人员的位置信息,我也就可以选中某一个人帮我完成任务了。

Ocelot+Consul流程示意图:
在这里插入图片描述
Ocelot添加Consul支持
在OcelotDemo项目中安装Consul支持,命令行或者直接使用Nuget搜索安装

Install-Package Ocelot.Provider.Consul

在Startup.cs的ConfigureServices方法中

services.AddOcelot().AddConsul()

配置路由

{
  "Routes": [
    {
      "DownstreamPathTemplate": "/{url}", //服务地址--url变量
      "DownstreamScheme": "http",
      "UseServiceDiscovery": true,
      "UpstreamPathTemplate": "/ocelot/{url}", //网关地址--url变量
      "UpstreamHttpMethod": [ "Get", "Post" ],
      "ServiceName": "Xzp", //一组服务实例名
      "LoadBalancerOptions": {
        "Type": "RoundRobin" //负载均衡 轮询
      }
    }
  ],
  "GlobalConfiguration": {
    "BaseUrl": "http://localhost:8010", //网关对外地址
    "ServiceDiscoveryProvider": {
      "Scheme": "http",
      "Host": "localhost",
      "Port": 8500,
      "Type": "Consul" //由Consul提供服务发现
    }
  }
}

在GlobalConfiguration添加ServiceDiscoveryProvider,指定服务发现支持程序为Consul。

还可以发现这一组路由相对其它路由,少了DownstreamHostAndPorts,多了ServiceName,也就是这一组路由的下游服务,不是由Ocelot直接指定,而是通过Consul查询得到。

服务治理Ocelot+Polly

在这里插入图片描述
缓存
引入Nuget程序包Ocelot.Provider.Polly

然后在Startup.cs中的ConfigureServices方法注册该中间件

 services.AddOcelot()
                .AddConsul()
                .AddPolly();
路由中配置缓存
{
  "Routes": [
    {
      "DownstreamPathTemplate": "/{url}", //服务地址--url变量
      "DownstreamScheme": "http",
      "UpstreamPathTemplate": "/ocelot/{url}", //网关地址--url变量
      "UpstreamHttpMethod": [ "Get", "Post" ],
      "UseServiceDiscovery": true,
      "ServiceName": "Xzp", //consul服务名称
      "LoadBalancerOptions": {
        "Type": "RoundRobin" //轮询      LeastConnection-最少连接数的服务器   NoLoadBalance不负载均衡
      },
      "FileCacheOptions": {
                "TtlSeconds": 10
       } //"缓存"   10秒内若果请求同一个api则在代理中缓存数据,当客户端再次请求时不会继续往下执行,而直接返回缓存中的数据
      }
  ],
  "GlobalConfiguration": {
    "BaseUrl": "http://127.0.0.1:8010", //网关对外地址
    "ServiceDiscoveryProvider": {
      "Host": "localhost",
      "Port": 8500,
      "Type": "Consul" //由Consul提供服务发现
    }
  }
}
Ocelot支持流量限制,只要在路由中添加RateLimitOptions配置即可
"RateLimitOptions": {
        "ClientWhitelist": [], //白名单
        "EnableRateLimiting": true,
        "Period": "1m", //1s, 5m, 1h, 1d
        "PeriodTimespan": 5, //多少秒之后客户端可以重试
        "Limit": 5 //统计时间段内允许的最大请求数量
      }

ClientWhiteList:数组,在请求头中包含ClientId=xxx的请求不受限流控制,其中ClientId这个key可以修改,xxx表示配置的白名单。

EnableRateLimiting:是否启用限流

Period:限流控制的时间周期,输入单位支持s(秒), m(分), h(时), d(天)

PeriodTimespan:恢复等待时间,当访问超过限流限制的次数后,需要等待的时间,单位为s,如当一分钟内访问超过5次,就需要等待5秒后,访问才会重新有效

Limit:一个时间周期内允许访问的最大次数。

当我在请求头中加上[ClientId]=markadmin后,清空Postman的请求记录,重新开始发出请求,无论请求多少次,Ocelot也不会对我的访问进行限流
在这里插入图片描述

流量限制的全局配置RateLimitOptionsGlobalConfiguration同级所以配置在GlobalConfiguration后面即可
 "RateLimitOptions": {
    "DisableRateLimitHeaders": true,
    "QuotaExceededMessage": "Customize Tips!",  // 当请求过载被截断时返回的消息
    "HttpStatusCode": 999, // 当请求过载被截断时返回的http status
    "ClientIdHeader": "Test"
}

DisableRateLimitHeaders:当设为true时,请求头中就不会输出流量限制的相关信息,默认值:“false”

QuotaExceededMessage:当请求数量超出流量限制时,输出的信息,默认值:“API calls quota exceeded! maximum admitted {Limit} per {Period}.”

HttpStatusCode:当请求数量超出流量限制时,输出的状态码,默认值:“429”

ClientIdHeader:标识为白名单中的客户端的请求头key,默认值:“ClientId”

熔断
{
    "DownstreamPathTemplate": "/{url}",
    "DownstreamScheme": "http",
    "DownstreamHostAndPorts": [
    {
        "Host": "localhost",
        "Port": 8001
    }
    ],
    "UpstreamPathTemplate": "/ocelot/{url}",
    "UpstreamHttpMethod": [ "Get" ],
    "Priority": 2,
    "QoSOptions": {
    "ExceptionsAllowedBeforeBreaking": 3, //允许多少个异常请求
    "DurationOfBreak": 3000,  //熔断的时间,单位为ms
    "TimeoutValue": 5000  //如果下游请求的处理时间超过多少则自如将请求设置为超时 默认90秒
    }
}

为了看出熔断效果,我将8001端口的下游服务停止了

当第一次向网关发出请求时,得到500的状态码

连续3次请求过后,就会得到503的状态码,证明Ocelot已经产生熔断

认证与授权

我们的下游服务接口都是公开的,没有经过任何的认证,只要知道接口的调用方法,任何人都可以随意调用,因此,很容易就造成信息泄露或者服务被攻击。

在这里集成一套 .net core的服务认证框架IdentityServer4,以及如何在Ocelot中接入IdentityServer4的认证与授权。

IdentityServer4有多种认证模式,包括用户密码、客户端等等,我这里只需要实现IdentityServer4的验证过程即可,因此,我选择了使用最简单的客户端模式。

首先我们来看,当没有Ocelot网关时系统是如何使用IdentityServer4进行认证的。
在这里插入图片描述
客户端需要先想IdentityServer请求认证,获得一个Token,然后再带着这个Token向下游服务发出请求。

创建IdentityServer服务端
新建一个空的Asp.Net Core Web API项目,因为这个项目只做IdentityServer服务端,因此,我将Controller也直接删除掉。

使用NuGet添加IdentityServer4,可以直接使用NuGet包管理器搜索IdentityServer4进行安装,或者通过VS中内置的PowerShell执行下面的命令行

在appsettings.json中添加IdentityServer4的配置

{
  "Logging": {
    "LogLevel": {
      "Default": "Warning"
    }
  },
  "SSOConfig": {
    "ApiResources": [
      {
        "Name": "identityAPIService",
        "DisplayName": "identityAPIServiceName"
      }
    ],
    "Clients": [
      {
        "ClientId": "mark",
        "ClientSecrets": [ "markjiang7m2" ],
        "AllowedGrantTypes": "ClientCredentials",
        "AllowedScopes": [ "identityAPIService" ]
      }
    ]
  },
  "AllowedHosts": "*"
}

ApiResources为数组类型,表示IdentityServer管理的所有的下游服务列表

Name: 下游服务名称

DisplayName: 下游服务别名

Clients为数组类型,表示IdentityServer管理的所有的上游客户端列表

ClientId: 客户端ID

ClientSecrets: 客户端对应的密钥

AllowedGrantTypes: 该客户端支持的认证模式,目前支持如下:

Implicit

ImplicitAndClientCredentials

Code

CodeAndClientCredentials

Hybrid

HybridAndClientCredentials

ClientCredentials

ResourceOwnerPassword

ResourceOwnerPasswordAndClientCredentials

DeviceFlow

AllowedScopes: 该客户端支持访问的下游服务列表,必须是在ApiResources列表中登记的

新建一个类用于读取IdentityServer4的配置

using IdentityServer4.Models;
using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace IdentityServer
{
    public class SSOConfig
    {
        public static IEnumerable<ApiResource> GetApiResources(IConfigurationSection section)
        {
            List<ApiResource> resource = new List<ApiResource>();

            if (section != null)
            {
                List<ApiConfig> configs = new List<ApiConfig>();
                section.Bind("ApiResources", configs);
                foreach (var config in configs)
                {
                    resource.Add(new ApiResource(config.Name, config.DisplayName));
                }
            }

            return resource.ToArray();
        }
        /// <summary>
        /// 定义受信任的客户端 Client
        /// </summary>
        /// <returns></returns>
        public static IEnumerable<Client> GetClients(IConfigurationSection section)
        {
            List<Client> clients = new List<Client>();

            if (section != null)
            {
                List<ClientConfig> configs = new List<ClientConfig>();
                section.Bind("Clients", configs);
                foreach (var config in configs)
                {
                    Client client = new Client();
                    client.ClientId = config.ClientId;
                    List<Secret> clientSecrets = new List<Secret>();
                    foreach (var secret in config.ClientSecrets)
                    {
                        clientSecrets.Add(new Secret(secret.Sha256()));
                    }
                    client.ClientSecrets = clientSecrets.ToArray();

                    GrantTypes grantTypes = new GrantTypes();
                    var allowedGrantTypes = grantTypes.GetType().GetProperty(config.AllowedGrantTypes);
                    client.AllowedGrantTypes = allowedGrantTypes == null ? 
                        GrantTypes.ClientCredentials : (ICollection<string>)allowedGrantTypes.GetValue(grantTypes, null);

                    client.AllowedScopes = config.AllowedScopes.ToArray();

                    clients.Add(client);
                }
            }
            return clients.ToArray();
        }
    }

    public class ApiConfig
    {
        public string Name { get; set; }
        public string DisplayName { get; set; }
    }

    public class ClientConfig
    {
        public string ClientId { get; set; }
        public List<string> ClientSecrets { get; set; }
        public string AllowedGrantTypes { get; set; }
        public List<string> AllowedScopes { get; set; }
    }
}

在Startup.cs中注入IdentityServer服务

public void ConfigureServices(IServiceCollection services)
{
    var section = Configuration.GetSection("SSOConfig");
    services.AddIdentityServer()
        .AddDeveloperSigningCredential()
        .AddInMemoryApiResources(SSOConfig.GetApiResources(section))
        .AddInMemoryClients(SSOConfig.GetClients(section));

    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}

使用IdentityServer中间件

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseIdentityServer();
    app.UseMvc();
}

配置完成,接下来用Debug模式看看IdentityServer是否可用,尝试向IdentityServer进行认证。因为需要使用post方式,而且在认证请求的body中加入认证信息,所以我这里借助Postman工具完成。

请求路径:+/connect/token

下游服务加入认证

在OcelotDownAPI项目中,使用NuGet添加AccessTokenValidation包,可以直接使用NuGet包管理器搜索IdentityServer4.AccessTokenValidation进行安装,或者通过VS中内置的PowerShell执行下面的命令行

Install-Package IdentityServer4.AccessTokenValidation

在appsettings.json中加入IdentityServer服务信息

"IdentityServerConfig": {
    "ServerIP": "localhost",
    "ServerPort": 8005,
    "IdentityScheme": "Bearer",
    "ResourceName": "identityAPIService"
}

这里的identityAPIService就是在IdentityServer服务端配置ApiResources列表中登记的其中一个下游服务。

在Startup.cs中读取IdentityServer服务信息,加入IdentityServer验证

public void ConfigureServices(IServiceCollection services)
{
    IdentityServerConfig identityServerConfig = new IdentityServerConfig();
    Configuration.Bind("IdentityServerConfig", identityServerConfig);
    services.AddAuthentication(identityServerConfig.IdentityScheme)
        .AddIdentityServerAuthentication(options =>
        {
            options.RequireHttpsMetadata = false;
            options.Authority = $"http://{identityServerConfig.IP}:{identityServerConfig.Port}";
            options.ApiName = identityServerConfig.ResourceName;
        }
        );

    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    app.UseAuthentication();

    app.UseMvc();
}

根据前面的配置,我们添加一个需要授权的下游服务API
注意添加属性[Authorize]
因为我这里只是为了演示IdentityServer的认证流程,所以我只是在其中一个API接口中添加该属性,如果还有其他接口需要整个认证,就需要在其他接口中添加该属性,如果是这个Controller所有的接口都需要IdentityServer认证,那就直接在类名前添加该属性。

// GET api/ocelot/identityWilling
[HttpGet("identityWilling")]
[Authorize]
public async Task<IActionResult> IdentityWilling(int id)
{
    var result = await Task.Run(() =>
    {
        ResponseResult response = new ResponseResult()
        { Comment = $"我是Willing,既然你是我公司员工,那我就帮你干活吧, host: {HttpContext.Request.Host.Value}, path: {HttpContext.Request.Path}" };
        return response;
    });
    return Ok(result);
}

重新打包OcelotDownAPI项目,并发布到8001端口。

首先,像之前那样直接请求API,得到如下结果:
在这里插入图片描述

得到了401的状态码,即未经授权。

因此,我必须先向IdentityServer请求认证并授权
在这里插入图片描述

然后将得到的Token以Bearer的方式加入到向下游服务的请求当中,这样我们就可以得到了正确的结果
在这里插入图片描述

其实熟悉Postman的朋友可能就知道怎么一回事,Postman为了我们在使用过程中更加方便填入Token信息而单独列出了Authorization,实际上,最终还是会转换加入到请求头当中
这个请求头的Key就是Authorization,对应的值是Bearer + (空格) + Token。
在这里插入图片描述

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐