【One by One系列】IdentityServer4(二)使用Client Credentials保護API資源

書接上回,我們將會正式開始介紹IdentityServer4。

IdentityServer4是實現了OAuth2.0+OpenId Connect兩種協議的優秀第三方庫,屬於.net生態中的優秀成員。可以很容易集成至ASP.NET Core,頒發token。

使用Id4基本步驟如下:

  • **1.**在Startup.Configure方法中調用

app.UseIdentityServer();

添加中間件,把Id4添加至http請求處理管道,這使得Id4可以爲OpenID Connect和OAuth2協議描述的端點(如/connect/token)請求提供服務。

  • **2.**在Startup.ConfigureServices中註冊IdentityServer4

 services.AddIdentityServer(options=>
                            {
                                ...
                            });
  • **3.**配置Identity Server

    • Identity資源表示提供給客戶端進行用戶識別的信息(聲明)。聲明可能包括用戶名稱、電子郵件地址等。

    • API資源表示用戶可通過訪問令牌訪問的受保護數據或功能。API 資源的一個示例是要求授權的 Web API(或 API集合)。

    • 用於簽名的憑據(credentials)

    • 用戶可能會請求訪問的Identity資源和API資源

    • 會請求獲取token的客戶端

    • 用戶信息的存儲機制,如ASP.NET Core Identity或者其他機制

當你指明Id4使用的客戶端和資源,可以將IEnumerable<T>傳遞給接受內存中的客戶端或資源存儲的方法,如果在更復雜的場景,可以通過依賴注入的方式提供客戶端和資源提供程序類型。

IdentityServer4 使用自定義 IClientStore 類型提供的內存中資源和客戶端的示例配置:

public IServiceProvider ConfigureServices(IServiceCollection services)
{
    //...
    services.AddSingleton<IClientStore, CustomClientStore>();
    services.AddIdentityServer()
        .AddSigningCredential("CN=sts")
        .AddInMemoryApiResources(MyApiResourceProvider.GetAllResources())
        .AddAspNetIdentity<ApplicationUser>();
    //...
}

經過上面的概述,我們先易後難,將從最簡單的客戶端憑證開始實戰搭建IdentityServer4項目並以此保護api資源,首先客戶端憑證屬於OAuth2.0的一種授權方式。

  • 主要是向IdentityServer發送post請求token?grant_type=client_credentials& client_id=CLIENT_ID& client_secret=CLIENT_SECRET,獲取access-token,以此來訪問api資源。

  • 在IdentityServer4中,增加了Scope參數,表明了客戶端的訪問權限

1.安裝Id4模板

dotnet new -i IdentityServer4.Templates
  • AdminUI:測試,生產環境需要交錢,商業軟件

  • ASP.NET Core Identity:結合ASP.NET Core Indentity

  • Empty:空模板

  • Entity Frame Store:使用ef數據持久化身份認證信息

  • In-Memory Stores and Test Users:添加內存中的用戶認證信息,和測試用戶

  • Quickstart UI (UI assets only):UI

2.創建ASP.NET Core應用,搭載Id4

2.1 創建項目

  • 使用IdentityServer4的空模板創建應用

md quickstart
cd quickstart

md src
cd src

#空模板 項目
dotnet new is4empty -n IdentityServer
The template "IdentityServer4 Empty" was created successfully.
  • 項目添加解決方案

cd ..
dotnet new sln -n QuickStart

dotnet sln add .\IdentityServer\IdentityServer.csproj

2.2 修改launchSettings.json

  • 測試環境,使用http,刪掉IIS相關的配置

{
  "profiles": {
    "SelfHost": {
      "commandName": "Project",
      "launchBrowser": true,
      "launchUrl": ".well-known/openid-configuration",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      },
      "applicationUrl": "http://localhost:5001/"
    }
  }
}

2.3 定義一個api scope

上篇與前文都介紹過,scope代表資源所有者在被保護資源那裏的一些權限,可以把被保護資源分爲不同的scope,具體的粒度由開發自定義。

  • 模板中ApiScope爲空,在Config.cs增加

public static IEnumerable<ApiScope> ApiScopes =>
new ApiScope[]
{
    //新增
    new ApiScope("api1", "My API")
};
  • 第二個參數,是displayname

2.4 定義一個客戶端

要讓我們的IdentityServer給客戶端頒發token,就要讓客戶端在IdentityServer註冊。

