原來看到很多示例都是基於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
專案還在繼續完善中,第一版的重點會放在功能實現上,程式碼優化和細節優化得排後面咯!