使用Abp vnext構建基於Duende.IdentityServer的統一授權中心(一)

喻平勇發表於2021-11-10

原來看到很多示例都是基於IdentityServer4的統一授權中心,但是IdentityServer4維護到2022年就不再進行更新維護了,所以我選擇了它的升級版Duende.IdentityServer(這個有總營收超過100W美金就需要付費的限制).

整個授權中心完成我打算分成4個部分去構建整個專案,爭取在12月中旬全部完成.

第一部分(已完成):與Abp vnext進行整合,實現資料庫儲存,並且能夠正常頒發token

第二部分(構建中):實現視覺化管理後臺

第三部分(未開始):實現自定義賬戶體系,單點登入等...

第四部分(未開始):接入閘道器(我還另外整了一個基於Yarp的簡單閘道器)

注:基於Yarp的閘道器專案以及統一授權中心在我完成第二部分的構建時會開源出來(並不包含Duende.IdentityServer本身)

接下來講解第一部分的實現

下圖是我的解決方案(我沒有使用預設的Abp vnext生成的專案模板,而是我在去掉ABP預設的模組後保留了自己覺得已經適用的基礎模組建立的模板):

既然是要支援持久化到資料庫,那麼我就需要把原來實體型別進行改造,以客戶端資訊表為例,下面程式碼中所變更之處

a.所有實體類都繼承自 Entity<Guid>並且使用GUID作為主鍵(Abp推薦使用GUID作為主鍵)

b.去掉了原有的外來鍵關係

