.Net6 微服務之Ocelot+IdentityServer4入門看這篇就夠了

2023-02-27 12:05:20

前言

.Net 6 使用 Consul 實現服務註冊與發現 看這篇就夠了
.Net6 使用 Ocelot + Consul 看這篇就夠了
.Net6 微服務之Polly入門看這篇就夠了


書接上文,本文將繼續建立在 .Net6 使用 Ocelot + Consul 看這篇就夠了文章中的專案基礎上來進行OcelotIdentityServer4的介紹。專案也都比較簡單,不熟悉的同學可以去翻翻我之前的文章,相信都能一看就會。其實網上關於Id4的文章還是比較多了,本文只會對其進行簡單的介紹,不會過多深究,感興趣的同學可以自行研究。然後本文只是個人學習與分享,不喜勿噴,謝謝!

什麼是IdentityServer4?


IdentityServer 是一個 OpenID Connect 提供者—— 它實現了 OpenID Connect 和 OAuth 2.0 協定。

  • User:使用者
  • Client:使用者端
  • Resources:Identity Data(身份資料)、Apis
  • Identity Server:認證授權伺服器
  • Token:Access Token(存取令牌)和 Identity Token(身份令牌)

基於以上我們先來簡單瞭解 OAuth2.0 與 OpenID Connect是個什麼東西,它們能幹些什麼?解決了哪些問題以及我們怎麼使用它們?我們需要帶著一些問題來學習新東西才會事半功倍。

OAuth2

OAuth是一個開放授權標準,是一個授權協定,並不是認證協定,它無法提供完善的身份認證功能,它解決的問題是授權 。本文介紹的版本為2.0。
它允許使用者讓第三方應用存取該使用者在某服務的特定私有資源,但是不提供賬號密碼給第三方使用者。然後它是通過給使用者提供Token(令牌)的方式來存取他們存放在特定服務商上的資料,每一個Token授權一個特定的網站記憶體取特定的資源,而不是一味的放開所有內容
下面是OAuth 2.0的執行流程圖以及相關介紹:

Client:使用者端
Resource Owner資源所有者
Authorization Server認證伺服器,即服務提供商專門用來處理認證的伺服器。
Resource Server資源伺服器,即服務提供商存放使用者生成的資源的伺服器。它與認證伺服器,可以是同一臺伺服器,也可以是不同的伺服器。

(A)使用者開啟使用者端以後,使用者端要求使用者給予授權。
(B)使用者同意給予使用者端授權。
(C)使用者端使用上一步獲得的授權,向認證伺服器申請令牌。
(D)認證伺服器對使用者端進行認證以後,確認無誤,同意發放令牌。
(E)使用者端使用令牌,向資源伺服器申請獲取資源。
(F)資源伺服器確認令牌無誤,同意向用戶端開放資源。

簡單瞭解之後,我們可以發現B步驟是關鍵,即使用者怎麼才能給使用者端授權。只有拿到這個授權之後,使用者端才能獲取到令牌,從而憑藉令牌獲取資源。所以OAuth2.0為此定義了四種授權方式,如下:

  • 授權碼模式(authorization code)
  • 簡化模式(implicit)
  • 密碼模式(resource owner password credentials)
  • 使用者端模式(client credentials)

這裡我們只做簡單瞭解,就不展開了,感興趣的同學可以參考官網或者大佬的文章。

OpenId Connect(OIDC)

OpenId Connect = OIDC = Authentication + Authorization + OAuth2.0
OIDC是一個基於OAuth2協定身份認證標準協定,解決的問題是使用者認證,而不關心授權
我們都知道OAuth2是一個授權協定,它無法提供完善的身份認證功能,所以OIDC乾的活就是使用OAuth2的授權伺服器來為第三方使用者端提供使用者的身份認證,並把對應的身份認證資訊傳遞給使用者端,且可以適用於各種型別的使用者端(比如伺服器端應用,移動APP,JS應用),且完全相容OAuth2,也就是說你搭建了一個OIDC的服務後,也可以當作一個OAuth2的服務來用。

下面是OIDC的執行流程圖以及相關介紹:

  • EU:End User:一個人類使用者。
  • RP:Relying Party ,用來代指OAuth2中的受信任的使用者端,身份認證和授權資訊的消費方;
  • OP:OpenID Provider,有能力提供EU認證的服務(比如OAuth2中的授權服務),用來為RP提供EU的身份認證資訊;
  • ID Token:JWT格式的資料,包含EU身份認證的資訊。
  • UserInfo Endpoint:使用者資訊介面(受OAuth2保護),當RP使用Access Token存取時,返回授權使用者的資訊,此介面必須使用HTTPS。
  • AuthN:Authentication 認證
  • AuthZ:Authorization 授權

  • RP傳送一個認證請求給OP;
  • OP對EU進行身份認證,然後提供授權;
  • OP把ID TokenAccess Token(需要的話)返回給RP;
  • RP使用Access Token傳送一個請求UserInfo EndPoint;
  • UserInfo EndPoint返回EU的Claims。

