作者:Zhang_Xiang
原文地址:.NET Core + Ocelot + IdentityServer4 + Consul 基础架构实现
先决条件
- 关于 Ocelot
- 针对使用 .NET 开发微服务架构或者面向服务架构提供一个统一访问系统的组件。 参考
- 本文将使用 Ocelot 构建统一入口的 Gateway。
- 关于 IdentityServer4
- IdentityServer4 是一个 OpenID Connect 和 OAuth 2.0 框架用于 ASP.NET Core 。IdentityServer4 在你的应用程序中集成了基于令牌认证、单点登录、API访问控制所需的所有协议和扩展点。参考
- 本文将使用 IdentityServer4 搭建独立认证服务器。
- 关于 Consul
- Consul 是一个服务网格解决方案,通过服务发现、配置、功能分割提供一个全功能的控制层。这些功能可以单独使用,也可以同时使用以形成一个完整的网格服务。参考
- 本文将使用 Consul 注册多个服务。
- 关于 .Net Core
- 将使用 WebApi 构建多个服务
构建 IdentityServer 服务
1.添加 ASP.Net Core Web 项目
2.添加空项目
3.在程序包管理控制台中输入:Install-Package IdentityServer4.AspNetIdentity
4.添加 Config.cs 文件,并添加内容如下:
1 using IdentityServer4.Models; 2 using IdentityServer4.Test; 3 using System; 4 using System.Collections.Generic; 5 using System.Linq; 6 using System.Threading.Tasks; 7 8 namespace IdentityServer 9 { 10 public sealed class Config 11 { 12 public static IEnumerable<ApiResource> GetApiResources() 13 { 14 return new List<ApiResource> 15 { 16 new ApiResource("ServiceA", "ServiceA API"), 17 new ApiResource("ServiceB", "ServiceB API") 18 }; 19 } 20 21 public static IEnumerable<Client> GetClients() 22 { 23 return new List<Client> 24 { 25 new Client 26 { 27 ClientId = "ServiceAClient", 28 AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, 29 ClientSecrets = 30 { 31 new Secret("ServiceAClient".Sha256()) 32 }, 33 AllowedScopes = new List<string> {"ServiceA"}, 34 AccessTokenLifetime = 60 * 60 * 1 35 }, 36 new Client 37 { 38 ClientId = "ServiceBClient", 39 AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, 40 ClientSecrets = 41 { 42 new Secret("ServiceBClient".Sha256()) 43 }, 44 AllowedScopes = new List<string> {"ServiceB"}, 45 AccessTokenLifetime = 60 * 60 * 1 46 } 47 }; 48 } 49 50 public static List<TestUser> GetUsers() 51 { 52 return new List<TestUser> 53 { 54 new TestUser 55 { 56 Username = "test", 57 Password = "123456", 58 SubjectId = "1" 59 } 60 }; 61 } 62 63 public static IEnumerable<IdentityResource> GetIdentityResources() 64 { 65 return new List<IdentityResource>(); 66 } 67 } 68 }
注意:这里添加了两个 Client ,分别为 ServiceA、ServiceB ,因此接下来将构建这两个服务。
5.修改Startup文件
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Threading.Tasks; 5 using Microsoft.AspNetCore.Builder; 6 using Microsoft.AspNetCore.Hosting; 7 using Microsoft.AspNetCore.Http; 8 using Microsoft.Extensions.DependencyInjection; 9 10 namespace IdentityServer 11 { 12 public class Startup 13 { 14 // This method gets called by the runtime. Use this method to add services to the container. 15 // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 16 public void ConfigureServices(IServiceCollection services) 17 { 18 services.AddMvc().SetCompatibilityVersion(Microsoft.AspNetCore.Mvc.CompatibilityVersion.Version_2_2); 19 services.AddIdentityServer() 20 .AddDeveloperSigningCredential() 21 .AddInMemoryIdentityResources(Config.GetIdentityResources()) 22 .AddInMemoryApiResources(Config.GetApiResources()) 23 .AddInMemoryClients(Config.GetClients()) 24 .AddTestUsers(Config.GetUsers()); 25 } 26 27 // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 28 public void Configure(IApplicationBuilder app, IHostingEnvironment env) 29 { 30 if (env.IsDevelopment()) 31 { 32 app.UseDeveloperExceptionPage(); 33 } 34 35 app.UseIdentityServer(); 36 37 app.Run(async (context) => 38 { 39 await context.Response.WriteAsync("Hello World!"); 40 }); 41 } 42 } 43 }
注意:AddDeveloperSigningCredential() 方法用于添加开发时使用的 Key material ,生产环境中不要使用该方法。在 .NET Core 2.2 中新建的 Web 项目文件 csproj 中包含了如下内容:
<PropertyGroup> <TargetFramework>netcoreapp2.2</TargetFramework> <AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel> </PropertyGroup>
这里更改
<AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
为或直接删除该行,这么做的原因是当值为 InProcess 时,读写 tempkey.rsa 将产生权限问题。关于 AspNetCoreHostingModel 可参考 ASP.NET Core Module 。
<AspNetCoreHostingModel>OutOfProcess</AspNetCoreHostingModel>
6.F5 启动该服务,显示如下:
在浏览器中输入 http://localhost:port/.well-known/openid-configuration ,得到以下内容
至此,一个包含两个服务认证的认证服务搭建完毕。
构建 ServiceA、ServiceB
1.添加 ASP.Net Core Web 项目,这里以 ServiceA 为例进行构建
2.添加 ASP.Net Core API
3.在程序包管理控制台中运行
Install-Package IdentityModel
4.在 StartUp.cs 中添加内容如下:
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Threading.Tasks; 5 using Microsoft.AspNetCore.Builder; 6 using Microsoft.AspNetCore.Hosting; 7 using Microsoft.AspNetCore.Mvc; 8 using Microsoft.Extensions.Configuration; 9 using Microsoft.Extensions.DependencyInjection; 10 using Microsoft.Extensions.Logging; 11 using Microsoft.Extensions.Options; 12 13 namespace ServiceA 14 { 15 public class Startup 16 { 17 public Startup(IConfiguration configuration) 18 { 19 Configuration = configuration; 20 } 21 22 public IConfiguration Configuration { get; } 23 24 // This method gets called by the runtime. Use this method to add services to the container. 25 public void ConfigureServices(IServiceCollection services) 26 { 27 services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2); 28 services.AddAuthentication("Bearer") 29 .AddJwtBearer("Bearer", options => 30 { 31 options.Authority = "http://localhost:3518"; 32 options.RequireHttpsMetadata = false; 33 options.Audience = "ServiceA"; 34 }); 35 } 36 37 // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 38 public void Configure(IApplicationBuilder app, IHostingEnvironment env) 39 { 40 if (env.IsDevelopment()) 41 { 42 app.UseDeveloperExceptionPage(); 43 } 44 app.UseAuthentication(); 45 app.UseMvc(); 46 } 47 } 48 }
5.添加 SessionController 用于用户登录,内容如下:
1 using System; 2 using System.Collections.Generic; 3 using System.ComponentModel.DataAnnotations; 4 using System.Linq; 5 using System.Net.Http; 6 using System.Threading.Tasks; 7 using IdentityModel.Client; 8 using Microsoft.AspNetCore.Http; 9 using Microsoft.AspNetCore.Mvc; 10 11 namespace ServiceA.Controllers 12 { 13 [Route("api/[controller]")] 14 [ApiController] 15 public class SessionController : ControllerBase 16 { 17 public async Task<string> Login(UserRequestModel userRequestModel) 18 { 19 var client = new HttpClient(); 20 DiscoveryResponse disco = await client.GetDiscoveryDocumentAsync("http://localhost:3518"); 21 if (disco.IsError) 22 { 23 return "认证服务未启动"; 24 } 25 TokenResponse tokenResponse = await client.RequestPasswordTokenAsync(new PasswordTokenRequest 26 { 27 Address = disco.TokenEndpoint, 28 ClientId = "ServiceAClient", 29 ClientSecret = "ServiceAClient", 30 UserName = userRequestModel.Name, 31 Password = userRequestModel.Password 32 }); 33 return tokenResponse.IsError ? tokenResponse.Error : tokenResponse.AccessToken; 34 } 35 } 36 public class UserRequestModel 37 { 38 [Required(ErrorMessage = "用户名称不可以为空")] 39 public string Name { get; set; } 40 41 [Required(ErrorMessage = "用户密码不可以为空")] 42 public string Password { get; set; } 43 } 44 }
使用clientFactory.CreateClient()参考官方文档:https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/http-requests?view=aspnetcore-2.2
6.添加 HealthController 用于 Consul 进行服务健康检查,内容如下:
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Threading.Tasks; 5 using Microsoft.AspNetCore.Http; 6 using Microsoft.AspNetCore.Mvc; 7 8 namespace ServiceA.Controllers 9 { 10 [Route("api/[controller]")] 11 [ApiController] 12 public class HealthController : ControllerBase 13 { 14 /// <summary> 15 /// 健康检查 16 /// </summary> 17 /// <returns></returns> 18 public IActionResult Get() 19 { 20 return Ok(); 21 } 22 } 23 }
7.更改 ValuesController.cs 内容如下:
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Threading.Tasks; 5 using Microsoft.AspNetCore.Authorization; 6 using Microsoft.AspNetCore.Mvc; 7 8 namespace ServiceA.Controllers 9 { 10 [Authorize] //添加 Authorize Attribute 以使该控制器启用认证 11 [Route("api/[controller]")] 12 [ApiController] 13 public class ValuesController : ControllerBase 14 { 15 [HttpGet] 16 public ActionResult<IEnumerable<string>> Get() 17 { 18 return new string[] { "value1", "value2" }; 19 } 20 } 21 }
注意,以上基本完成了 ServiceA 的服务构建,但在实际应用中应做一些修改,例如:IdentityServer 地址应在 appsettings.json 中进行配置,不应把地址分散于项目中各处;认证服务启用最好在全局启用,以防止漏写等等。ServiceB 的内容与 ServiceA 大致相似,因此文章中将不再展示 ServiceB 的构建过程。
Gateway 构建
1.添加ASP.Net Web
2.添加空项目
3.打开程序包管理器控制台输入命令:
csharp install-package Ocelot //添加 Ocelot csharp install-package Ocelot.Provider.Consul // 添加 Consul 服务发现
4.添加 ocelot.json 文件,内容如下
1 { 2 "ReRoutes": [ 3 { 4 "DownstreamPathTemplate": "/api/{everything}", 5 "DownstreamScheme": "http", 6 "UpstreamPathTemplate": "/ServiceA/{everything}", 7 "UpstreamHttpMethod": [ "GET", "POST", "DELETE", "PUT" ], 8 "ServiceName": "ServiceA", //consul 服务中 ServiceA 的名称 9 "LoadBalancerOptions": { 10 "Type": "LeastConnection" 11 } 12 }, 13 { 14 "DownstreamPathTemplate": "/api/{everything}", 15 "DownstreamScheme": "http", 16 "UpstreamPathTemplate": "/ServiceB/{everything}", 17 "UpstreamHttpMethod": [ "GET", "POST", "DELETE", "PUT" ], 18 "ServiceName": "ServiceB", //consul 服务中 ServiceB 的名称 19 "LoadBalancerOptions": { 20 "Type": "LeastConnection" 21 } 22 } 23 ], 24 "GlobalConfiguration": { 25 "ServiceDiscoveryProvider": { // Consul 服务发现配置 26 "Host": "localhost", // Consul 地址 27 "Port": 8500, 28 "Type": "Consul" 29 } 30 } 31 }
5.删除 StartUp.cs 文件,在 Program.cs 文件中添加如下内容
1 using System; 2 using System.Collections.Generic; 3 using System.IO; 4 using System.Linq; 5 using System.Threading.Tasks; 6 using Microsoft.AspNetCore; 7 using Microsoft.AspNetCore.Hosting; 8 using Microsoft.Extensions.Configuration; 9 using Microsoft.Extensions.Logging; 10 using Ocelot.DependencyInjection; 11 using Ocelot.Middleware; 12 using Ocelot.Provider.Consul; 13 14 namespace ApiGateway 15 { 16 public class Program 17 { 18 public static void Main(string[] args) 19 { 20 CreateWebHostBuilder(args).Build().Run(); 21 } 22 23 public static IWebHostBuilder CreateWebHostBuilder(string[] args) => 24 WebHost.CreateDefaultBuilder(args) 25 .ConfigureAppConfiguration((context, builder) => 26 { 27 builder.SetBasePath(context.HostingEnvironment.ContentRootPath); 28 builder.AddJsonFile("appsettings.json", true, true); 29 builder.AddJsonFile($"appsettings.{context.HostingEnvironment.EnvironmentName}.json", true, true); 30 builder.AddJsonFile("ocelot.json"); 31 builder.AddEnvironmentVariables(); 32 }) 33 .ConfigureServices(services => 34 { 35 services.AddOcelot().AddConsul(); 36 }) 37 .ConfigureLogging((hostingContext, logging) => 38 { 39 //add your logging 40 }) 41 .Configure(app => 42 { 43 app.UseOcelot().Wait(); 44 }); 45 } 46 }
注意:打开 ApiGateway.csproj 文件,更改
<PropertyGroup> <TargetFramework>netcoreapp2.2</TargetFramework> <AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel> </PropertyGroup>
为
<PropertyGroup> <TargetFramework>netcoreapp2.2</TargetFramework> <AspNetCoreHostingModel>OutOfProcess</AspNetCoreHostingModel> </PropertyGroup>
至此,一个基础网关基本构建完成。
构建 Consul 服务
-
使用 Chocoletey 安装 Consul,
choco install consul
***我是从官网直接下载的。
2.新建conf文件夹以保存 Consul 服务配置
3.在conf文件夹中添加配置文件,内容如下:
1 { 2 "services": [{ 3 "ID": "ServiceA", 4 "Name": "ServiceA", 5 "Tags": [ 6 "ServiceAWebApi", "Api" 7 ], 8 "Address": "127.0.0.1", 9 "Port": 8010, 10 "Check": { 11 "HTTP": "http://127.0.0.1:8010/Api/health", 12 "Interval": "10s" 13 } 14 }, { 15 "id": "ServiceB", 16 "name": "ServiceB", 17 "tags": [ 18 "ServiceBWebApi","Api" 19 ], 20 "Address": "127.0.0.1", 21 "Port": 8011, 22 "Check": [{ 23 "HTTP": "http://127.0.0.1:8011/Api/health", 24 "Interval": "10s" 25 } 26 ] 27 } 28 ] 29 }
4.启动 consul 服务
consul agent -dev -config-dir=./conf
启动后在浏览器中输入 http://localhost:8500/ui/ 以查看Consul服务
Postman 验证
-
F5 启动 Gateway 项目,启动 Postman 发送请求到 ServiceA 获取 Token。
2.使用 Token 请求 ServiceA Values 接口
3.当尝试使用 ServiceA 获取到的 Token 去获取 ServiceB 的数据时,请求也如意料之中返回 401
总结
至此,一个由 .NET Core、IdentityServer4、Ocelot、Consul实现的基础架构搭建完毕。源码地址