c.增加了字串型別欄位的長度限制

  1 #pragma warning disable 1591
  2 
  3 using System;
  4 using System.Collections.Generic;
  5 using System.ComponentModel.DataAnnotations;
  6 using Duende.IdentityServer.Models;
  7 using Volo.Abp.Domain.Entities;
  8 
  9 namespace Pterosaur.Authorization.Domain.Entities
 10 {
 11     public class Client: Entity<Guid>
 12     {
 13         public Client() { }
 14         public Client(Guid id)
 15         {
 16             Id = id;
 17         }
 18         /// <summary>
 19         /// 是否啟用
 20         /// </summary>
 21         public bool Enabled { get; set; } = true;
 22         /// <summary>
 23         /// 客戶端ID
 24         /// </summary>
 25         [MaxLength(128)]
 26         public string ClientId { get; set; }
 27         /// <summary>
 28         /// 協議型別
 29         /// </summary>
 30         [MaxLength(64)]
 31         public string ProtocolType { get; set; } = "oidc";
 32         /// <summary>
 33         /// 如果設定為false,則在令牌端點請求令牌時不需要客戶端機密(預設為<c>true</c> 34         /// </summary>
 35         public bool RequireClientSecret { get; set; } = true;
 36         /// <summary>
 37         /// 客戶端名
 38         /// </summary>
 39         [MaxLength(128)]
 40         public string ClientName { get; set; }
 41         /// <summary>
 42         /// 描述
 43         /// </summary>
 44         [MaxLength(1024)]
 45         public string Description { get; set; }
 46         /// <summary>
 47         /// 客戶端地址
 48         /// </summary>
 49         [MaxLength(256)]
 50         public string ClientUri { get; set; }
 51         /// <summary>
 52         /// 客戶端LGOGO地址
 53         /// </summary>
 54         [MaxLength(512)]
 55         public string LogoUri { get; set; }
 56         /// <summary>
 57         /// 指定是否需要同意螢幕(預設為<c>false</c> 58         /// </summary>
 59         public bool RequireConsent { get; set; } = false;
 60         /// <summary>
 61         /// 指定使用者是否可以選擇儲存同意決定(預設為<c>true</c> 62         /// </summary>
 63         public bool AllowRememberConsent { get; set; } = true;
 64         /// <summary>
 65         /// 當同時請求id令牌和訪問令牌時,是否應始終將使用者宣告新增到id令牌,而不是要求客戶端使用userinfo端點。 
 66         /// </summary>
 67         public bool AlwaysIncludeUserClaimsInIdToken { get; set; } = false;
 68         /// <summary>
 69         /// 是否需要驗證金鑰(預設為<c>true</c>)。
 70         /// </summary>
 71         public bool RequirePkce { get; set; } = true;
 72         /// <summary>
 73         /// 是否可以使用普通方法傳送驗證金鑰(不推薦,預設為<c>false</c> 74         /// </summary>
 75         public bool AllowPlainTextPkce { get; set; } = false;
 76         /// <summary>
 77         /// 是否必須在授權請求上使用請求物件(預設為<c>false</c> 78         /// </summary>
 79         public bool RequireRequestObject { get; set; }
 80         /// <summary>
 81         /// 控制是否通過此客戶端的瀏覽器傳輸訪問令牌(預設為<c>false</c>)。
 82         /// 當允許多種響應型別時,這可以防止訪問令牌的意外洩漏。
 83         /// </summary>
 84         public bool AllowAccessTokensViaBrowser { get; set; }
 85         /// <summary>
 86         /// 客戶端上基於HTTP前端通道的登出的登出URI。
 87         /// </summary>
 88         [MaxLength(512)]
 89         public string FrontChannelLogoutUri { get; set; }
 90         /// <summary>
 91         /// 是否應將使用者的會話id傳送到FrontChannelLogoutUri。預設值為<c>true</c> 92         /// </summary>
 93         public bool FrontChannelLogoutSessionRequired { get; set; } = true;
 94         /// <summary>
 95         /// 指定客戶端上基於HTTP反向通道的登出的登出URI。
 96         /// </summary>
 97         [MaxLength(512)]
 98         public string BackChannelLogoutUri { get; set; }
 99         /// <summary>
100         /// 是否應將使用者的會話id傳送到BackChannelLogoutUri。預設值為<c>true</c>
101         /// </summary>
102         public bool BackChannelLogoutSessionRequired { get; set; } = true;
103         /// <summary>
104         /// [是否允許離線訪問]。預設值為<c>false</c>105         /// </summary>
106         public bool AllowOfflineAccess { get; set; }
107         /// <summary>
108         /// 標識令牌的生存期(秒)(預設為300秒/5分鐘)
109         /// </summary>
110         public int IdentityTokenLifetime { get; set; } = 300;
111         /// <summary>
112         /// 身份令牌的簽名演算法。如果為空,將使用伺服器預設簽名演算法。
113         /// </summary>
114         [MaxLength(128)]
115         public string AllowedIdentityTokenSigningAlgorithms { get; set; }
116         /// <summary>
117         /// 訪問令牌的生存期(秒)(預設為3600秒/1小時)
118         /// </summary>
119         public int AccessTokenLifetime { get; set; } = 3600;
120         /// <summary>
121         /// 授權程式碼的生存期(秒)(預設為300秒/5分鐘)
122         /// </summary>
123         public int AuthorizationCodeLifetime { get; set; } = 300;
124         /// <summary>
125         /// 使用者同意的生存期(秒)。預設為null(無過期)
126         /// </summary>
127         public int? ConsentLifetime { get; set; } = null;
128         /// <summary>
129         /// 重新整理令牌的最長生存期(秒)。預設值為2592000秒/30天
130         /// </summary>
131         public int AbsoluteRefreshTokenLifetime { get; set; } = 2592000;
132         /// <summary>
133         /// 重新整理令牌的滑動生存期(秒)。預設為1296000秒/15天
134         /// </summary>
135         public int SlidingRefreshTokenLifetime { get; set; } = 1296000;
136         /// <summary>
137         /// 重用:重新整理令牌時,重新整理令牌控制程式碼將保持不變
138         /// 一次性:重新整理令牌時將更新重新整理令牌控制程式碼 
139         /// </summary>
140         public int RefreshTokenUsage { get; set; } = (int)TokenUsage.OneTimeOnly;
141         /// <summary>
142         /// 是否應在重新整理令牌請求時更新訪問令牌(及其宣告)。
143         /// 預設值為<c>false</c>144         /// </summary>
145         public bool UpdateAccessTokenClaimsOnRefresh { get; set; } = false;
146         /// <summary>
147         /// 絕對:重新整理令牌將在固定時間點過期(由絕對重新整理令牌生命週期指定)
148         /// 滑動:重新整理令牌時,重新整理令牌的生存期將被更新(按SlidingRefreshTokenLifetime中指定的數量)。壽命不會超過絕對壽命。
149         /// </summary>
150         public int RefreshTokenExpiration { get; set; } = (int)TokenExpiration.Absolute;
151         /// <summary>
152         /// 訪問令牌型別(預設為JWT)。
153         /// </summary>
154         public int AccessTokenType { get; set; } = 0; // AccessTokenType.Jwt;
155         /// <summary>
156         /// 客戶端是否允許本地登入。預設值為<c>true</c>157         /// </summary>
158         public bool EnableLocalLogin { get; set; } = true;
159         /// <summary>
160         /// JWT訪問令牌是否應包含識別符號。預設值為<c>true</c>161         /// </summary>
162         public bool IncludeJwtId { get; set; }
163         /// <summary>
164         /// 該值指示客戶端宣告應始終包含在訪問令牌中,還是僅包含在客戶端憑據流中。
165         /// 預設值為<c>false</c>
166         /// </summary>
167         public bool AlwaysSendClientClaims { get; set; }
168         /// <summary>
169         /// 客戶端宣告型別字首。預設為<c>client_</c>170         /// </summary>
171         [MaxLength(256)]
172         public string ClientClaimsPrefix { get; set; } = "client_";
173         /// <summary>
174         /// 此客戶端的使用者在成對主體生成中使用的salt值。
175         /// </summary>
176         [MaxLength(128)]
177         public string PairWiseSubjectSalt { get; set; }
178         /// <summary>
179         /// 自上次使用者身份驗證以來的最長持續時間(秒)。
180         /// </summary>
181         public int? UserSsoLifetime { get; set; }
182         /// <summary>
183         /// 裝置流使用者程式碼的型別。
184         /// </summary>
185         [MaxLength(128)]
186         public string UserCodeType { get; set; }
187         /// <summary>
188         /// 裝置程式碼生存期。
189         /// </summary>
190         public int DeviceCodeLifetime { get; set; } = 300;
191         /// <summary>
192         /// 建立時間
193         /// </summary>
194         public DateTime Created { get; set; } = DateTime.UtcNow;
195         /// <summary>
196         /// 更新時間
197         /// </summary>
198         public DateTime? Updated { get; set; }
199         /// <summary>
200         /// 最後訪問時間
201         /// </summary>
202         public DateTime? LastAccessed { get; set; }
203     }
204 }

