Ocelot(四)- 認證與授權
作者:markjiang7m2
原文地址:https://www.cnblogs.com/markjiang7m2/p/10932805.html
原始碼地址:https://gitee.com/Sevenm2/OcelotDemo
本文是我關於Ocelot系列文章的第四篇,認證與授權。在前面的系列文章中,我們的下游服務介面都是公開的,沒有經過任何的認證,只要知道介面的呼叫方法,任何人都可以隨意呼叫,因此,很容易就造成資訊洩露或者服務被攻擊。
正如,我要找Willing幹活之前,我得先到HR部門那裡登記並且拿到屬於我自己的工卡,然後我帶著我的工卡去找Willing,亮出我是公司員工的身份,並且有權利要求他幫我完成一個任務。
在這裡整合一套 .net core的服務認證框架IdentityServer4,以及如何在Ocelot中接入IdentityServer4的認證與授權。
跟上一篇Ocelot(三)- 服務發現文章中的Consul類似,這一個是關於Ocelot的系列文章,我暫時也不打算詳細展開說明IdentityServer4,在本文中也是使用IdentityServer4最簡單的Client認證模式。
關於更多的Ocelot功能介紹,可以檢視我的系列文章
本文中涉及案例的完整程式碼都可以從我的程式碼倉庫進行下載。
IdentityServer4使用
IdentityServer4有多種認證模式,包括使用者密碼、客戶端等等,我這裡只需要實現IdentityServer4的驗證過程即可,因此,我選擇了使用最簡單的客戶端模式。
首先我們來看,當沒有Ocelot閘道器時系統是如何使用IdentityServer4進行認證的。
客戶端需要先想IdentityServer請求認證,獲得一個Token,然後再帶著這個Token向下遊服務發出請求。
我嘗試根據流程圖搭建出這樣的認證服務。
建立IdentityServer服務端
新建一個空的Asp.Net Core Web API專案,因為這個專案只做IdentityServer服務端,因此,我將Controller也直接刪除掉。
使用NuGet新增IdentityServer4,可以直接使用NuGet包管理器搜尋IdentityServer4
進行安裝,或者通過VS中內建的PowerShell執行下面的命令列
Install-Package IdentityServer4
在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
- Implicit
- 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工具完成。
請求路徑:<host>
+/connect/token
如果認證正確,會得到如下結果:
如果認證失敗,則會返回如下:
這樣,最簡單的IdentityServer服務就配置完成了。當然,我剛剛為了快速驗證IdentityServer服務是否搭建成功,所以使用的是Debug模式,接下來要使用的話,還是要通過IIS部署使用的,我這裡就把IdentityServer服務部署到8005
埠。
下游服務加入認證
在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認證,那就直接在類名前新增該屬性。
using Microsoft.AspNetCore.Authorization;
// 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中我們在Authorization
中加入這個Token,但是在我們實際呼叫中該怎麼加入Token?
其實熟悉Postman的朋友可能就知道怎麼一回事,Postman為了我們在使用過程中更加方便填入Token資訊而單獨列出了Authorization
,實際上,最終還是會轉換加入到請求頭當中
這個請求頭的Key就是Authorization
,對應的值是Bearer
+ (空格)
+ Token
。
以上就是沒有Ocelot閘道器時,IdentityServer的認證流程。
案例五 Ocelot整合IdentityServer服務
在上面的例子中,我是直接將下游服務暴露給客戶端呼叫,當接入Ocelot閘道器時,我們要達到內外互隔的特性,於是就把IdentityServer服務也託管到Ocelot閘道器中,這樣我們就能統一認證和服務請求時的入口。
於是,我們可以形成下面這個流程圖:
根據流程圖,我在Ocelot ReRoutes
中新增兩組路由
{
"DownstreamPathTemplate": "/connect/token",
"DownstreamScheme": "http",
"DownstreamHostAndPorts": [
{
"Host": "localhost",
"Port": 8005
}
],
"UpstreamPathTemplate": "/token",
"UpstreamHttpMethod": [ "Post" ],
"Priority": 2
},
{
"DownstreamPathTemplate": "/api/ocelot/identityWilling",
"DownstreamScheme": "http",
"DownstreamHostAndPorts": [
{
"Host": "localhost",
"Port": 8001
}
],
"UpstreamPathTemplate": "/ocelot/identityWilling",
"UpstreamHttpMethod": [ "Get" ],
"Priority": 2
}
第一組是將IdentityServer服務進行託管,這樣客戶端就可以直接通過Ocelot閘道器訪問/token
就可以進行認證,第二組是將下游服務進行託管
然後,也是按照之前例子的步驟,先通過http://localhost:4727/token
認證,然後將得到的Token
以Bearer
的方式加入到向下遊服務的請求當中
結果也是跟我預想的是一致的,可以按照這樣的流程進行身份認證。
但是!!!但是!!!但是!!!
當外面隨便來一個人,跟前臺說他要找我做一件事情,然後前臺直接告訴他我的具體位置,就讓他進公司找我了,然後當我接待他的時候,我才發現這個人根本就是來搞事的,拒絕他的請求。如果一天來這麼幾十號人,我還要不要正常幹活了?
這明顯就不符合實際應用場景,外面的人(客戶端)在前臺(Ocelot)的時候,就需要進行身份認證(IdentityServer),只有通過認證的人才能進公司(路由),我才會接觸到這個人(響應),這才叫專人做專事。
於是,認證流程改為下圖:
準備下遊服務
為了保證我的案例與上面這個認證流程是一致的,我就把前面在下游服務中的認證配置去掉。而且在實際生產環境中,客戶端與下游服務的網路是隔斷的,客戶端只能通過閘道器的轉發才能向下遊服務發出請求。
OcelotDownAPI專案
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]
屬性去除。
然後將OcelotDownAPI
專案重新打包,部署在8001
、8002
埠,作為兩個獨立的下游服務。
配置IdentityServer
回到IdentityServer
專案的appsettings.json
,在ApiResources
中另外新增兩個服務
{
"Name": "identityAPIService8001",
"DisplayName": "identityAPIService8001Name"
},
{
"Name": "identityAPIService8002",
"DisplayName": "identityAPIService8002Name"
}
在Clients
中新增兩個Client
{
"ClientId": "markfull",
"ClientSecrets": [ "markjiang7m2" ],
"AllowedGrantTypes": "ClientCredentials",
"AllowedScopes": [ "identityAPIService8001", "identityAPIService8002" ]
},
{
"ClientId": "marklimit",
"ClientSecrets": [ "123456" ],
"AllowedGrantTypes": "ClientCredentials",
"AllowedScopes": [ "identityAPIService8001" ]
}
這裡我為了能讓大家看出允許訪問範圍的效果,特意分配了兩個不同的AllowedScopes
。
使用markfull
登入的客戶端可以同時請求identityAPIService8001
和identityAPIService8002
兩個下游服務,而使用marklimit
登入的客戶端只允許請求identityAPIService8001
服務。
Ocelot整合IdentityServer認證
跟前面的例子一樣,要支援IdentityServer認證,OcelotDemo專案就需要安裝IdentityServer4.AccessTokenValidation
包。
OcelotDemo
專案的appsettings.json
新增IdentityServer資訊
"IdentityServerConfig": {
"IP": "localhost",
"Port": 8005,
"IdentityScheme": "Bearer",
"Resources": [
{
"Key": "APIService8001",
"Name": "identityAPIService8001"
},
{
"Key": "APIService8002",
"Name": "identityAPIService8002"
}
]
}
當然這個配置項的結構是任意的,我這裡的Resources
陣列配置的就是Ocelot閘道器支援哪些服務的認證,Name
就是服務的名稱,同時會唯一對應一個Key
。
為了能更加方便讀取IdentityServerConfig
的資訊,我定義了一個跟它同結構的類
public class IdentityServerConfig
{
public string IP { get; set; }
public string Port { get; set; }
public string IdentityScheme { get; set; }
public List<APIResource> Resources { get; set; }
}
public class APIResource
{
public string Key { get; set; }
public string Name { get; set; }
}
然後來到Startup.cs
的ConfigureServices
方法,就能很快地將IdentityServer
資訊進行註冊
var identityBuilder = services.AddAuthentication();
IdentityServerConfig identityServerConfig = new IdentityServerConfig();
Configuration.Bind("IdentityServerConfig", identityServerConfig);
if (identityServerConfig != null && identityServerConfig.Resources != null)
{
foreach (var resource in identityServerConfig.Resources)
{
identityBuilder.AddIdentityServerAuthentication(resource.Key, options =>
{
options.Authority = $"http://{identityServerConfig.IP}:{identityServerConfig.Port}";
options.RequireHttpsMetadata = false;
options.ApiName = resource.Name;
options.SupportedTokens = SupportedTokens.Both;
});
}
}
Configure
方法中新增
app.UseAuthentication();
最後,就是配置Ocelot.json
檔案。
在ReRoutes
中新增兩組路由
{
"DownstreamPathTemplate": "/api/ocelot/identityWilling",
"DownstreamScheme": "http",
"DownstreamHostAndPorts": [
{
"Host": "localhost",
"Port": 8001
}
],
"UpstreamPathTemplate": "/ocelot/8001/identityWilling",
"UpstreamHttpMethod": [ "Get" ],
"Priority": 2,
"AuthenticationOptions": {
"AuthenticationProviderKey": "APIService8001",
"AllowedScopes": []
}
},
{
"DownstreamPathTemplate": "/api/ocelot/identityWilling",
"DownstreamScheme": "http",
"DownstreamHostAndPorts": [
{
"Host": "localhost",
"Port": 8002
}
],
"UpstreamPathTemplate": "/ocelot/8002/identityWilling",
"UpstreamHttpMethod": [ "Get" ],
"Priority": 2,
"AuthenticationOptions": {
"AuthenticationProviderKey": "APIService8002",
"AllowedScopes": []
}
}
跟其他普通路由相比,這兩組路由都多了一個AuthenticationOptions
屬性,它裡面的AuthenticationProviderKey
就是我們在前面ConfigureServices
方法中登記過的Key
。
我們來捋順一下這個路由跟認證授權過程。以markfull的ID和這裡的第一組路由為例。
- 客戶端拿著
markfull
的clientID向IdentityServer(http://localhost:4727/token
)進行認證,得到了一個的Token
- 客戶端帶著這個Token,因此有了
markfull
的身份,請求Url地址http://localhost:4727/ocelot/8001/identityWilling
- Ocelot閘道器接收到請求,根據路由表找到了認證支援關鍵字為
APIService8001
,從而得到了對應的IdentityServer服務資訊:IdentityServer服務地址為http://localhost:8005
,下游服務名稱為identityAPIService8001
- Ocelot帶著Token向IdentityServer服務(
http://localhost:8005
)進行配對,即檢視markfull
身份的訪問範圍是否包含了identityAPIService8001
服務
- Ocelot認證過
markfull
是允許訪問的,將請求轉發到下游服務中,根據路由配置,下游服務地址為http://localhost:8001/api/ocelot/identityWilling
下面我將Ocelot執行起來,並通過Postman進行驗證。
markfull身份認證
使用markfull
ClientId向IdentityServer進行認證
向8001請求
將得到的Token加入到請求中,請求Url地址http://localhost:4727/ocelot/8001/identityWilling
,得到下游服務返回的響應結果
向8002請求
將得到的Token加入到請求中,請求Url地址http://localhost:4727/ocelot/8002/identityWilling
,得到下游服務返回的響應結果
然後,更換marklimit
身份再驗證一遍
marklimit身份認證
使用marklimit
ClientId向IdentityServer進行認證
向8001請求
將得到的Token加入到請求中,請求Url地址http://localhost:4727/ocelot/8001/identityWilling
,得到下游服務返回的響應結果
向8002請求
將得到的Token加入到請求中,請求Url地址http://localhost:4727/ocelot/8002/identityWilling
,此時,我們得到了401
的狀態碼,即未授權。
總結
在這篇文章中就跟大家介紹了基於IdentityServer4為認證伺服器的Ocelot認證與授權,主要是通過一些案例的實踐,讓大家理解Ocelot對客戶端身份的驗證過程,使用了IdentityServer中最簡單的客戶端認證模式,因為這種模式下IdentityServer的認證沒有複雜的層級關係。但通常在我們實際開發時,更多的可能是通過使用者密碼等方式進行身份認證的,之後我會盡快給大家分享關於IdentityServer如何使用其它模式進行認證。今天就先跟大家介紹到這裡,希望大家能持續關注我們。