注意這裡面RP發往OP的請求,是屬於Authentication型別的請求,雖然在OIDC中是複用OAuth2的Authorization請求通道,但是用途是不一樣的,且OIDC的AuthN請求中scope引數必須要有一個值為的openid的引數,用來區分這是一個OIDC的Authentication請求,而不是OAuth2的Authorization請求。
然後我們簡單瞭解下 ID Token

ID Token

OIDC對OAuth2最主要的擴充套件就是提供了ID Token.
ID Token是一個安全令牌,是一個授權伺服器提供的包含使用者資訊(由一組Cliams構成以及其他輔助的Cliams)的JWT格式的資料結構。
ID Token的主要構成部分如下(使用OAuth2流程的OIDC)。

  • iss = Issuer Identifier:必須。提供認證資訊者的唯一標識。一般是一個https的url(不包含querystring和fragment部分)。
  • sub = Subject Identifier:必須。iss提供的EU的標識,在iss範圍內唯一。它會被RP用來標識唯一的使用者。最長為255個ASCII個字元。
  • aud = Audience(s):必須。標識ID Token的受眾。必須包含OAuth2的client_id。
  • exp = Expiration time:必須。過期時間,超過此時間的ID Token會作廢不再被驗證通過。
  • iat = Issued At Time:必須。JWT的構建的時間。
  • auth_time = AuthenticationTime:EU完成認證的時間。如果RP傳送AuthN請求的時候攜帶max_age的引數,則此Claim是必須的。
  • nonce:RP傳送請求的時候提供的隨機字串,用來減緩重放攻擊,也可以來關聯ID Token和RP本身的Session資訊。
  • acr = Authentication Context Class Reference:可選。表示一個認證上下文參照值,可以用來標識認證上下文類。
  • amr = Authentication Methods References:可選。表示一組認證方法。
  • azp = Authorized party:可選。結合aud使用。只有在被認證的一方和受眾(aud)不一致時才使用此值,一般情況下很少使用。

這裡只是簡單介紹,感興趣的同學可以去官網瞭解更多。下面開始實操。
拋玉引磚 O(∩_∩)O !

專案准備

.Net 6
Visual Studio 2022
https://github.com/fengzhonghao8-24/Consul.Ocelot

搭建IdentityServer4專案

我們基於上篇專案來新增一個IdentityServer4Center專案

引入IdentityServer4 Nuget包,然後新增一個組態檔 Config.cs 設定模擬資料
定義API範圍

//https://www.cnblogs.com/Mamba8-24
public static IEnumerable<ApiScope> ApiScopes =>
    new List<ApiScope>
    {
       new ApiScope("serviceA"),
       new ApiScope("serviceB")
    };

定義API資源

//https://www.cnblogs.com/Mamba8-24
public static IEnumerable<ApiResource> GetApiResources()
{
    return new List<ApiResource>()
    {
        new ApiResource("serviceA","serviceA"){ Scopes={ "serviceA" } },
        new ApiResource("serviceB","serviceB"){ Scopes={ "serviceB" } }
    };
}

定義使用者端

//https://www.cnblogs.com/Mamba8-24
public static IEnumerable<Client> GetClients()
{
    return new List<Client>
    {
       new Client{
           ClientId="web_client",//使用者端唯一標識
           ClientName="AuthCenter",
           //AllowedGrantTypes=new List<string>{ "paas_password", "paas_auth_code", "client_credentials"},
           AllowedGrantTypes=GrantTypes.ClientCredentials,
           ClientSecrets=new[]{new Secret("Mamba24".Sha256()) },//使用者端密碼,進行了加密
           AccessTokenLifetime=3600,
           AllowedScopes=new List<string>//允許存取的資源
           {
                "serviceA",
                "serviceB"
           },
           Claims=new List<ClientClaim>(){
           new ClientClaim(IdentityModel.JwtClaimTypes.Role,"Admin"),
           new ClientClaim(IdentityModel.JwtClaimTypes.NickName,"Mamba24"),
           }
       }
    };
}

然後在Program.cs進行DI

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddIdentityServer()
    .AddDeveloperSigningCredential()
    .AddInMemoryClients(Config.GetClients())//Client模式
    .AddInMemoryApiScopes(Config.ApiScopes)//作用域
    .AddInMemoryApiResources(Config.GetApiResources());//資源

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

app.UseIdentityServer();//使用Id4

然後我們將專案run起來看看效果:

使用Postman請求Token