下圖是所有實體類圖:

 

 

 

使用EFCore 6.0做好資料庫表結構遷移工作,在自定義的DbContext上下文中新增實體類

 1 using Microsoft.EntityFrameworkCore;
 2 using Pterosaur.Authorization.Domain.Entities;
 3 using Volo.Abp.Data;
 4 using Volo.Abp.DependencyInjection;
 5 using Volo.Abp.EntityFrameworkCore;
 6 
 7 namespace Pterosaur.Authorization.EntityFrameworkCore
 8 {
 9     [ConnectionStringName("Default")]
10     public class PterosaurDbContext : AbpDbContext<PterosaurDbContext>
11     {
12 
13         #region IdentityServer Entities from the modules
14         public DbSet<IdentityResourceProperty> IdentityResourceProperties { get; set; }
15         public DbSet<IdentityResourceClaim> IdentityResourceClaims { get; set; }
16         public DbSet<IdentityResource> IdentityResources { get; set; }
17         public DbSet<IdentityProvider> IdentityProviders { get; set; }
18         public DbSet<DeviceFlowCodes> DeviceFlowCodes { get; set; }
19         public DbSet<ApiScopeProperty> ApiScopeProperties { get; set; }
20         public DbSet<ApiScopeClaim> ApiScopeClaims { get; set; }
21         public DbSet<ApiScope> ApiScopes { get; set; }
22         public DbSet<ApiResourceSecret> ApiResourceSecrets { get; set; }
23         public DbSet<ApiResourceScope> ApiResourceScopes { get; set; }
24         public DbSet<ApiResourceProperty> ApiResourceProperties { get; set; }
25         public DbSet<ApiResourceClaim> ApiResourceClaims { get; set; }
26         public DbSet<ApiResource> ApiResources { get; set; }
27 
28         public DbSet<Client> Clients { get; set; }
29         public DbSet<ClientClaim> ClientClaims { get; set; }
30         public DbSet<ClientCorsOrigin> ClientCorsOrigins { get; set; }
31         public DbSet<ClientGrantType> ClientGrantTypes { get; set; }
32         public DbSet<ClientIdPRestriction> ClientIdPRestrictions { get; set; }
33         public DbSet<ClientPostLogoutRedirectUri> ClientPostLogoutRedirectUris { get; set; }
34         public DbSet<ClientProperty> ClientProperties { get; set; }
35         public DbSet<ClientRedirectUri> ClientRedirectUris { get; set; }
36         public DbSet<ClientScope> ClientScopes { get; set; }
37         public DbSet<ClientSecret> ClientSecrets { get; set; }
38         #endregion
39         
40         public PterosaurDbContext(DbContextOptions<PterosaurDbContext> options): base(options)
41         {
42 
43         }
44 
45         protected override void OnModelCreating(ModelBuilder builder)
46         {
47             builder.Seed();//此處構建種子資料
48             base.OnModelCreating(builder);
49         }
50     }
51 }

