在 IdentityServer4 中創建客戶端


創建客戶端

在創建了 IdentityServer4 服務器之后,我們可以准備從獲取一個訪問令牌開始。

1. 客戶端憑證式驗證流

在 OpenID Connect 中,最為簡單的驗證方式為客戶端憑借方式了。我們從這種方式開始。OpenID Connect 是 OAuth 的擴展,我們找一段阮一峰的博客來進行說明。

第四種方式:憑證式

最后一種方式是憑證式(client credentials),適用於沒有前端的命令行應用,即在命令行下請求令牌。

第一步,A 應用在命令行向 B 發出請求。

https://oauth.b.com/token?
  grant_type=client_credentials&
  client_id=CLIENT_ID&
  client_secret=CLIENT_SECRET

上面 URL 中,grant_type參數等於client_credentials表示采用憑證式,client_idclient_secret用來讓 B 確認 A 的身份。

第二步,B 網站驗證通過以后,直接返回令牌。

這種方式給出的令牌,是針對第三方應用的,而不是針對用戶的,即有可能多個用戶共享同一個令牌。

原文地址:http://www.ruanyifeng.com/blog/2019/04/oauth-grant-types.html

2. 服務器端實現

實際的服務端點在上一篇中,通過訪問端點 http://localhost:5000/.well-known/openid-configuration 就可以得到,從響應的結果中可以找到如下的一行:

 "token_endpoint": "http://localhost:5000/connect/token",

而客戶端的標示和密鑰則需要我們在服務器端提供。

將上一個項目中的 Config.cs 文件替換為如下內容,代碼中硬編碼了客戶端的標識為 client ,密鑰為 secret

// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.


using IdentityServer4.Models;
using System.Collections.Generic;

namespace IdentityServer
{
    public static class Config
    {
        public static IEnumerable<IdentityResource> Ids =>
            new IdentityResource[]
            { 
                new IdentityResources.OpenId()
            };

        // scopes define the API resources in your system
        public static IEnumerable<ApiResource> Apis =>
            new List<ApiResource>
            {
                new ApiResource("api1", "My API")
            };
        

        // clients want to access resources (aka scopes)
        public static IEnumerable<Client> Clients =>
            new List<Client>
            {
                new Client
                {
                    ClientId = "client",
                    AllowedGrantTypes = GrantTypes.ClientCredentials,

                    ClientSecrets =
                    {
                        new Secret("secret".Sha256())
                    },
                    AllowedScopes = { "api1" }
                }
            };
    }
}

在這里通過 Apis 這個委托提供了 API 資源,通過 Clients 這個委托提供了客戶端的憑據。

在啟動應用之后,我們可以訪問服務器來獲取令牌。

3. 獲取訪問令牌

這里,我們使用 Postman 來完成。

將請求的發送方式修改為 Post

地址設置為:http://localhost:5000/connect/token

請求的 Body ,在 Body 的設置中,首先選中格式:x-www-form-urlencoded,提供如下三個參數:

KEY VALUE
grant_type client_credentials
client_id Client
client_secret Secret

點擊 Send 按鈕,發送請求之后,就應該可以看到如下的響應內容:

{
    "access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IklWR2VMQ3h5NFJjcWJnbmUxb1JVN3ciLCJ0eXAiOiJhdCtqd3QifQ.eyJuYmYiOjE1ODU4MTQwNzMsImV4cCI6MTU4NTgxNzY3MywiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo1MDAwIiwiYXVkIjoiYXBpMSIsImNsaWVudF9pZCI6ImNsaWVudCIsInNjb3BlIjpbImFwaTEiXX0.R5RNGRM6bVvdNIgdXnD-QK5HK-kHA5hcZ-ltn0K3kLZp9R3BGQeg5qfnQXT4sU2CqPGYIatwbZY3bysQ9krkq5BpWzSzwY7EYPybsP3gty0BUK2QXnEwxsT1boN_cM2Hw9ua4nal3IHB4XJJkMj7jo33S8NtQQyJr26_G1WqlOgvlVfUiPYQWiY9OHPgTAIqrU_4aogoxiC84lHWC5Pf6oX6jxLoAWzKkhl-NdH33gW169xdtkPXp51XpbXhxNujBo7LAVOI-_5ztouuYLShOf5bOt1bunHfeNCv1DPl2rBsfFITjkoltQXVrTSZGLEQgNH_ryBqdoTyM-jWP1HN4g",
    "expires_in": 3600,
    "token_type": "Bearer",
    "scope": "api1"
}

4. 實現自定義的存儲

這個 Config 中提供的靜態屬性只是為了方便進行簡單的測試,在 IdentityServer4 內部,定義了驗證客戶端的接口 IClientStore,如下所示:

// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.


using IdentityServer4.Models;
using System.Threading.Tasks;

namespace IdentityServer4.Stores
{
    /// <summary>
    /// Retrieval of client configuration
    /// </summary>
    public interface IClientStore
    {
        /// <summary>
        /// Finds a client by id
        /// </summary>
        /// <param name="clientId">The client id</param>
        /// <returns>The client</returns>
        Task<Client> FindClientByIdAsync(string clientId);
    }
}

