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

时间:2024-04-17 15:48:48

原来看到很多示例都是基于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

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