接下構建一條測試用的客戶端資訊種子資料

 1 using Microsoft.EntityFrameworkCore;
 2 using Pterosaur.Authorization.Domain.Entities;
 3 using System;
 4 
 5 namespace Pterosaur.Authorization.EntityFrameworkCore
 6 {
 7     public static class ModelBuilderExtensions
 8     {
 9         public static void Seed(this ModelBuilder modelBuilder)
10         {
11             var id = Guid.NewGuid();
12             modelBuilder.Entity<Client>().HasData(
13                 new Client(id) 
14                 {
15                     ClientId = "pterosaur.io",
16                     ClientName = "pterosaur.io",
17                     Description = "pterosaur.io"
18                 }
19             );
20 
21             modelBuilder.Entity<ClientSecret>().HasData(
22                 new ClientSecret(Guid.NewGuid())
23                 {
24                     ClientId= id,
25                     Created=DateTime.Now,
26                     Expiration=DateTime.Now.AddYears(10),
27                     Value= "pterosaur.io",
28                     Description = "pterosaur.io"
29                 }
30             );
31             modelBuilder.Entity<ClientScope>().HasData(
32                 new ClientScope(Guid.NewGuid())
33                 {
34                     ClientId = id,
35                     Scope="api"
36                 }
37            );
38         }
39     }
40 }

執行完資料庫遷移指令碼命令,就能看到資料庫表了

 

 

 接下來就是如何讓IdentityServer從資料庫讀取了,這裡我們需要實現幾個核心介面,這個參考了它本身的EFCore的實現,不過我想改造成適配Abp vnext的所以折騰了下:

IClientStore 介面: 客戶端儲存介面,實現了此介面IdentityServer就會從指定的實現去讀取客戶端資料,程式碼實現如下

 

 1 using Duende.IdentityServer.Models;
 2 using Duende.IdentityServer.Stores;
 3 using System;
 4 using System.Collections.Generic;
 5 using System.Linq;
 6 using System.Text;
 7 using System.Threading.Tasks;
 8 using Volo.Abp.Domain.Repositories;
 9 using Mapster;
10 using Volo.Abp.Uow;
11 
12 namespace Pterosaur.Authorization.Domain.Services.IdentityServer
13 {
14     public class ClientStoreManager : IClientStore
15     {
16         private readonly IClientManager _clientManager;
17         public ClientStoreManager(IClientManager clientManager)
18         {
19             _clientManager = clientManager;
20         }
21         public async Task<Client> FindClientByIdAsync(string clientId)
22         {
23             //
24             var client =await _clientManager.GetClientDetail(clientId);
25             if (client == null)
26             {
27                 return null;
28             }
29             var result = new Client();
30             TypeAdapter.Adapt(client, result);
31             result.AllowedCorsOrigins = client.ClientCorsOrigins.Select(c => c.Origin).ToList();
32             result.AllowedGrantTypes = client.ClientGrantTypes.Select(c => c.GrantType).ToList();
33             result.AllowedScopes = client.AllowedScopes.Select(c => c.Scope).ToList();
34             result.Claims = client.ClientClaims.Select(c => new ClientClaim() { Type = c.Type, Value = c.Value, ValueType = c.ValueType }).ToList();
35 
36             
37             result.ClientSecrets = client.ClientSecrets.Select(c => new Secret() { Description = c.Description, Expiration = c.Expiration, Type = c.Type, Value = c.Value.Sha256() }).ToList();
38             result.IdentityProviderRestrictions = client.ClientIdPRestrictions.Select(c => c.Provider).ToList();
39             result.PostLogoutRedirectUris = client.ClientPostLogoutRedirectUris.Select(c => c.PostLogoutRedirectUri).ToList();
40             result.Properties = client.ClientProperties.ToDictionary(c => c.Key, c => c.Value);
41             result.RedirectUris = client.ClientRedirectUris.Select(c => c.RedirectUri).ToList();
42             return result;
43         }
44     }
45 }

 