源碼地址:https://github.com/IdentityServer/IdentityServer4/blob/master/src/Storage/src/Stores/IClientStore.cs

在 IdentityServer4 的內部,也實現了一個基於內存中集合實現的內存中客戶端存儲 InMemoryClientStore,代碼實現如下:

// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.


using IdentityServer4.Extensions;
using IdentityServer4.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace IdentityServer4.Stores
{
    /// <summary>
    /// In-memory client store
    /// </summary>
    public class InMemoryClientStore : IClientStore
    {
        private readonly IEnumerable<Client> _clients;

        /// <summary>
        /// Initializes a new instance of the <see cref="InMemoryClientStore"/> class.
        /// </summary>
        /// <param name="clients">The clients.</param>
        public InMemoryClientStore(IEnumerable<Client> clients)
        {
            if (clients.HasDuplicates(m => m.ClientId))
            {
                throw new ArgumentException("Clients must not contain duplicate ids");
            }
            _clients = clients;
        }

        /// <summary>
        /// Finds a client by id
        /// </summary>
        /// <param name="clientId">The client id</param>
        /// <returns>
        /// The client
        /// </returns>
        public Task<Client> FindClientByIdAsync(string clientId)
        {
            var query =
                from client in _clients
                where client.ClientId == clientId
                select client;
            
            return Task.FromResult(query.SingleOrDefault());
        }
    }
}

源碼地址:https://github.com/IdentityServer/IdentityServer4/blob/master/src/IdentityServer4/src/Stores/InMemory/InMemoryClientStore.cs

你看,我們只需要傳一個 Client 的可迭代對象就可以構造這樣一個 InMemoryClientStore 對象實例。實際上,我們調用的 AddInMemoryClients 這個擴展方法確實就是這么做的:

/// <summary>
/// Adds the in memory clients.
/// </summary>
/// <param name="builder">The builder.</param>
/// <param name="clients">The clients.</param>
/// <returns></returns>
public static IIdentityServerBuilder AddInMemoryClients(this IIdentityServerBuilder builder, IEnumerable<Client> clients)
{
  builder.Services.AddSingleton(clients);

  builder.AddClientStore<InMemoryClientStore>();

  var existingCors = builder.Services.Where(x => x.ServiceType == typeof(ICorsPolicyService)).LastOrDefault();
  if (existingCors != null && 
      existingCors.ImplementationType == typeof(DefaultCorsPolicyService) && 
      existingCors.Lifetime == ServiceLifetime.Transient)
  {
    // if our default is registered, then overwrite with the InMemoryCorsPolicyService
    // otherwise don't overwrite with the InMemoryCorsPolicyService, which uses the custom one registered by the host
    builder.Services.AddTransient<ICorsPolicyService, InMemoryCorsPolicyService>();
  }

  return builder;
}

源碼地址:https://github.com/IdentityServer/IdentityServer4/blob/master/src/IdentityServer4/src/Configuration/DependencyInjection/BuilderExtensions/InMemory.cs

這里使用了依賴注入完成 InMemoryClientStore 的構造注入。

好了,我們自己實現一個自定義的客戶端憑據存儲。比如 CustomClientStore。

在構造函數中,我們構建了內部存儲客戶端憑據的集合,由於實現了 IClientStore ,在實現的方法中,提供了檢索客戶端端的實現,其實這個方法完全從 InMemoryClientStore 復制過來的。

using IdentityServer4.Models;
using IdentityServer4.Stores;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace IdentityServer
{
    public class CustomClientStore : IClientStore
    {
        private readonly IEnumerable<Client> _clients;
        public CustomClientStore()
        {
            _clients = new List<Client>
            {
                new Client
                {
                    ClientId = "client",
                    AllowedGrantTypes = GrantTypes.ClientCredentials,

                    ClientSecrets =
                    {
                        new Secret("secret".Sha256())
                    },
                    AllowedScopes = { "api1" }
                }
            };
        }

        public Task<Client> FindClientByIdAsync(string clientId)
        {
            var query =
                from client in _clients
                where client.ClientId == clientId
                select client;
            
            return Task.FromResult(query.SingleOrDefault());
        }
    }
}

在 .NET Core 中,服務是通過依賴注入的方式被使用的,所以,我們需要注冊這個服務。回到 Startup.cs 這個文件,將 ConfigureServices() 方法替換為如下內容。

public void ConfigureServices(IServiceCollection services)
{
  // uncomment, if you want to add an MVC-based UI
  //services.AddControllersWithViews();

  services.AddSingleton<IClientStore, CustomClientStore>();

  var builder = services.AddIdentityServer()
    .AddInMemoryIdentityResources(Config.Ids)
    .AddInMemoryApiResources(Config.Apis);

  // not recommended for production - you need to store your key material somewhere secure
  builder.AddDeveloperSigningCredential();
}

