原来看到很多示例都是基于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. 1 #pragma warning disable 1591
  2. 2
  3. 3 using System;
  4. 4 using System.Collections.Generic;
  5. 5 using System.ComponentModel.DataAnnotations;
  6. 6 using Duende.IdentityServer.Models;
  7. 7 using Volo.Abp.Domain.Entities;
  8. 8
  9. 9 namespace Pterosaur.Authorization.Domain.Entities
  10. 10 {
  11. 11 public class Client: Entity<Guid>
  12. 12 {
  13. 13 public Client() { }
  14. 14 public Client(Guid id)
  15. 15 {
  16. 16 Id = id;
  17. 17 }
  18. 18 /// <summary>
  19. 19 /// 是否启用
  20. 20 /// </summary>
  21. 21 public bool Enabled { get; set; } = true;
  22. 22 /// <summary>
  23. 23 /// 客户端ID
  24. 24 /// </summary>
  25. 25 [MaxLength(128)]
  26. 26 public string ClientId { get; set; }
  27. 27 /// <summary>
  28. 28 /// 协议类型
  29. 29 /// </summary>
  30. 30 [MaxLength(64)]
  31. 31 public string ProtocolType { get; set; } = "oidc";
  32. 32 /// <summary>
  33. 33 /// 如果设置为false,则在令牌端点请求令牌时不需要客户端机密(默认为<c>true</c>)
  34. 34 /// </summary>
  35. 35 public bool RequireClientSecret { get; set; } = true;
  36. 36 /// <summary>
  37. 37 /// 客户端名
  38. 38 /// </summary>
  39. 39 [MaxLength(128)]
  40. 40 public string ClientName { get; set; }
  41. 41 /// <summary>
  42. 42 /// 描述
  43. 43 /// </summary>
  44. 44 [MaxLength(1024)]
  45. 45 public string Description { get; set; }
  46. 46 /// <summary>
  47. 47 /// 客户端地址
  48. 48 /// </summary>
  49. 49 [MaxLength(256)]
  50. 50 public string ClientUri { get; set; }
  51. 51 /// <summary>
  52. 52 /// 客户端LGOGO地址
  53. 53 /// </summary>
  54. 54 [MaxLength(512)]
  55. 55 public string LogoUri { get; set; }
  56. 56 /// <summary>
  57. 57 /// 指定是否需要同意屏幕(默认为<c>false</c>)
  58. 58 /// </summary>
  59. 59 public bool RequireConsent { get; set; } = false;
  60. 60 /// <summary>
  61. 61 /// 指定用户是否可以选择存储同意决定(默认为<c>true</c>)
  62. 62 /// </summary>
  63. 63 public bool AllowRememberConsent { get; set; } = true;
  64. 64 /// <summary>
  65. 65 /// 当同时请求id令牌和访问令牌时,是否应始终将用户声明添加到id令牌,而不是要求客户端使用userinfo端点。
  66. 66 /// </summary>
  67. 67 public bool AlwaysIncludeUserClaimsInIdToken { get; set; } = false;
  68. 68 /// <summary>
  69. 69 /// 是否需要验证密钥(默认为<c>true</c>)。
  70. 70 /// </summary>
  71. 71 public bool RequirePkce { get; set; } = true;
  72. 72 /// <summary>
  73. 73 /// 是否可以使用普通方法发送验证密钥(不推荐,默认为<c>false</c>)
  74. 74 /// </summary>
  75. 75 public bool AllowPlainTextPkce { get; set; } = false;
  76. 76 /// <summary>
  77. 77 /// 是否必须在授权请求上使用请求对象(默认为<c>false</c>)
  78. 78 /// </summary>
  79. 79 public bool RequireRequestObject { get; set; }
  80. 80 /// <summary>
  81. 81 /// 控制是否通过此客户端的浏览器传输访问令牌(默认为<c>false</c>)。
  82. 82 /// 当允许多种响应类型时,这可以防止访问令牌的意外泄漏。
  83. 83 /// </summary>
  84. 84 public bool AllowAccessTokensViaBrowser { get; set; }
  85. 85 /// <summary>
  86. 86 /// 客户端上基于HTTP前端通道的注销的注销URI。
  87. 87 /// </summary>
  88. 88 [MaxLength(512)]
  89. 89 public string FrontChannelLogoutUri { get; set; }
  90. 90 /// <summary>
  91. 91 /// 是否应将用户的会话id发送到FrontChannelLogoutUri。默认值为<c>true</c>。
  92. 92 /// </summary>
  93. 93 public bool FrontChannelLogoutSessionRequired { get; set; } = true;
  94. 94 /// <summary>
  95. 95 /// 指定客户端上基于HTTP反向通道的注销的注销URI。
  96. 96 /// </summary>
  97. 97 [MaxLength(512)]
  98. 98 public string BackChannelLogoutUri { get; set; }
  99. 99 /// <summary>
  100. 100 /// 是否应将用户的会话id发送到BackChannelLogoutUri。默认值为<c>true</c>
  101. 101 /// </summary>
  102. 102 public bool BackChannelLogoutSessionRequired { get; set; } = true;
  103. 103 /// <summary>
  104. 104 /// [是否允许脱机访问]。默认值为<c>false</c>。
  105. 105 /// </summary>
  106. 106 public bool AllowOfflineAccess { get; set; }
  107. 107 /// <summary>
  108. 108 /// 标识令牌的生存期(秒)(默认为300秒/5分钟)
  109. 109 /// </summary>
  110. 110 public int IdentityTokenLifetime { get; set; } = 300;
  111. 111 /// <summary>
  112. 112 /// 身份令牌的签名算法。如果为空,将使用服务器默认签名算法。
  113. 113 /// </summary>
  114. 114 [MaxLength(128)]
  115. 115 public string AllowedIdentityTokenSigningAlgorithms { get; set; }
  116. 116 /// <summary>
  117. 117 /// 访问令牌的生存期(秒)(默认为3600秒/1小时)
  118. 118 /// </summary>
  119. 119 public int AccessTokenLifetime { get; set; } = 3600;
  120. 120 /// <summary>
  121. 121 /// 授权代码的生存期(秒)(默认为300秒/5分钟)
  122. 122 /// </summary>
  123. 123 public int AuthorizationCodeLifetime { get; set; } = 300;
  124. 124 /// <summary>
  125. 125 /// 用户同意的生存期(秒)。默认为null(无过期)
  126. 126 /// </summary>
  127. 127 public int? ConsentLifetime { get; set; } = null;
  128. 128 /// <summary>
  129. 129 /// 刷新令牌的最长生存期(秒)。默认值为2592000秒/30天
  130. 130 /// </summary>
  131. 131 public int AbsoluteRefreshTokenLifetime { get; set; } = 2592000;
  132. 132 /// <summary>
  133. 133 /// 刷新令牌的滑动生存期(秒)。默认为1296000秒/15天
  134. 134 /// </summary>
  135. 135 public int SlidingRefreshTokenLifetime { get; set; } = 1296000;
  136. 136 /// <summary>
  137. 137 /// 重用:刷新令牌时,刷新令牌句柄将保持不变
  138. 138 /// 一次性:刷新令牌时将更新刷新令牌句柄
  139. 139 /// </summary>
  140. 140 public int RefreshTokenUsage { get; set; } = (int)TokenUsage.OneTimeOnly;
  141. 141 /// <summary>
  142. 142 /// 是否应在刷新令牌请求时更新访问令牌(及其声明)。
  143. 143 /// 默认值为<c>false</c>。
  144. 144 /// </summary>
  145. 145 public bool UpdateAccessTokenClaimsOnRefresh { get; set; } = false;
  146. 146 /// <summary>
  147. 147 /// 绝对:刷新令牌将在固定时间点过期(由绝对刷新令牌生命周期指定)
  148. 148 /// 滑动:刷新令牌时,刷新令牌的生存期将被更新(按SlidingRefreshTokenLifetime中指定的数量)。寿命不会超过绝对寿命。
  149. 149 /// </summary>
  150. 150 public int RefreshTokenExpiration { get; set; } = (int)TokenExpiration.Absolute;
  151. 151 /// <summary>
  152. 152 /// 访问令牌类型(默认为JWT)。
  153. 153 /// </summary>
  154. 154 public int AccessTokenType { get; set; } = 0; // AccessTokenType.Jwt;
  155. 155 /// <summary>
  156. 156 /// 客户端是否允许本地登录。默认值为<c>true</c>。
  157. 157 /// </summary>
  158. 158 public bool EnableLocalLogin { get; set; } = true;
  159. 159 /// <summary>
  160. 160 /// JWT访问令牌是否应包含标识符。默认值为<c>true</c>。
  161. 161 /// </summary>
  162. 162 public bool IncludeJwtId { get; set; }
  163. 163 /// <summary>
  164. 164 /// 该值指示客户端声明应始终包含在访问令牌中,还是仅包含在客户端凭据流中。
  165. 165 /// 默认值为<c>false</c>
  166. 166 /// </summary>
  167. 167 public bool AlwaysSendClientClaims { get; set; }
  168. 168 /// <summary>
  169. 169 /// 客户端声明类型前缀。默认为<c>client_</c>。
  170. 170 /// </summary>
  171. 171 [MaxLength(256)]
  172. 172 public string ClientClaimsPrefix { get; set; } = "client_";
  173. 173 /// <summary>
  174. 174 /// 此客户端的用户在成对主体生成中使用的salt值。
  175. 175 /// </summary>
  176. 176 [MaxLength(128)]
  177. 177 public string PairWiseSubjectSalt { get; set; }
  178. 178 /// <summary>
  179. 179 /// 自上次用户身份验证以来的最长持续时间(秒)。
  180. 180 /// </summary>
  181. 181 public int? UserSsoLifetime { get; set; }
  182. 182 /// <summary>
  183. 183 /// 设备流用户代码的类型。
  184. 184 /// </summary>
  185. 185 [MaxLength(128)]
  186. 186 public string UserCodeType { get; set; }
  187. 187 /// <summary>
  188. 188 /// 设备代码生存期。
  189. 189 /// </summary>
  190. 190 public int DeviceCodeLifetime { get; set; } = 300;
  191. 191 /// <summary>
  192. 192 /// 创建时间
  193. 193 /// </summary>
  194. 194 public DateTime Created { get; set; } = DateTime.UtcNow;
  195. 195 /// <summary>
  196. 196 /// 更新时间
  197. 197 /// </summary>
  198. 198 public DateTime? Updated { get; set; }
  199. 199 /// <summary>
  200. 200 /// 最后访问时间
  201. 201 /// </summary>
  202. 202 public DateTime? LastAccessed { get; set; }
  203. 203 }
  204. 204 }