客戶端,模板中的客戶端與scope一樣爲空,在Config.cs增加客戶端,代碼如下:

 public static IEnumerable<Client> Clients =>
            new Client[]
            {
                 new Client
                    {
                        ClientId = "client app",

                        // no interactive user, use the clientid/secret for authentication
                        AllowedGrantTypes = GrantTypes.ClientCredentials,

                        // secret for authentication
                        ClientSecrets =
                        {
                            new Secret("secret-123456".Sha256())
                        },

                        // scopes that client has access to
                        AllowedScopes = { "api1" }
                    }
            };

我們之後會增加這個定義的客戶端,這個客戶端將會訪問AllowedScopes指定的api scope。

注意:在此場景下,客戶端跟用戶是沒有交互的,身份認證是通過IdentityServer的客戶密鑰。

官方描述:你可以把ClientId和ClientSecret看作應用程序本身的登錄名和密碼。它向身份服務器表明您的應用程序的身份(我是xx應用程序,想訪問服務器)。

2.5 註冊IdentityServer

註釋模板代碼Startup.ConfigureServices()所有代碼,增加代碼:加載定義的資源和客戶端,代碼如下:

public void ConfigureServices(IServiceCollection services)
{
    var builder = services.AddIdentityServer()
        .AddInMemoryApiScopes(Config.ApiScopes)
        .AddInMemoryClients(Config.Clients);

    // omitted for brevity
}

配置完成。運行並瀏覽器訪問http://localhost:5001/.well-known/openid-configuration,就能看到discovery document.

  • 它是IdentityServer中的標準端點

  • 客戶端和APIs會使用它下載必要的配置數據,容後再表

在第一次啓動時,IdentityServer將創建一個開發者簽名密鑰,它是一個名爲tempkey.rsa的文件。您不必將該文件簽入源代碼版本控制,如果不存在該文件,它將被重新創建。

3.創建webapi

限制開始創建我們需要保護的api資源

3.1 新建項目

dotnet new webapi -n webapi
cd ..
dotnet sln add  .\webapi\webapi.csproj

3.2 修改launchSettings.json

{
  "profiles": {
    "Api": {
      "commandName": "Project",
      "launchBrowser": true,
      "applicationUrl": "http://localhost:6001",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

3.3 新增api

IdentityController.cs

    [Route("api/[controller]")]
    [ApiController]
    public class IdentityController : ControllerBase
    {
        [HttpGet]
        public IActionResult Get()
        {
            return new JsonResult(from c in User.Claims select new { c.Type, c.Value });
        }
    }

3.4 引入nuget包

Microsoft.AspNetCore.Authentication.JwtBearer

這個包是當收到請求時,對授權頭中JWT的具體身份認證

dotnet add .\webapi\webapi.csproj package Microsoft.AspNetCore.Authentication.JwtBearer

3.5 註冊服務和添加中間件

最後一步是將身份認證服務添加到依賴注入中,並將身份認證中間件添加到管道中。以便:

  • 驗證傳入的token,確保token來自可信的頒佈者(服務器)

  • 驗證這個token在這個api中使用是有效的(也就是受衆)

看代碼:

{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();

        services.AddAuthentication("Bearer")
            .AddJwtBearer("Bearer", options =>
            {
                //token頒發者
                options.Authority = "http://localhost:5001";

                options.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateAudience = false
                };
                options.RequireHttpsMetadata = false;
            });
        
        services.AddAuthorization(options =>
            {
                options.AddPolicy("ApiScope", policy =>
                {
                    policy.RequireAuthenticatedUser();
                    
                    //scope
                    policy.RequireClaim("scope", "api1");
                });
            });
    }

    public void Configure(IApplicationBuilder app)
    {
        app.UseRouting();

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

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers().RequireAuthorization("ApiScope");
        });
    }
}
  • AddAuthentication:增加認證服務到依賴注入,註冊Bearer作爲默認scheme

  • AddAuthorization:增加授權服務到依賴注入,驗證token中是否存在scope,這裏使用的是ASP.NET Core授權策略系統

    這裏實質是驗證jwt中的payload的scope

  • RequireHttpsMetadata 用於測試目的;將此參數設置爲 false,可在你沒有證書的環境中進行測試。在實際部署中,JWT 持有者令牌應始終只能通過 HTTPS 傳遞。

  • UseAuthentication:添加認證中間件,以便對host的每次調用自動執行身份認證,此中間件準備就緒後,會自動從授權標頭中提取 JWT 令牌。然後對其進行反序列化、驗證,,並將其存儲爲用戶信息,稍後供 MVC 操作或授權篩選器引用。

    JWT 持有者身份驗證中間件還可以支持更高級的方案,例如頒發機構authority 不可用時使用本地證書驗證令牌。對於此情景,可以在 JwtBearerOptions 對象中指定 TokenValidationParameters 對象。

  • UseAuthorization:添加授權中間件,以確保我們的api不會被匿名客戶端訪問

  • RequireAuthorization("ApiScope"):全局執行授權策略

    除了全局以外,還可以針對多有的api端點,或者特定的controller,action,根據實際的業務場景靈活變化吧

