第十二节:Ocelot集成IDS4认证授权-微服务主体架构完成
一. 前言
1.业务背景
我们前面尝试了在业务服务器上加IDS4校验,实际上是不合理的, 在生产环境中,业务服务器会有很多个,如果把校验加在每个业务服务器上,代码冗余且不易维护(很多情况下业务服务器不直接对外开放),所以我们通常把校验加在Ocelot网关上,也就是说校验通过了,Ocelot网关才会把请求转发给相应的业务服务器上.(我们这里通常是网关和业务服务器在一个内网中,业务服务器不开放外网)
(和前面:Jwt配合中间件校验流程上是一样的,只不过这里的认证和授权都基于IDS4来做)
PS:关于IDS4服务器,可以配置在网关后面,通过网关转发;
也可以不经网关转发,单独存在, 这里要说明的是,如果经过网关转发,那么对于IDS4而言,只是单纯的转发,不走Ocelot上的校验,其实也很简单,就是不配置AuthenticationProviderKey即可.
2.用到的项目
(1).Case2下的GateWay_Server :网关服务器
(2).Case2下的ID4_Server:认证+授权服务器
(3).GoodsService + OrderService:二者都是资源服务器
(4).PostMan:充当客户端(即第三方应用)
(5).MyClient2:用控制台充当客户端(即第三方应用)
(6).Consul:网关Ocelot已经集成Consul服务发现了,而且资源服务器也已经注册到Consul中了.
二. 核心剖析和测试
1.搭建步骤
(一).启动资源服务器
(1).启动Consul:【consul.exe agent -dev】
(2).启动资源服务器:【dotnet GoodsService.dll --urls="http://*:7001" --ip="127.0.0.1" --port=7001 】
【dotnet OrderService.dll --urls="http://*:7004" --ip="127.0.0.1" --port=7004 】
代码分享:
[Route("api/[controller]/[action]")] [ApiController] public class CatalogController : ControllerBase { [HttpGet] public string GetGoodById1(int id) { var myData = new { status = "ok", goods = new Goods() { id = id, goodName = "apple", goodPrice = 6000, addTime = DateTime.Now } }; var jsonData = JsonHelp.ToJsonString(myData); return jsonData; //返回前端的数据不能直接点出来 } } [Route("api/[controller]/[action]")] [ApiController] public class BuyController : ControllerBase { [HttpPost] public string pOrder1() { return "ok"; } }
(二). GateWay_Server网关的基本配置
(1).Nuget安装包【Ocelot 16.0.1】【Ocelot.Provider.Consul 16.0.1】, 并在ConfigureService和Config中进行配置 services.AddOcelot().AddConsul(); 和 app.UseOcelot().Wait();
(2).Nuget安装包【IdentityServer4.AccessTokenValidation 3.0.1】,用于身份校验.
(3).编写配置文件(OcelotConfig.json),属性改为始终复制,在Program中进行加载;
在配置文件,给GoodsService和OrderService下的节点, 添加 "AuthenticationProviderKey": "OrderServiceKey"/"GoodsServiceKey", 和ConfigureService中的注册进行对应,表示该转发需要走校验.
(把IDS4获取Token的地址也配置进来,但不做校验,也可以不配置进来)
代码分享
//模式三:将Ocelot与consul结合处理,在consul中已经注册业务服务器地址,在Ocelot端不需要再注册了(推荐用法) { "Routes": [ { "DownstreamPathTemplate": "/api/{url}", "DownstreamScheme": "http", "ServiceName": "GoodsService", //Consul中的服务名称 "LoadBalancerOptions": { "Type": "RoundRobin" //轮询算法:依次调用在consul中注册的服务器 }, "UseServiceDiscovery": true, //启用服务发现(可以省略,因为会默认赋值) "UpstreamPathTemplate": "/GoodsService/{url}", "UpstreamHttpMethod": [ "Get", "Post" ], "AuthenticationOptions": { "AuthenticationProviderKey": "GoodsServiceKey", "AllowedScopes": [] } }, { "DownstreamPathTemplate": "/api/{url}", "DownstreamScheme": "http", "ServiceName": "OrderService", "LoadBalancerOptions": { "Type": "LeastConnection" //最小连接数算法 }, "UseServiceDiscovery": true, "UpstreamPathTemplate": "/OrderService/{url}", "UpstreamHttpMethod": [ "Get", "Post" ], "AuthenticationOptions": { "AuthenticationProviderKey": "OrderServiceKey", "AllowedScopes": [] } }, //把ID4_Server认证授权服务器也配置进来,但它不再Ocelot层次上加密,单纯的进行转发 { //转发下游(业务服务器)的匹配规则 "DownstreamPathTemplate": "/{url}", //下游请求类型 "DownstreamScheme": "http", //下游的ip和端口,和上面的DownstreamPathTemplate匹配起来 "DownstreamHostAndPorts": [ { "Host": "127.0.0.1", "Port": 7051 } ], //上游(即Ocelot)接收规则 "UpstreamPathTemplate": "/auth/{url}", //上游接收请求类型 "UpstreamHttpMethod": [ "Get", "Post" ] } ], //下面是配置Consul的地址和端口 "GlobalConfiguration": { //对应Consul的ip和Port(可以省略,因为会默认赋值) "ServiceDiscoveryProvider": { "Host": "127.0.0.1", "Port": 8500 } } }
(4). 在ConfigureServices注册ID4校验,详细参数见代码说明 特别注意:ApiName必须对应Id4中配置的
代码分享:
public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { //1.注册Ocelot services.AddOcelot().AddConsul(); //2.注册ID4校验 services.AddAuthentication("Bearer") .AddIdentityServerAuthentication("GoodsServiceKey", option => //这里GoodsServiceKey与Ocelot配置文件中的AuthenticationProviderKey对应,从而进行绑定验证 { option.Authority = "http://127.0.0.1:7051"; //这里配置是127.0.0.1,那么通过ID4服务器获取token的时候,就必须写127.0.0.1,不能写localhost. option.ApiName = "GoodsService"; //必须对应ID4服务器中GetApiResources配置的apiName,此处不能随便写!! option.RequireHttpsMetadata = false; }) .AddIdentityServerAuthentication("OrderServiceKey", option => { option.Authority = "http://127.0.0.1:7051"; option.ApiName = "OrderService"; option.RequireHttpsMetadata = false; }); services.AddControllers(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseRouting(); //启用Ocelot app.UseOcelot().Wait(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); } }
(5).配置IP+端口的命令行启动 【dotnet GateWay_Server.dll --urls="http://*:7050" --ip="127.0.0.1" --port=7050 】
(三). ID4_Server的基本配置
(1). Nuget安装包【IdentityServer4 4.0.2】
(2). 在ConfigureServie注册客户端模式 或 用户名密码模式,根据需要开启或注释哪个, Config中启用IDS4
配置文件-客户端模式
/// <summary> /// 客户端模式 /// </summary> public class Config1 { /// <summary> /// 配置Api范围集合 /// 4.x版本新增的配置 /// </summary> /// <returns></returns> public static IEnumerable<ApiScope> GetApiScopes() { return new List<ApiScope> { new ApiScope("GoodsService"), new ApiScope("OrderService") }; } /// <summary> /// 需要保护的Api资源 /// 4.x版本新增后续Scopes的配置 /// </summary> /// <returns></returns> public static IEnumerable<ApiResource> GetApiResources() { List<ApiResource> resources = new List<ApiResource>(); //ApiResource第一个参数是ServiceName,第二个参数是描述 resources.Add(new ApiResource("GoodsService", "GoodsService服务需要保护哦") { Scopes = { "GoodsService" } }); resources.Add(new ApiResource("OrderService", "OrderService服务需要保护哦") { Scopes = { "OrderService" } }); return resources; } /// <summary> /// 可以使用ID4 Server 客户端资源 /// </summary> /// <returns></returns> public static IEnumerable<Client> GetClients() { List<Client> clients = new List<Client>() { new Client { ClientId = "client1",//客户端ID AllowedGrantTypes = GrantTypes.ClientCredentials, //验证类型:客户端验证 ClientSecrets ={ new Secret("0001".Sha256())}, //密钥和加密方式 AllowedScopes = { "GoodsService", "OrderService" } //允许访问的api服务 }, new Client { ClientId = "client2",//客户端ID AllowedGrantTypes = GrantTypes.ClientCredentials, //验证类型:客户端验证 ClientSecrets ={ new Secret("0002".Sha256())}, //密钥和加密方式 AllowedScopes = { "GoodsService"} //允许访问的api服务 }, new Client { ClientId = "client3",//客户端ID AllowedGrantTypes = GrantTypes.ClientCredentials, //验证类型:客户端验证 ClientSecrets ={ new Secret("0003".Sha256())}, //密钥和加密方式 AllowedScopes = {"OrderService" } //允许访问的api服务 } }; return clients; } }
配置文件-用户名密码模式
/// <summary> /// 用户名密码模式 /// </summary> public class Config2 { /// <summary> /// 配置Api范围集合 /// 4.x版本新增的配置 /// </summary> /// <returns></returns> public static IEnumerable<ApiScope> GetApiScopes() { return new List<ApiScope> { new ApiScope("GoodsService"), new ApiScope("OrderService") }; } /// <summary> /// 需要保护的Api资源 /// 4.x版本新增后续Scopes的配置 /// </summary> /// <returns></returns> public static IEnumerable<ApiResource> GetApiResources() { List<ApiResource> resources = new List<ApiResource>(); //ApiResource第一个参数是ServiceName,第二个参数是描述 resources.Add(new ApiResource("GoodsService", "GoodsService服务需要保护哦") { Scopes = { "GoodsService" } }); resources.Add(new ApiResource("OrderService", "OrderService服务需要保护哦") { Scopes = { "OrderService" } }); return resources; } /// <summary> /// 可以使用ID4 Server 客户端资源 /// </summary> /// <returns></returns> public static IEnumerable<Client> GetClients() { List<Client> clients = new List<Client>() { new Client { ClientId = "client1",//客户端ID AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, //验证类型:客户端验证 ClientSecrets ={ new Secret("0001".Sha256())}, //密钥和加密方式 AllowedScopes = { "GoodsService", "OrderService" } //允许访问的api服务 }, new Client { ClientId = "client2",//客户端ID AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, //验证类型:客户端验证 ClientSecrets ={ new Secret("0002".Sha256())}, //密钥和加密方式 AllowedScopes = { "GoodsService" } //允许访问的api服务 }, new Client { ClientId = "client3",//客户端ID AllowedGrantTypes = GrantTypes.ResourceOwnerPasswordAndClientCredentials, //验证类型:用户名密码模式 和 客户端模式 ClientSecrets ={ new Secret("0003".Sha256())}, //密钥和加密方式 AllowedScopes = {"OrderService" } //允许访问的api服务 } }; return clients; } /// <summary> /// 定义可以使用ID4的用户资源 /// </summary> /// <returns></returns> public static IEnumerable<TestUser> GetUsers() { return new List<TestUser>() { new TestUser { SubjectId = "10001", Username = "ypf1", //账号 Password = "ypf001" //密码 }, new TestUser { SubjectId = "10002", Username = "ypf2", Password = "ypf002" }, new TestUser { SubjectId = "10003", Username = "ypf3", Password = "ypf003" } }; } }
Startup类
public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { //1. 客户端模式 //services.AddIdentityServer() // .AddDeveloperSigningCredential() //生成Token签名需要的公钥和私钥,存储在bin下tempkey.rsa(生产场景要用真实证书,此处改为AddSigningCredential) // .AddInMemoryApiResources(Config1.GetApiResources()) //存储需要保护api资源 // .AddInMemoryApiScopes(Config1.GetApiScopes()) //配置api范围 4.x版本必须配置的 // .AddInMemoryClients(Config1.GetClients()); //存储客户端模式(即哪些客户端可以用) //2. 用户名密码模式 services.AddIdentityServer() .AddDeveloperSigningCredential() //生成Token签名需要的公钥和私钥,存储在bin下tempkey.rsa(生产场景要用真实证书,此处改为AddSigningCredential) .AddInMemoryApiResources(Config2.GetApiResources()) //存储需要保护api资源 .AddInMemoryClients(Config2.GetClients()) //存储客户端模式(即哪些客户端可以用) .AddInMemoryApiScopes(Config1.GetApiScopes()) //配置api范围 4.x版本必须配置的 .AddTestUsers(Config2.GetUsers().ToList()); //存储哪些用户、密码可以访问 (用户名密码模式) services.AddControllers(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseRouting(); //1.启用IdentityServe4 app.UseIdentityServer(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); } }
(3). 配置IP+端口的命令行启动 【dotnet ID4_Server.dll --urls="http://*:7051" --ip="127.0.0.1" --port=7051 】
2. 用PostMan测试
场景1:用PostMan进行下面测试
测试Get请求:http://127.0.0.1:7050/GoodsService/Catalog/GetGoodById1?id=123 测试结果:401未授权
测试Post请求:http://127.0.0.1:7050/OrderService/Buy/pOrder1 测试结果:401未授权
测试结果:
场景2:用PostMan进行下面测试
先请求:http://127.0.0.1:7051/connect/token 表单参数如下,获取token值
client_id=client1
grant_type=client_credentials
client_secret=0001
然后再Header中要配置 token=Bear xxxxxxxx(上面获取的token值),
(也可用PostMan中Authorization选项卡下,TYPE选择Bearer Token,然后内容直接输入上面获取的token即可)
测试Get请求:http://127.0.0.1:7050/GoodsService/Catalog/GetGoodById1?id=123 测试结果:测试通过,获得返回值
测试Post请求:http://127.0.0.1:7050/OrderService/Buy/pOrder1 测试结果:测试通过,获得返回值
PS: 上述场景2的测试,是直接请求的IDS4服务器,并没有通过Ocelot转发哦,当然也可以请求 http://127.0.0.1:7050/auth/connect/token"来获取(本质上是通过Ocelot转发到了 http://127.0.0.1:7051/connect/token)
测试结果:
3.用控制台客户端测试
公用代码
//认证服务器地址 string rzAddress = "http://127.0.0.1:7051"; //通过Ocelot转发到认证服务器地址 string ocelotRzAddress = "http://127.0.0.1:7050/auth"; //资源服务器1api地址 string resAddress1 = "http://127.0.0.1:7050/GoodsService/Catalog/GetGoodById1?id=123"; //资源服务器2api地址 string resAddress2 = "http://127.0.0.1:7050/OrderService/Buy/pOrder1 ";
(1).客户端模式(直接请求IDS4服务器)
代码分享
var client = new HttpClient(); var disco = await client.GetDiscoveryDocumentAsync(rzAddress); if (disco.IsError) { Console.WriteLine(disco.Error); return; } //向认证服务器发送请求,要求获得令牌 Console.WriteLine("---------------------------- 一.向认证服务器发送请求,要求获得令牌-----------------------------------"); var tokenResponse = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest { //在上面的地址上拼接:/connect/token,最终:http://xxxx/connect/token Address = disco.TokenEndpoint, ClientId = "client1", ClientSecret = "0001", //空格分隔的请求范围列表,省略的话是默认配置的所有api资源,如: client1对应的是:{ "GoodsService", "OrderService", "ProductService" } //这里填写的范围可以和配置的相同或者比配置的少,比如{ "GoodsService OrderService"},这里只是一个范围列表,并不是请求哪个api资源必须要 写在里面 //但是如果配置的和默认配置出现不同,则认证不能通过 比如{ "ProductService OrderService111"}, //综上所述:此处可以不必配置 //Scope = "ProductService OrderService111" }); if (tokenResponse.IsError) { Console.WriteLine($"认证错误:{tokenResponse.Error}"); Console.ReadKey(); } Console.WriteLine(tokenResponse.Json); //携带token向资源服务器发送请求 Console.WriteLine("----------------------------二.携带token向资源服务器发送请求-----------------------------------"); var apiClient = new HttpClient(); apiClient.SetBearerToken(tokenResponse.AccessToken); //设置Token格式 【Bear xxxxxx】 //2.1 向资源服务器1发送请求 var response = await apiClient.GetAsync(resAddress1); if (!response.IsSuccessStatusCode) { Console.WriteLine(response.StatusCode); Console.ReadKey(); } else { var content = await response.Content.ReadAsStringAsync(); Console.WriteLine($"请求资源服务器1的结果为:{content}"); } //2.2 向资源服务器2发送请求 var sContent = new StringContent("", Encoding.UTF8, "application/x-www-form-urlencoded"); var response2 = await apiClient.PostAsync(resAddress2, sContent); if (!response2.IsSuccessStatusCode) { Console.WriteLine(response2.StatusCode); Console.ReadKey(); } else { var content = await response2.Content.ReadAsStringAsync(); Console.WriteLine($"请求资源服务器2的结果为:{content}"); }
运行结果
(2).客户端模式(通过Ocelot转发到IDS4服务器)
代码分享
var client = new HttpClient(); //向认证服务器发送请求,要求获得令牌 Console.WriteLine("---------------------------- 一.向认证服务器发送请求,要求获得令牌-----------------------------------"); var tokenResponse = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest { Address = ocelotRzAddress + "/connect/token", ClientId = "client1", ClientSecret = "0001", //空格分隔的请求范围列表,省略的话是默认配置的所有api资源,如: client1对应的是:{ "GoodsService", "OrderService", "ProductService" } //这里填写的范围可以和配置的相同或者比配置的少,比如{ "GoodsService OrderService"},这里只是一个范围列表,并不是请求哪个api资源必须要 写在里面 //但是如果配置的和默认配置出现不同,则认证不能通过 比如{ "ProductService OrderService111"}, //综上所述:此处可以不必配置 //Scope = "ProductService OrderService111" }); if (tokenResponse.IsError) { Console.WriteLine($"认证错误:{tokenResponse.Error}"); Console.ReadKey(); } Console.WriteLine(tokenResponse.Json); //携带token向资源服务器发送请求 Console.WriteLine("----------------------------二.携带token向资源服务器发送请求-----------------------------------"); var apiClient = new HttpClient(); apiClient.SetBearerToken(tokenResponse.AccessToken); //设置Token格式 【Bear xxxxxx】 //2.1 向资源服务器1发送请求 var response = await apiClient.GetAsync(resAddress1); if (!response.IsSuccessStatusCode) { Console.WriteLine(response.StatusCode); Console.ReadKey(); } else { var content = await response.Content.ReadAsStringAsync(); Console.WriteLine($"请求资源服务器1的结果为:{content}"); } //2.2 向资源服务器2发送请求 var sContent = new StringContent("", Encoding.UTF8, "application/x-www-form-urlencoded"); var response2 = await apiClient.PostAsync(resAddress2, sContent); if (!response2.IsSuccessStatusCode) { Console.WriteLine(response2.StatusCode); Console.ReadKey(); } else { var content = await response2.Content.ReadAsStringAsync(); Console.WriteLine($"请求资源服务器2的结果为:{content}"); }
运行结果
(3).用户名密码模式(直接请求IDS服务器)
代码分享
var client = new HttpClient(); var disco = await client.GetDiscoveryDocumentAsync(rzAddress); if (disco.IsError) { Console.WriteLine(disco.Error); Console.ReadKey(); } //向认证服务器发送请求,要求获得令牌 Console.WriteLine("---------------------------- 一.向认证服务器发送请求,要求获得令牌-----------------------------------"); var tokenResponse = await client.RequestPasswordTokenAsync(new PasswordTokenRequest { Address = disco.TokenEndpoint, ClientId = "client1", ClientSecret = "0001", UserName = "ypf2", Password = "ypf002" //Scope = "" //可以不用配置 }); if (tokenResponse.IsError) { Console.WriteLine($"认证错误:{tokenResponse.Error}"); Console.ReadKey(); } Console.WriteLine(tokenResponse.Json); //携带token向资源服务器发送请求 Console.WriteLine("----------------------------二.携带token向资源服务器发送请求-----------------------------------"); var apiClient = new HttpClient(); apiClient.SetBearerToken(tokenResponse.AccessToken); //设置Token格式 【Bear xxxxxx】 //2.1 向资源服务器1发送请求 var response = await apiClient.GetAsync(resAddress1); if (!response.IsSuccessStatusCode) { Console.WriteLine(response.StatusCode); Console.ReadKey(); } else { var content = await response.Content.ReadAsStringAsync(); Console.WriteLine($"请求资源服务器1的结果为:{content}"); } //2.2 向资源服务器2发送请求 var sContent = new StringContent("", Encoding.UTF8, "application/x-www-form-urlencoded"); var response2 = await apiClient.PostAsync(resAddress2, sContent); if (!response2.IsSuccessStatusCode) { Console.WriteLine(response2.StatusCode); Console.ReadKey(); } else { var content = await response2.Content.ReadAsStringAsync(); Console.WriteLine($"请求资源服务器2的结果为:{content}"); }
运行结果
!
- 作 者 : Yaopengfei(姚鹏飞)
- 博客地址 : http://www.cnblogs.com/yaopengfei/
- 声 明1 : 如有错误,欢迎讨论,请勿谩骂^_^。
- 声 明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。