下图是所有实体类图:

使用EFCore 6.0做好数据库表结构迁移工作,在自定义的DbContext上下文中添加实体类

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

接下构建一条测试用的客户端信息种子数据

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

执行完数据库迁移脚本命令,就能看到数据库表了

接下来就是如何让IdentityServer从数据库读取了,这里我们需要实现几个核心接口,这个参考了它本身的EFCore的实现,不过我想改造成适配Abp vnext的所以折腾了下:

IClientStore 接口: 客户端存储接口,实现了此接口IdentityServer就会从指定的实现去读取客户端数据,代码实现如下

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

IResourceStore 接口: Api资源存储接口,代码实现如下(代码其实有很多地方可以优化的,不过我想的是先实现功能先)

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

IIdentityProviderStore 接口:身份资源存储接口,代码实现如下(突然发现这个接口实现还没把数据库查询剥离出去[捂脸]...脸呢...不重要...)

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

接口实现完成,还需要把接口实现注入到IdentityServer中去,我们创建一个IdentityServerBuilderExtensions的类

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

然后在Abp vnext项目启动模块中添加IdentityServer中间件

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

在Program启动类中添加Abp vnext

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

到此第一部分结束,我们使用Postman发起请求看看,效果图如下:

结尾附上Abp vnext 脚手架模板地址:

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