訪問:http://localhost:6001/identity,返回狀態碼401,這是api要求憑證,所以現在api是被IdentityServer保護着

4.創建客戶端

最後一步,創建一個由IdentityServer管理的客戶端,並通過客戶端請求access-token,然後訪問api

4.1 新建項目

dotnet new console -n Client
dotnet sln add .\Client\Client.csproj

4.2 引入nuget包

需要引入IdentityModel包,一個客戶端類,以請求disconvery endpoint

cd .\Client\
dotnet add package IdentityModel

4.3 編碼-請求Idisconvery endpoint

只需要知道IdentityServer的基礎地址,實際的各類端點地址就可以從元數據中讀取:

    class Program
    {
        static async Task Main(string[] args)
        {
            // discover endpoints from metadata
            var client = new HttpClient();
            var disco = await client.GetDiscoveryDocumentAsync("http://localhost:5001");
            if (disco.IsError)
            {
                Console.WriteLine(disco.Error);
                return;
            }
        }
    }

4.4 編碼-請求access token

這一步,使用從discovery document中獲取的信息,向IdentityServer請求一個訪問api1的token:

// request token
var tokenResponse = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
{
    Address = disco.TokenEndpoint,

    //在IdentityServer註冊的id與secret
    ClientId = "client app",
    ClientSecret = "secret-123456",
    Scope = "api1"
});

if (tokenResponse.IsError)
{
    Console.WriteLine(tokenResponse.Error);
    return;
}

Console.WriteLine(tokenResponse.Json);

注意看,這裏的ClientId,ClientSecret必須與2.4中定義客戶端保持一致,Scope也要與AllowedScopes保持一致。

4.5 編碼-調用api

在這一步,使用擴展方法SetBearerToken,這個方法主要組裝http請求:授權頭+access token,並以此請求訪問api資源:

// call api
var apiClient = new HttpClient();
apiClient.SetBearerToken(tokenResponse.AccessToken);

var response = await apiClient.GetAsync("http://localhost:6001/api/identity");
if (!response.IsSuccessStatusCode)
{
    Console.WriteLine(response.StatusCode);
}
else
{
    var content = await response.Content.ReadAsStringAsync();
    Console.WriteLine(JArray.Parse(content));
}

5.測試

  • 啓動IdentityServer

cd .\IdentityServer\
dotnet run
  • 啓動webapi

cd .\webapi\
dotnet run
  • 用vs啓動client

獲取access-token,我們通過http://jwt.calebb.net/解析

這也是api返回的Claims

身份認證的中間對JWT進行了身份認證後,會把解析到的Claims組裝進HttpContext,以供下一個中間件(如授權中間件)調用

接下來我們就去觸發不同的錯誤去了解IdentityServer是如何工作的,我選擇其中幾個比較有意義的測試:

5.1 使用一個無效客戶端id或者密鑰請求token

沒被註冊的客戶端,訪問時,所以是invalid_client

類比場景:去辦理門禁卡,物業沒找到你這個業主信息,辦個鬼呀

5.2 在請求token時指定無效的scope

請求token,指定的scope,在indentityserver中並不存在,所以是invalid_scope

類比場景:去辦理門禁卡,小區一共10棟,你去辦11棟,辦個鬼呀

5.3 請求api時,不傳入toekn

不傳入token,那麼webapi就沒收到token,所以返回Unauthorized未授權

類比場景:進入小區,沒有門禁,肯定不讓你進

5.4 修改API對scope的驗證要求

被保護的資源webapi中配置plicy.RequireClaim("scope","api2");

而客戶端指定的scope是api1

客戶端是有access-token,具有進入系統憑證,但是,只允許scope爲api2的訪問,傳入的時api1,當然就返回Forbidden

類比場景:小區進入後,進入單元樓,明明是3棟2單元的樓宇,但是你的門禁只能針對3棟1單元,當然也不會刷開2單元的大門

參考鏈接

https://identityserver4.readthedocs.io/en/latest/quickstarts/1_client_credentials.html#source-code

長按二維碼關注

點外賣,先領券
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章