IResourceStore 介面: Api資源儲存介面,程式碼實現如下(程式碼其實有很多地方可以優化的,不過我想的是先實現功能先)

  1 using Duende.IdentityServer.Models;
  2 using Duende.IdentityServer.Services;
  3 using Duende.IdentityServer.Stores;
  4 using System;
  5 using System.Collections.Generic;
  6 using System.Linq;
  7 using System.Linq.Expressions;
  8 using System.Text;
  9 using System.Threading.Tasks;
 10 using Volo.Abp.Domain.Repositories;
 11 using Mapster;
 12 using Volo.Abp.Uow;
 13 
 14 namespace Pterosaur.Authorization.Domain.Services.IdentityServer
 15 {
 16     public class ResourceStoreManager : IResourceStore
 17     {
 18         //
 19         private readonly IApiResourceManager _apiResourceManager;
 20         private readonly IApiScopeManager _apiScopeManager;
 21         private readonly IIdentityResourceManager _identityResourceManager;
 22         public ResourceStoreManager(IApiResourceManager apiResourceManager, IApiScopeManager apiScopeManager, IIdentityResourceManager identityResourceManager)
 23         {
 24             _apiResourceManager = apiResourceManager;
 25             _apiScopeManager = apiScopeManager;
 26             _identityResourceManager= identityResourceManager;
 27         }
 28         /// <summary>
 29         /// 根據API資源名稱獲取API資源資料
 30         /// </summary>
 31         /// <param name="apiResourceNames"></param>
 32         /// <returns></returns>
 33         /// <exception cref="ArgumentNullException"></exception>
 34         public async Task<IEnumerable<ApiResource>> FindApiResourcesByNameAsync(IEnumerable<string> apiResourceNames)
 35         {
 36             if (apiResourceNames == null) throw new ArgumentNullException(nameof(apiResourceNames));
 37 
 38             var queryResult =await _apiResourceManager.GetApiResourcesAsync(x => apiResourceNames.Contains(x.Name));
 39 
 40             var apiResources = queryResult.Select(x => new ApiResource()
 41             {
 42                 Description = x.Description,
 43                 DisplayName = x.DisplayName,
 44                 Enabled = x.Enabled,
 45                 Name = x.Name,
 46                 RequireResourceIndicator = x.RequireResourceIndicator,
 47                 ShowInDiscoveryDocument = x.ShowInDiscoveryDocument,
 48                 ApiSecrets = x.Secrets.Where(sec => sec.ApiResourceId == x.Id).Select(sec => new Secret()
 49                 {
 50                     Description = sec.Description,
 51                     Expiration = sec.Expiration,
 52                     Type = sec.Type,
 53                     Value = sec.Value
 54                 }).ToList(),
 55                 
 56                 Scopes = x.Scopes.Where(sco => sco.ApiResourceId == x.Id).Select(x => x.Scope).ToList(),
 57                 UserClaims = x.UserClaims.Where(c => c.ApiResourceId == x.Id).Select(x => x.Type).ToList(),
 58                 Properties=x.Properties.ToDictionary(c=>c.Key,c=>c.Value)
 59             })
 60             .ToList();
 61             return apiResources;
 62         }
 63         /// <summary>
 64         /// 根據作用域名稱獲取API資源資料
 65         /// </summary>
 66         /// <param name="scopeNames"></param>
 67         /// <returns></returns>
 68         /// <exception cref="ArgumentNullException"></exception>
 69         public async Task<IEnumerable<ApiResource>> FindApiResourcesByScopeNameAsync(IEnumerable<string> scopeNames)
 70         {
 71             if (scopeNames == null) throw new ArgumentNullException(nameof(scopeNames));
 72             var queryResult = await _apiResourceManager.GetApiResourcesAsync(x => x.Scopes.Where(s => scopeNames.Contains(s.Scope)).Any());
 73 
 74             var apiResources = queryResult.Select(x => new ApiResource()
 75             {
 76                 Description = x.Description,
 77                 DisplayName = x.DisplayName,
 78                 Enabled = x.Enabled,
 79                 Name = x.Name,
 80                 RequireResourceIndicator = x.RequireResourceIndicator,
 81                 ShowInDiscoveryDocument = x.ShowInDiscoveryDocument,
 82                 ApiSecrets = x.Secrets.Where(sec => sec.ApiResourceId == x.Id).Select(sec => new Secret()
 83                 {
 84                     Description = sec.Description,
 85                     Expiration = sec.Expiration,
 86                     Type = sec.Type,
 87                     Value = sec.Value
 88                 }).ToList(),
 89 
 90                 Scopes = x.Scopes.Where(sco => sco.ApiResourceId == x.Id).Select(x => x.Scope).ToList(),
 91                 UserClaims = x.UserClaims.Where(c => c.ApiResourceId == x.Id).Select(x => x.Type).ToList(),
 92                 Properties = x.Properties.ToDictionary(c => c.Key, c => c.Value)
 93             })
 94             .ToList();
 95             return apiResources;
 96         }
 97         /// <summary>
 98         /// 根據作用域名稱獲取作用域資料
 99         /// </summary>
100         /// <param name="scopeNames"></param>
101         /// <returns></returns>
102         public async Task<IEnumerable<ApiScope>> FindApiScopesByNameAsync(IEnumerable<string> scopeNames)
103         {
104             var queryResult=await _apiScopeManager.GetApiScopesAsync(x => scopeNames.Contains(x.Name));
105             var apiScopes = queryResult
106                 .Select(x => new ApiScope()
107                 {
108                     Description = x.Description,
109                     Name = x.Name,
110                     DisplayName = x.DisplayName,
111                     Emphasize = x.Emphasize,
112                     Enabled = x.Enabled,
113                     Properties = x.Properties.Where(p => p.ScopeId == x.Id).ToList().ToDictionary(x => x.Key, x => x.Value),
114                     Required = x.Required,
115                     ShowInDiscoveryDocument = x.ShowInDiscoveryDocument,
116                     UserClaims = x.UserClaims.Where(c => c.ScopeId == x.Id).Select(c => c.Type).ToList()
117                 })
118                 .ToList();
119             
120             return apiScopes;
121         }
122 
123         public async Task<IEnumerable<IdentityResource>> FindIdentityResourcesByScopeNameAsync(IEnumerable<string> scopeNames)
124         {
125             //身份資源資料
126             var queryResult = await _identityResourceManager.GetIdentityResourcesAsync(x => scopeNames.Contains(x.Name));
127             
128             var identityResources = queryResult.Select(x => new IdentityResource()
129             {
130                 Description = x.Description,
131                 DisplayName = x.DisplayName,
132                 Emphasize = x.Emphasize,
133                 Enabled = x.Enabled,
134                 Name = x.Name,
135                 Required = x.Required,
136                 ShowInDiscoveryDocument = x.ShowInDiscoveryDocument,
137                 Properties = x.IdentityResourceProperties.Where(p => p.IdentityResourceId == x.Id).ToDictionary(x => x.Key, x => x.Value),
138                 UserClaims = x.IdentityResourceClaims.Where(c => c.IdentityResourceId == x.Id).Select(c => c.Type).ToList(),
139                 
140             })
141             .ToList();
142             return identityResources;
143         }
144         /// <summary>
145         /// 獲取所有資源資料
146         /// </summary>
147         /// <returns></returns>
148         public async Task<Resources> GetAllResourcesAsync()
149         {
150             //身份資源資料
151             var identityResourceQueryResult = await _identityResourceManager.GetIdentityResourcesAsync(null);
152 
153             var identityResources = identityResourceQueryResult.Select(x => new IdentityResource()
154             {
155                 Description = x.Description,
156                 DisplayName = x.DisplayName,
157                 Emphasize = x.Emphasize,
158                 Enabled = x.Enabled,
159                 Name = x.Name,
160                 Required = x.Required,
161                 ShowInDiscoveryDocument = x.ShowInDiscoveryDocument,
162                 Properties = x.IdentityResourceProperties.Where(p => p.IdentityResourceId == x.Id).ToDictionary(x => x.Key, x => x.Value),
163                 UserClaims = x.IdentityResourceClaims.Where(c => c.IdentityResourceId == x.Id).Select(c => c.Type).ToList(),
164 
165             })
166             .ToList();
167             //api資源資料
168             var apiResourceQueryResult = await _apiResourceManager.GetApiResourcesAsync(null);
169             var apiResources = apiResourceQueryResult.Select(x => new ApiResource()
170             {
171                 Description = x.Description,
172                 DisplayName = x.DisplayName,
173                 Enabled = x.Enabled,
174                 Name = x.Name,
175                 RequireResourceIndicator = x.RequireResourceIndicator,
176                 ShowInDiscoveryDocument = x.ShowInDiscoveryDocument,
177                 ApiSecrets = x.Secrets.Where(sec => sec.ApiResourceId == x.Id).Select(sec => new Secret()
178                 {
179                     Description = sec.Description,
180                     Expiration = sec.Expiration,
181                     Type = sec.Type,
182                     Value = sec.Value
183                 }).ToList(),
184 
185                 Scopes = x.Scopes.Where(sco => sco.ApiResourceId == x.Id).Select(x => x.Scope).ToList(),
186                 UserClaims = x.UserClaims.Where(c => c.ApiResourceId == x.Id).Select(x => x.Type).ToList(),
187                 Properties = x.Properties.ToDictionary(c => c.Key, c => c.Value)
188             })
189             .ToList();
190             //api作用域資料
191             var apiScopeQueryResult = await _apiScopeManager.GetApiScopesAsync(null);
192             var apiScopes = apiScopeQueryResult
193                 .Select(x => new ApiScope()
194                 {
195                     Description = x.Description,
196                     Name = x.Name,
197                     DisplayName = x.DisplayName,
198                     Emphasize = x.Emphasize,
199                     Enabled = x.Enabled,
200                     Properties = x.Properties.Where(p => p.ScopeId == x.Id).ToList().ToDictionary(x => x.Key, x => x.Value),
201                     Required = x.Required,
202                     ShowInDiscoveryDocument = x.ShowInDiscoveryDocument,
203                     UserClaims = x.UserClaims.Where(c => c.ScopeId == x.Id).Select(c => c.Type).ToList()
204                 })
205                 .ToList();
206             //返回結果
207             var result = new Resources(identityResources, apiResources, apiScopes);
208             return result;
209         }
210     }
211 }