主要做了兩件事:

  1. 刪除了原來的 .AddInMemoryClients(Config.Clients);
  2. 添加了 services.AddSingleton<IClientStore, CustomClientStore>(); 來注冊服務實現

重新運行程序,並使用 Postman 訪問,可以重新得到一個新的訪問令牌。

控制台輸出如下所示:

PS C:\temp\is4\IdentityServer> dotnet run
[02:06:36 Information]
Starting host...

[02:06:37 Information] IdentityServer4.Startup
Starting IdentityServer4 version 3.1.0.0

[02:06:37 Information] IdentityServer4.Startup
You are using the in-memory version of the persisted grant store. This will store consent decisions, authorization codes
, refresh and reference tokens in memory only. If you are using any of those features in production, you want to switch
to a different store implementation.

[02:06:37 Information] IdentityServer4.Startup
Using the default authentication scheme idsrv for IdentityServer

[02:06:37 Debug] IdentityServer4.Startup
Using idsrv as default ASP.NET Core scheme for authentication

[02:06:37 Debug] IdentityServer4.Startup
Using idsrv as default ASP.NET Core scheme for sign-in

[02:06:37 Debug] IdentityServer4.Startup
Using idsrv as default ASP.NET Core scheme for sign-out

[02:06:37 Debug] IdentityServer4.Startup
Using idsrv as default ASP.NET Core scheme for challenge

[02:06:37 Debug] IdentityServer4.Startup
Using idsrv as default ASP.NET Core scheme for forbid

[02:06:59 Debug] IdentityServer4.Startup
Login Url: /Account/Login

[02:06:59 Debug] IdentityServer4.Startup
Login Return Url Parameter: ReturnUrl

[02:06:59 Debug] IdentityServer4.Startup
Logout Url: /Account/Logout

[02:06:59 Debug] IdentityServer4.Startup
ConsentUrl Url: /consent

[02:06:59 Debug] IdentityServer4.Startup
Consent Return Url Parameter: returnUrl

[02:06:59 Debug] IdentityServer4.Startup
Error Url: /home/error

[02:06:59 Debug] IdentityServer4.Startup
Error Id Parameter: errorId

[02:06:59 Debug] IdentityServer4.Hosting.EndpointRouter
Request path /connect/token matched to endpoint type Token

[02:06:59 Debug] IdentityServer4.Hosting.EndpointRouter
Endpoint enabled: Token, successfully created handler: IdentityServer4.Endpoints.TokenEndpoint

[02:06:59 Information] IdentityServer4.Hosting.IdentityServerMiddleware
Invoking IdentityServer endpoint: IdentityServer4.Endpoints.TokenEndpoint for /connect/token

[02:06:59 Debug] IdentityServer4.Endpoints.TokenEndpoint
Start token request.

[02:06:59 Debug] IdentityServer4.Validation.ClientSecretValidator
Start client validation

[02:06:59 Debug] IdentityServer4.Validation.BasicAuthenticationSecretParser
Start parsing Basic Authentication secret

[02:06:59 Debug] IdentityServer4.Validation.PostBodySecretParser
Start parsing for secret in post body

[02:06:59 Debug] IdentityServer4.Validation.SecretParser
Parser found secret: PostBodySecretParser

[02:06:59 Debug] IdentityServer4.Validation.SecretParser
Secret id found: client

[02:06:59 Debug] IdentityServer4.Validation.SecretValidator
Secret validator success: HashedSharedSecretValidator

[02:06:59 Debug] IdentityServer4.Validation.ClientSecretValidator
Client validation success

[02:06:59 Debug] IdentityServer4.Validation.TokenRequestValidator
Start token request validation

[02:06:59 Debug] IdentityServer4.Validation.TokenRequestValidator
Start client credentials token request validation

[02:06:59 Debug] IdentityServer4.Validation.TokenRequestValidator
client credentials token request validation success

[02:06:59 Information] IdentityServer4.Validation.TokenRequestValidator
Token request validation success, {"ClientId": "client", "ClientName": null, "GrantType": "client_credentials", "Scopes"
: "api1", "AuthorizationCode": null, "RefreshToken": null, "UserName": null, "AuthenticationContextReferenceClasses": nu
ll, "Tenant": null, "IdP": null, "Raw": {"grant_type": "client_credentials", "client_id": "client", "client_secret": "**
*REDACTED***"}, "$type": "TokenRequestValidationLog"}

[02:06:59 Debug] IdentityServer4.Services.DefaultClaimsService
Getting claims for access token for client: client

[02:06:59 Debug] IdentityServer4.Endpoints.TokenEndpoint
Token request success.

通過 IdentityServer4 提供的擴展方法 AddClientStore() ,還可以使用 IdentityServer4 來完成服務的注冊。

            var builder = services.AddIdentityServer()
                .AddInMemoryIdentityResources(Config.Ids)
                .AddInMemoryApiResources(Config.Apis)
                .AddClientStore<CustomClientStore>();

祝你順利!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM