一. 前言
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 : 原創博客請在轉載時保留原文鏈接或在文章開頭加上本人博客地址,否則保留追究法律責任的權利。