IIdentityProviderStore 介面:身份資源儲存介面,程式碼實現如下(突然發現這個介面實現還沒把資料庫查詢剝離出去[捂臉]...臉呢...不重要...)

 1 using Duende.IdentityServer.Models;
 2 using Duende.IdentityServer.Stores;
 3 using Serilog;
 4 using System;
 5 using System.Collections.Generic;
 6 using System.Linq;
 7 using System.Text;
 8 using System.Threading.Tasks;
 9 using Volo.Abp.Domain.Repositories;
10 using Mapster;
11 using Volo.Abp.Uow;
12 
13 namespace Pterosaur.Authorization.Domain.Services.IdentityServer
14 {
15     public class IdentityProviderStoreManager: IIdentityProviderStore
16     {
17         private readonly IRepository<Entities.IdentityProvider> _repository;
18 
19         private readonly IUnitOfWorkManager _unitOfWorkManager;
20         public IdentityProviderStoreManager(IRepository<Entities.IdentityProvider> repository, IUnitOfWorkManager unitOfWorkManager)
21         {
22             _repository = repository;
23             _unitOfWorkManager = unitOfWorkManager;
24         }
25 
26         public async Task<IEnumerable<IdentityProviderName>> GetAllSchemeNamesAsync()
27         {
28             using var unitOfWork = _unitOfWorkManager.Begin();
29             var identityProviderNames = (await _repository.GetQueryableAsync()).Select(x => new IdentityProviderName
30             {
31                 Enabled = x.Enabled,
32                 Scheme = x.Scheme,
33                 DisplayName = x.DisplayName
34             })
35             .ToList();
36             return identityProviderNames;
37         }
38 
39         public async Task<IdentityProvider> GetBySchemeAsync(string scheme)
40         {
41             using var unitOfWork = _unitOfWorkManager.Begin();
42             var idp = (await _repository.GetQueryableAsync()).Where(x => x.Scheme == scheme)
43                 .SingleOrDefault(x => x.Scheme == scheme);
44             if (idp == null) return null;
45 
46             var result = MapIdp(idp);
47             if (result == null)
48             {
49                 Log.Error("Identity provider record found in database, but mapping failed for scheme {scheme} and protocol type {protocol}", idp.Scheme, idp.Type);
50             }
51             return result;
52         }
53         /// <summary>
54         /// Maps from the identity provider entity to identity provider model.
55         /// </summary>
56         /// <param name="idp"></param>
57         /// <returns></returns>
58         protected virtual IdentityProvider MapIdp(Entities.IdentityProvider idp)
59         {
60             if (idp.Type == "oidc")
61             {
62                 return new OidcProvider(TypeAdapter.Adapt<IdentityProvider>(idp));
63             }
64 
65             return null;
66         }
67     }
68 }

