使用Abp|使用Abp vnext构建基于Duende.IdentityServer的统一授权中心(一)

原来看到很多示例都是基于IdentityServer4的统一授权中心,但是IdentityServer4维护到2022年就不再进行更新维护了,所以我选择了它的升级版Duende.IdentityServer(这个有总营收超过100W美金就需要付费的限制).
整个授权中心完成我打算分成4个部分去构建整个项目,争取在12月中旬全部完成.
第一部分(已完成):与Abp vnext进行整合,实现数据库存储,并且能够正常颁发token
第二部分(构建中):实现可视化管理后台
第三部分(未开始):实现自定义账户体系,单点登录等...
第四部分(未开始):接入网关(我还另外整了一个基于Yarp的简单网关)
注:基于Yarp的网关项目以及统一授权中心在我完成第二部分的构建时会开源出来(并不包含Duende.IdentityServer本身)
接下来讲解第一部分的实现
【使用Abp|使用Abp vnext构建基于Duende.IdentityServer的统一授权中心(一)】下图是我的解决方案(我没有使用默认的Abp vnext生成的项目模板,而是我在去掉ABP默认的模块后保留了自己觉得已经适用的基础模块创建的模板):
使用Abp|使用Abp vnext构建基于Duende.IdentityServer的统一授权中心(一)
文章图片

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

下图是所有实体类图:
使用Abp|使用Abp vnext构建基于Duende.IdentityServer的统一授权中心(一)
文章图片




使用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")] 10public class PterosaurDbContext : AbpDbContext11{ 12 13#region IdentityServer Entities from the modules 14public DbSet IdentityResourceProperties { get; set; } 15public DbSet IdentityResourceClaims { get; set; } 16public DbSet IdentityResources { get; set; } 17public DbSet IdentityProviders { get; set; } 18public DbSet DeviceFlowCodes { get; set; } 19public DbSet ApiScopeProperties { get; set; } 20public DbSet ApiScopeClaims { get; set; } 21public DbSet ApiScopes { get; set; } 22public DbSet ApiResourceSecrets { get; set; } 23public DbSet ApiResourceScopes { get; set; } 24public DbSet ApiResourceProperties { get; set; } 25public DbSet ApiResourceClaims { get; set; } 26public DbSet ApiResources { get; set; } 27 28public DbSet Clients { get; set; } 29public DbSet ClientClaims { get; set; } 30public DbSet ClientCorsOrigins { get; set; } 31public DbSet ClientGrantTypes { get; set; } 32public DbSet ClientIdPRestrictions { get; set; } 33public DbSet ClientPostLogoutRedirectUris { get; set; } 34public DbSet ClientProperties { get; set; } 35public DbSet ClientRedirectUris { get; set; } 36public DbSet ClientScopes { get; set; } 37public DbSet ClientSecrets { get; set; } 38#endregion 39 40public PterosaurDbContext(DbContextOptions options): base(options) 41{ 42 43} 44 45protected override void OnModelCreating(ModelBuilder builder) 46{ 47builder.Seed(); //此处构建种子数据 48base.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 { 7public static class ModelBuilderExtensions 8{ 9public static void Seed(this ModelBuilder modelBuilder) 10{ 11var id = Guid.NewGuid(); 12modelBuilder.Entity().HasData( 13new Client(id) 14{ 15ClientId = "pterosaur.io", 16ClientName = "pterosaur.io", 17Description = "pterosaur.io" 18} 19); 20 21modelBuilder.Entity().HasData( 22new ClientSecret(Guid.NewGuid()) 23{ 24ClientId= id, 25Created=DateTime.Now, 26Expiration=DateTime.Now.AddYears(10), 27Value= "https://www.it610.com/article/pterosaur.io", 28Description = "pterosaur.io" 29} 30); 31modelBuilder.Entity().HasData( 32new ClientScope(Guid.NewGuid()) 33{ 34ClientId = id, 35Scope="api" 36} 37); 38} 39} 40 }

执行完数据库迁移脚本命令,就能看到数据库表了
使用Abp|使用Abp vnext构建基于Duende.IdentityServer的统一授权中心(一)
文章图片