项目还在继续完善中,第一版的重点会放在功能实现上,代码优化和细节优化得排后面咯!

使用Abp vnext构建基于Duende.IdentityServer的统一授权中心(一)的更多相关文章

  1. 手把手教你用Abp vnext构建API接口服务

    ABP是一个开源应用程序框架,该项目是ASP.NET Boilerplate Web应用程序框架的下一代,专注于基于ASP.NET Core的Web应用程序开发,也支持开发控制台应用程序. 官方网站: ...

  2. .NET Core微服务之基于Ocelot+IdentityServer实现统一验证与授权

    Tip: 此篇已加入.NET Core微服务基础系列文章索引 一.案例结构总览 这里,假设我们有两个客户端(一个Web网站,一个移动App),他们要使用系统,需要通过API网关(这里API网关始终作为 ...

  3. ABP vnext模块化架构的最佳实践的实现

    在上一篇文章<手把手教你用Abp vnext构建API接口服务>中,我们用ABP vnext实现了WebAPI接口服务,但是并非ABP模块化架构的最佳实践.我本身也在学习ABP,我认为AB ...

  4. 构建基于分布式SOA架构的统一身份认证体系

    摘要:本文充分利用SOA架构松耦合的特点,通过规范统一网络接口实现业务系统整合,既提升系统安全性,又简化资源访问操作,具有重要的理论和现实意义. 统一身份认证旨在将分散在各个信息系统中的用户和权限资源 ...

  5. [WCF安全3]使用wsHttpBinding构建基于SSL与UserName授权的WCF应用程序

    上一篇文章中介绍了如何使用wsHttpBinding构建UserName授权的WCF应用程序,本文将为您介绍如何使用wsHttpBinding构建基于SSL的UserName安全授权的WCF应用程序. ...

  6. 基于ZK构建统一配置中心的方案和实践

    背景: 近期使用Zk实现了一个简单的配置管理的小东西,在此开源出来,有兴趣的希望提出您的宝贵意见.如果恰巧您也使用或者接触过类似的东西, 也希望您可以分享下您觉得现在这个项目可以优化和改进的地方. 项 ...

  7. 基于 abp vNext 微服务开发的敏捷应用构建平台 - 项目介绍

    缘起 目前使用ABP框架已经将近3年了,大大小小的项目也陆陆续续做了很多.由于现有信息系统的架构模式是在底层的技术平台上直接构建信息系统并采用技术主导,使用业务无关的编程工具来开发信息系统的缺陷使得系 ...

  8. 基于 abp vNext 微服务开发的敏捷应用构建平台 - 文章目录

    系列文章: <基于 abp vNext 微服务开发的敏捷应用构建平台 - 设计构想> [点击查看] <基于 abp vNext 微服务开发的敏捷应用构建平台 - 文章目录> [ ...

  9. 基于 abp vNext 微服务开发的敏捷应用构建平台 - 框架分析

    总体架构     本平台从技术上采用ABP vNext和.NET Core编写的微服务架构.客户端层主要以现代浏览器为主,适配了PC端和移动端的访问,采用API和应用程序进行交互,同时提供第三方使用的 ...

随机推荐

  1. django如何加载外部文件

    django如何加载外部文件(环境:pycharm python2.7 django1.11) 有一份新的文件夹名为:py_aiplat_demo,内含有多个文件夹(SDK,demo,data). 1 ...

  2. nginx负载轮询

    下面是一个可以使用nginx负载轮询,如果有一台服务器连接不通,返404,500,502,503,504,会自动切换到下一台服务器 upstream www { server 111.111.111. ...

  3. P4983-忘情【wqs二分,斜率优化】

    正题 题目链接:https://www.luogu.com.cn/problem/P4983 题目大意 给出长度为\(n\)的序列\(x\),记平均数为\(\bar{x}\),要求将序列分成\(m\) ...

  4. Liunx下Mysql,MongoDB性能优化的配置

    场景 这几天在赶十一上线的项目,但是突然发现接口性能不好,高并发支持不住.又不想改代码,就在数据库层面进行优化. Mysql 分区:项目中有对40万条的数据进行时间查询的要求,就算对DateTime建 ...

  5. HTML(思维导图)

  6. python paramiko实现ssh上传下载执行命令

    paramiko ssh上传下载执行命令 序言 最近项目经常需要动态在跳板机上登录服务器进行部署环境,且服务器比较多,每次完成所有服务器到环境部署执行耗费大量时间.为了解决这个问题,根据所学的执行实现 ...

  7. 低代码BPM平台

    为了做出明智的决策并为客户提供服务,员工需要在正确的环境中使用正确的工具和访问关键信息的权限.但是,当业务关键信息分散在多个现成的和自定义编码的应用程序中时,员工效率会降低,客户体验也会受到影响. 低 ...

  8. 遇到括号就是栈(bushi)

    CF508E Arthur and Brackets 我在赛场上想都没想直接DP \(O(n^3)\)过了 但别人说正解是栈+贪心 讲讲DP \(bool\) \(dp[i][j]\)表示从第i对括号 ...

  9. 使用 PyTorch Lightning 将深度学习管道速度提高 10 倍

    ​  前言  本文介绍了如何使用 PyTorch Lightning 构建高效且快速的深度学习管道,主要包括有为什么优化深度学习管道很重要.使用 PyTorch Lightning 加快实验周期的六种 ...

  10. javascript-jquery对象的属性处理

    1.attr()方法:获取元素某个属性的值. $("img").attr("title");//获得第一个<img>元素的title属性 $(&qu ...