這裡請求引數內容都是在定義使用者端的時候定義好的,只需要注意 grant_type 這裡為 client_credentials 使用者端模式,如果在定義使用者端的時候AllowedGrantTypes 設定的是 ResourceOwnerPassword grant_typepassword,具體參考GrantTypes.cs。

OK,IdentityServer4Center專案搭建完成。接下來我們將閘道器與 IdentityServer4 結合使用

Ocelot結合dentityServer4

接下來會模擬通過整合了Id4的閘道器來存取ServiceA或者ServiceB服務以達到演示效果。
首先我們在Gateway專案中裝兩個包

然後在ocelot.json 檔案中增加Id4鑑權資源

{
  "Routes": [
    {
      "DownstreamPathTemplate": "/{url}", //下游(轉發的服務地址模板)
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 5550
        }
      ],
      "UpstreamPathTemplate": "/ocelot/{url}", //上游(請求路徑模板)
      "UpstreamHttpMethod": [ "Get", "Post" ],
      //鑑權
      "AuthenticationOptions": {
        "AuthenticationProviderKey": "serviceA", //指定一個key
        "AllowedScopes": [ "serviceA" ] //id4的作用域名稱
      }
    },
    {
      "DownstreamPathTemplate": "/{url}", //下游(轉發的服務地址模板)
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 5551
        }
      ],
      "UpstreamPathTemplate": "/ocelotB/{url}", //上游(請求路徑模板)
      "UpstreamHttpMethod": [ "Get", "Post" ],
      //鑑權
      "AuthenticationOptions": {
        "AuthenticationProviderKey": "serviceB", //指定一個key
        "AllowedScopes": [ "serviceB" ] //id4的作用域名稱
      }
    }
  ]
}

然後在 Program.cs進行設定
由於Ocelot閘道器預設整合了Id4所以我們只需要在之前程式碼的基礎上增加對需要進行鑑權服務的設定。

var builder = WebApplication.CreateBuilder(args);

builder.Configuration.AddJsonFile("ocelot.json", optional: false, reloadOnChange: true);

var authenticationProviderKey = "serviceA";
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
              .AddIdentityServerAuthentication(authenticationProviderKey, options =>
              {
                  options.Authority = "http://localhost:5269";//id4服務地址
                  options.ApiName = "serviceA";//id4 api資源裡的apiname
                  options.RequireHttpsMetadata = false; //不使用https
                  options.SupportedTokens = SupportedTokens.Both;
              });

authenticationProviderKey = "serviceB";
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
              .AddIdentityServerAuthentication(authenticationProviderKey, options =>
              {
                  options.Authority = "http://localhost:5269";//id4服務地址
                  options.ApiName = "serviceB";//id4 api資源裡的apiname
                  options.RequireHttpsMetadata = false; //不使用https
                  options.SupportedTokens = SupportedTokens.Both;
              });

builder.Services.AddOcelot().AddConsul();

到這裡我們就差不多簡單的設定完成了,接下來看看效果。
同時啟動GatewayIdentityServer4CenterServiceAServiceB專案
閘道器專案啟動地址埠為5055

我們現在直接通過閘道器來請求ServiceA服務的介面(不帶token) http://localhost:5055/ocelot/testA
結果出現401,達到預期效果

接著我們帶上token再次存取
返回200,請求成功。

然後接下來我們給ServiceB 服務單獨增加授權認證,模擬請求繞過閘道器直接存取下游服務。
Program.cs


程式碼很簡單,我們單獨存取ServiceB看看效果

結果出現 401
然後我們拿著token去存取

可以正常存取
到此,我們基於 使用者端模式(client credentials)模擬的一個簡單的範例就完成了,這個模式沒有使用者參與也就是說此模式只適用於在資源或者資源伺服器不屬於某個人或者使用者,沒有資源所有者對資源進行控制,但是使用者端需要存取這個受保護資源的情況下。
接下來我們可以嘗試在使用者端模式(client credentials)下存取Id4的身份認證資源(對應使用者)
我們稍微調整程式碼
Config.cs


模擬使用者

Program.cs

然後我們先在 client credentials 使用者端模式 下請求token,去存取Id4的身份認證資源。

出現 403異常,所以在這個模式下這樣的請求是不合理的,這也就驗證了我們上述所說。

接下來我們可以嘗試模擬 密碼模式(resource owner password credentials)這個模式會有使用者參與

然後再次存取Id4的身份認證資源

OK,成功拿到資源資訊。

結尾

由於文章篇幅有限,涉及到知識內容也不是很深入,感興趣的同學可以自行研究。
然後本文都是基於我的個人理解,然後也有參考官網以及大佬的文章和視訊,文章如有什麼不妥的地方歡迎指正,共同進步。後續有時間還會繼續學習相關技術知識,歡迎Star與關注。謝謝