介面實現完成,還需要把介面實現注入到IdentityServer中去,我們建立一個IdentityServerBuilderExtensions的類

 1 using Pterosaur.Authorization.Domain.Services.IdentityServer;
 2 
 3 namespace Pterosaur.Authorization.Hosting
 4 {
 5     public static class IdentityServerBuilderExtensions
 6     {
 7         public static IIdentityServerBuilder AddConfigurationStore(
 8             this IIdentityServerBuilder builder)
 9         {
10             builder.AddClientStore<ClientStoreManager>();
11             builder.AddResourceStore<ResourceStoreManager>();
12             builder.AddIdentityProviderStore<IdentityProviderStoreManager>();
13             return builder;
14         }
15 
16     }
17 }

然後在Abp vnext專案啟動模組中新增IdentityServer中介軟體

1 //注入
2             var builder = context.Services.AddIdentityServer(options =>
3             {
4 
5             })
6             .AddConfigurationStore()
7             .AddSigningCredential(new X509Certificate2(Path.Combine(environment.WebRootPath, configuration.GetSection("IdentityServer:SigningCredentialPath").Value), configuration.GetSection("IdentityServer:SigningCredentialPassword").Value));

 

 

在Program啟動類中新增Abp vnext

 1 using Pterosaur.Authorization.Hosting;
 2 using Serilog;
 3 
 4 var builder = WebApplication.CreateBuilder(args);
 5 builder.Host
 6      .ConfigureLogging((context, logBuilder) =>
 7      {
 8          Log.Logger = new LoggerConfiguration()
 9           .Enrich.FromLogContext()
10           .WriteTo.Console()// 日誌輸出到控制檯
11           .MinimumLevel.Information()
12           .MinimumLevel.Override("Microsoft", Serilog.Events.LogEventLevel.Warning)
13           .CreateLogger();
14          logBuilder.AddSerilog(dispose: true);
15      })
16      .UseAutofac();
17 builder.Services.ReplaceConfiguration(builder.Configuration);
18 builder.Services.AddApplication<WebModule>();
19 
20 var app = builder.Build();
21 
22 app.InitializeApplication();
23 
24 app.MapGet("/", () => "Hello World!");
25 app.Run();

 

 到此第一部分結束,我們使用Postman發起請求看看,效果圖如下:

 

結尾附上Abp vnext 腳手架模板地址:

https://gitee.com/pterosaur-open/abp-template

專案還在繼續完善中,第一版的重點會放在功能實現上,程式碼優化和細節優化得排後面咯!

 

相關文章