接下来就是如何让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 { 14public class ClientStoreManager : IClientStore 15{ 16private readonly IClientManager _clientManager; 17public ClientStoreManager(IClientManager clientManager) 18{ 19_clientManager = clientManager; 20} 21public async Task FindClientByIdAsync(string clientId) 22{ 23// 24var client =await _clientManager.GetClientDetail(clientId); 25if (client == null) 26{ 27return null; 28} 29var result = new Client(); 30TypeAdapter.Adapt(client, result); 31result.AllowedCorsOrigins = client.ClientCorsOrigins.Select(c => c.Origin).ToList(); 32result.AllowedGrantTypes = client.ClientGrantTypes.Select(c => c.GrantType).ToList(); 33result.AllowedScopes = client.AllowedScopes.Select(c => c.Scope).ToList(); 34result.Claims = client.ClientClaims.Select(c => new ClientClaim() { Type = c.Type, Value = https://www.it610.com/article/c.Value, ValueType = c.ValueType }).ToList(); 35 36 37result.ClientSecrets = client.ClientSecrets.Select(c => new Secret() { Description = c.Description, Expiration = c.Expiration, Type = c.Type, Value = https://www.it610.com/article/c.Value.Sha256() }).ToList(); 38result.IdentityProviderRestrictions = client.ClientIdPRestrictions.Select(c => c.Provider).ToList(); 39result.PostLogoutRedirectUris = client.ClientPostLogoutRedirectUris.Select(c => c.PostLogoutRedirectUri).ToList(); 40result.Properties = client.ClientProperties.ToDictionary(c => c.Key, c => c.Value); 41result.RedirectUris = client.ClientRedirectUris.Select(c => c.RedirectUri).ToList(); 42return 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 { 16public class ResourceStoreManager : IResourceStore 17{ 18// 19private readonly IApiResourceManager _apiResourceManager; 20private readonly IApiScopeManager _apiScopeManager; 21private readonly IIdentityResourceManager _identityResourceManager; 22public ResourceStoreManager(IApiResourceManager apiResourceManager, IApiScopeManager apiScopeManager, IIdentityResourceManager identityResourceManager) 23{ 24_apiResourceManager = apiResourceManager; 25_apiScopeManager = apiScopeManager; 26_identityResourceManager= identityResourceManager; 27} 28/// 29/// 根据API资源名称获取API资源数据 30/// 31/// 32/// 33/// 34public async Task FindApiResourcesByNameAsync(IEnumerable apiResourceNames) 35{ 36if (apiResourceNames == null) throw new ArgumentNullException(nameof(apiResourceNames)); 37 38var queryResult =await _apiResourceManager.GetApiResourcesAsync(x => apiResourceNames.Contains(x.Name)); 39 40var apiResources = queryResult.Select(x => new ApiResource() 41{ 42Description = x.Description, 43DisplayName = x.DisplayName, 44Enabled = x.Enabled, 45Name = x.Name, 46RequireResourceIndicator = x.RequireResourceIndicator, 47ShowInDiscoveryDocument = x.ShowInDiscoveryDocument, 48ApiSecrets = x.Secrets.Where(sec => sec.ApiResourceId == x.Id).Select(sec => new Secret() 49{ 50Description = sec.Description, 51Expiration = sec.Expiration, 52Type = sec.Type, 53Value = https://www.it610.com/article/sec.Value 54}).ToList(), 55 56Scopes = x.Scopes.Where(sco => sco.ApiResourceId == x.Id).Select(x => x.Scope).ToList(), 57UserClaims = x.UserClaims.Where(c => c.ApiResourceId == x.Id).Select(x => x.Type).ToList(), 58Properties=x.Properties.ToDictionary(c=>c.Key,c=>c.Value) 59}) 60.ToList(); 61return apiResources; 62} 63/// 64/// 根据作用域名称获取API资源数据 65/// 66/// 67/// 68/// 69public async Task FindApiResourcesByScopeNameAsync(IEnumerable scopeNames) 70{ 71if (scopeNames == null) throw new ArgumentNullException(nameof(scopeNames)); 72var queryResult = await _apiResourceManager.GetApiResourcesAsync(x => x.Scopes.Where(s => scopeNames.Contains(s.Scope)).Any()); 73 74var apiResources = queryResult.Select(x => new ApiResource() 75{ 76Description = x.Description, 77DisplayName = x.DisplayName, 78Enabled = x.Enabled, 79Name = x.Name, 80RequireResourceIndicator = x.RequireResourceIndicator, 81ShowInDiscoveryDocument = x.ShowInDiscoveryDocument, 82ApiSecrets = x.Secrets.Where(sec => sec.ApiResourceId == x.Id).Select(sec => new Secret() 83{ 84Description = sec.Description, 85Expiration = sec.Expiration, 86Type = sec.Type, 87Value = https://www.it610.com/article/sec.Value 88}).ToList(), 89 90Scopes = x.Scopes.Where(sco => sco.ApiResourceId == x.Id).Select(x => x.Scope).ToList(), 91UserClaims = x.UserClaims.Where(c => c.ApiResourceId == x.Id).Select(x => x.Type).ToList(), 92Properties = x.Properties.ToDictionary(c => c.Key, c => c.Value) 93}) 94.ToList(); 95return apiResources; 96} 97/// 98/// 根据作用域名称获取作用域数据 99/// 100/// 101/// 102public async Task FindApiScopesByNameAsync(IEnumerable scopeNames) 103{ 104var queryResult=await _apiScopeManager.GetApiScopesAsync(x => scopeNames.Contains(x.Name)); 105var apiScopes = queryResult 106.Select(x => new ApiScope() 107{ 108Description = x.Description, 109Name = x.Name, 110DisplayName = x.DisplayName, 111Emphasize = x.Emphasize, 112Enabled = x.Enabled, 113Properties = x.Properties.Where(p => p.ScopeId == x.Id).ToList().ToDictionary(x => x.Key, x => x.Value), 114Required = x.Required, 115ShowInDiscoveryDocument = x.ShowInDiscoveryDocument, 116UserClaims = x.UserClaims.Where(c => c.ScopeId == x.Id).Select(c => c.Type).ToList() 117}) 118.ToList(); 119 120return apiScopes; 121} 122 123public async Task> FindIdentityResourcesByScopeNameAsync(IEnumerable scopeNames) 124{ 125//身份资源数据 126var queryResult = await _identityResourceManager.GetIdentityResourcesAsync(x => scopeNames.Contains(x.Name)); 127 128var identityResources = queryResult.Select(x => new IdentityResource() 129{ 130Description = x.Description, 131DisplayName = x.DisplayName, 132Emphasize = x.Emphasize, 133Enabled = x.Enabled, 134Name = x.Name, 135Required = x.Required, 136ShowInDiscoveryDocument = x.ShowInDiscoveryDocument, 137Properties = x.IdentityResourceProperties.Where(p => p.IdentityResourceId == x.Id).ToDictionary(x => x.Key, x => x.Value), 138UserClaims = x.IdentityResourceClaims.Where(c => c.IdentityResourceId == x.Id).Select(c => c.Type).ToList(), 139 140}) 141.ToList(); 142return identityResources; 143} 144/// 145/// 获取所有资源数据 146/// 147/// 148public async Task GetAllResourcesAsync() 149{ 150//身份资源数据 151var identityResourceQueryResult = await _identityResourceManager.GetIdentityResourcesAsync(null); 152 153var identityResources = identityResourceQueryResult.Select(x => new IdentityResource() 154{ 155Description = x.Description, 156DisplayName = x.DisplayName, 157Emphasize = x.Emphasize, 158Enabled = x.Enabled, 159Name = x.Name, 160Required = x.Required, 161ShowInDiscoveryDocument = x.ShowInDiscoveryDocument, 162Properties = x.IdentityResourceProperties.Where(p => p.IdentityResourceId == x.Id).ToDictionary(x => x.Key, x => x.Value), 163UserClaims = x.IdentityResourceClaims.Where(c => c.IdentityResourceId == x.Id).Select(c => c.Type).ToList(), 164 165}) 166.ToList(); 167//api资源数据 168var apiResourceQueryResult = await _apiResourceManager.GetApiResourcesAsync(null); 169var apiResources = apiResourceQueryResult.Select(x => new ApiResource() 170{ 171Description = x.Description, 172DisplayName = x.DisplayName, 173Enabled = x.Enabled, 174Name = x.Name, 175RequireResourceIndicator = x.RequireResourceIndicator, 176ShowInDiscoveryDocument = x.ShowInDiscoveryDocument, 177ApiSecrets = x.Secrets.Where(sec => sec.ApiResourceId == x.Id).Select(sec => new Secret() 178{ 179Description = sec.Description, 180Expiration = sec.Expiration, 181Type = sec.Type, 182Value = https://www.it610.com/article/sec.Value 183}).ToList(), 184 185Scopes = x.Scopes.Where(sco => sco.ApiResourceId == x.Id).Select(x => x.Scope).ToList(), 186UserClaims = x.UserClaims.Where(c => c.ApiResourceId == x.Id).Select(x => x.Type).ToList(), 187Properties = x.Properties.ToDictionary(c => c.Key, c => c.Value) 188}) 189.ToList(); 190//api作用域数据 191var apiScopeQueryResult = await _apiScopeManager.GetApiScopesAsync(null); 192var apiScopes = apiScopeQueryResult 193.Select(x => new ApiScope() 194{ 195Description = x.Description, 196Name = x.Name, 197DisplayName = x.DisplayName, 198Emphasize = x.Emphasize, 199Enabled = x.Enabled, 200Properties = x.Properties.Where(p => p.ScopeId == x.Id).ToList().ToDictionary(x => x.Key, x => x.Value), 201Required = x.Required, 202ShowInDiscoveryDocument = x.ShowInDiscoveryDocument, 203UserClaims = x.UserClaims.Where(c => c.ScopeId == x.Id).Select(c => c.Type).ToList() 204}) 205.ToList(); 206//返回结果 207var result = new Resources(identityResources, apiResources, apiScopes); 208return 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 { 15public class IdentityProviderStoreManager: IIdentityProviderStore 16{ 17private readonly IRepository _repository; 18 19private readonly IUnitOfWorkManager _unitOfWorkManager; 20public IdentityProviderStoreManager(IRepository repository, IUnitOfWorkManager unitOfWorkManager) 21{ 22_repository = repository; 23_unitOfWorkManager = unitOfWorkManager; 24} 25 26public async Task> GetAllSchemeNamesAsync() 27{ 28using var unitOfWork = _unitOfWorkManager.Begin(); 29var identityProviderNames = (await _repository.GetQueryableAsync()).Select(x => new IdentityProviderName 30{ 31Enabled = x.Enabled, 32Scheme = x.Scheme, 33DisplayName = x.DisplayName 34}) 35.ToList(); 36return identityProviderNames; 37} 38 39public async Task GetBySchemeAsync(string scheme) 40{ 41using var unitOfWork = _unitOfWorkManager.Begin(); 42var idp = (await _repository.GetQueryableAsync()).Where(x => x.Scheme == scheme) 43.SingleOrDefault(x => x.Scheme == scheme); 44if (idp == null) return null; 45 46var result = MapIdp(idp); 47if (result == null) 48{ 49Log.Error("Identity provider record found in database, but mapping failed for scheme {scheme} and protocol type {protocol}", idp.Scheme, idp.Type); 50} 51return result; 52} 53/// 54/// Maps from the identity provider entity to identity provider model. 55/// 56/// 57/// 58protected virtual IdentityProvider MapIdp(Entities.IdentityProvider idp) 59{ 60if (idp.Type == "oidc") 61{ 62return new OidcProvider(TypeAdapter.Adapt(idp)); 63} 64 65return null; 66} 67} 68 }

接口实现完成,还需要把接口实现注入到IdentityServer中去,我们创建一个IdentityServerBuilderExtensions的类
1 using Pterosaur.Authorization.Domain.Services.IdentityServer; 2 3 namespace Pterosaur.Authorization.Hosting 4 { 5public static class IdentityServerBuilderExtensions 6{ 7public static IIdentityServerBuilder AddConfigurationStore( 8this IIdentityServerBuilder builder) 9{ 10builder.AddClientStore(); 11builder.AddResourceStore(); 12builder.AddIdentityProviderStore(); 13return builder; 14} 15 16} 17 }

然后在Abp vnext项目启动模块中添加IdentityServer中间件
1 //注入 2var 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));


使用Abp|使用Abp vnext构建基于Duende.IdentityServer的统一授权中心(一)
文章图片


在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{ 8Log.Logger = new LoggerConfiguration() 9.Enrich.FromLogContext() 10.WriteTo.Console()// 日志输出到控制台 11.MinimumLevel.Information() 12.MinimumLevel.Override("Microsoft", Serilog.Events.LogEventLevel.Warning) 13.CreateLogger(); 14logBuilder.AddSerilog(dispose: true); 15}) 16.UseAutofac(); 17 builder.Services.ReplaceConfiguration(builder.Configuration); 18 builder.Services.AddApplication(); 19 20 var app = builder.Build(); 21 22 app.InitializeApplication(); 23 24 app.MapGet("/", () => "Hello World!"); 25 app.Run();


到此第一部分结束,我们使用Postman发起请求看看,效果图如下:
使用Abp|使用Abp vnext构建基于Duende.IdentityServer的统一授权中心(一)
文章图片


结尾附上Abp vnext 脚手架模板地址:
https://gitee.com/pterosaur-open/abp-template
项目还在继续完善中,第一版的重点会放在功能实现上,代码优化和细节优化得排后面咯!

    推荐阅读