原来看到很多示例都是基于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
项目还在继续完善中,第一版的重点会放在功能实现上,代码优化和细节优化得排后面咯!