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

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

使用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. 『GoLang』面向对象

    我们总结一下前面看到的:Go 没有类,而是松耦合的类型.方法对接口的实现. 面向对象语言最重要的三个方面分别是:封装,继承和多态,在 Go 中它们是怎样表现的呢? Go实现面向对象的两个关键是stru ...

  2. 2017第二届广东省强网杯线上赛:WEB phone number (SQL注入)

    目录 解题思路 总结 解题思路 拿到题目的时候,只有一个登录界面 拿到登录界面,而且还伴随着有注册界面,联想到SQL的二次注入漏洞 尝试注册admin'#,并使用admin登录,发现登录失败,说明可能 ...

  3. 设计 4 个线程,其中两个线程每次对 j 增加 1 ,另外两个线程对 j 每次减少 1 。写出程序。

    题目:设计 4 个线程,其中两个线程每次对 j 增加 1 ,另外两个线程对 j 每次减少 1 .写出程序. 代码实现 public class ThreadTest{ private int j; c ...

  4. 通俗易懂,Layui前端框架!

    前言   layui 是一款采用自身模块规范编写的前端 UI 框架,遵循原生 HTML/CSS/JS 的书写与组织形式,门槛极低,拿来即用.其外在极简,却又不失饱满的内在,体积轻盈,组件丰盈,从核心代 ...

  5. Pandas高级教程之:时间处理

    目录 简介 时间分类 Timestamp DatetimeIndex date_range 和 bdate_range origin 格式化 Period DateOffset 作为index 切片和 ...

  6. System.Drawing Linux Centos7 The type initializer for 'Gdip' threw an exception

    System.Drawing 在linux使用时提示异常 The type initializer for 'Gdip' threw an exception 解决方案: yum install au ...

  7. InstallScript脚本语言基本知识(一)

    1.自定义函数 1 //函数的声明:prototype 返回值 函数名(形参类型1,...) 2 export prototype STRING GetPreDir(STRING); 3 4 //函数 ...

  8. Linux中的文件使用FTP进行文件备份

    注意!!! 本文是在linux中进行ftp备份(备份到另一个linux服务器) 上传思路: 1.每次上传文件时, 后台接收文件, 使用transferTo上传到Linux服务器 2.把文件路径 + F ...

  9. LOJ6469 Magic(trie)

    纪念我菜的真实的一场模拟赛 首先看到这个题目,一开始就很毒瘤.一定是没有办法直接做的. 我们考虑转化问题 假设,我们选择枚举\(x\),其中\(x\)是\(10\)的若干次方,那么我们只需要求有多少对 ...

  10. 【MySQL】MySQL(三)存储过程和函数、触发器、事务

    MySQL存储过程和函数 存储过程和函数的概念 存储过程和函数是 事先经过编译并存储在数据库中的一段 SQL 语句的集合 存储过程和函数的好处 存储过程和函数可以重复使用,减轻开发人员的工作量.类似于 ...