使用Abp vnext构建基于Duende.IdentityServer的统一授权中心(一)
原来看到很多示例都是基于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的统一授权中心(一)的更多相关文章
- 手把手教你用Abp vnext构建API接口服务
ABP是一个开源应用程序框架,该项目是ASP.NET Boilerplate Web应用程序框架的下一代,专注于基于ASP.NET Core的Web应用程序开发,也支持开发控制台应用程序. 官方网站: ...
- .NET Core微服务之基于Ocelot+IdentityServer实现统一验证与授权
Tip: 此篇已加入.NET Core微服务基础系列文章索引 一.案例结构总览 这里,假设我们有两个客户端(一个Web网站,一个移动App),他们要使用系统,需要通过API网关(这里API网关始终作为 ...
- ABP vnext模块化架构的最佳实践的实现
在上一篇文章<手把手教你用Abp vnext构建API接口服务>中,我们用ABP vnext实现了WebAPI接口服务,但是并非ABP模块化架构的最佳实践.我本身也在学习ABP,我认为AB ...
- 构建基于分布式SOA架构的统一身份认证体系
摘要:本文充分利用SOA架构松耦合的特点,通过规范统一网络接口实现业务系统整合,既提升系统安全性,又简化资源访问操作,具有重要的理论和现实意义. 统一身份认证旨在将分散在各个信息系统中的用户和权限资源 ...
- [WCF安全3]使用wsHttpBinding构建基于SSL与UserName授权的WCF应用程序
上一篇文章中介绍了如何使用wsHttpBinding构建UserName授权的WCF应用程序,本文将为您介绍如何使用wsHttpBinding构建基于SSL的UserName安全授权的WCF应用程序. ...
- 基于ZK构建统一配置中心的方案和实践
背景: 近期使用Zk实现了一个简单的配置管理的小东西,在此开源出来,有兴趣的希望提出您的宝贵意见.如果恰巧您也使用或者接触过类似的东西, 也希望您可以分享下您觉得现在这个项目可以优化和改进的地方. 项 ...
- 基于 abp vNext 微服务开发的敏捷应用构建平台 - 项目介绍
缘起 目前使用ABP框架已经将近3年了,大大小小的项目也陆陆续续做了很多.由于现有信息系统的架构模式是在底层的技术平台上直接构建信息系统并采用技术主导,使用业务无关的编程工具来开发信息系统的缺陷使得系 ...
- 基于 abp vNext 微服务开发的敏捷应用构建平台 - 文章目录
系列文章: <基于 abp vNext 微服务开发的敏捷应用构建平台 - 设计构想> [点击查看] <基于 abp vNext 微服务开发的敏捷应用构建平台 - 文章目录> [ ...
- 基于 abp vNext 微服务开发的敏捷应用构建平台 - 框架分析
总体架构 本平台从技术上采用ABP vNext和.NET Core编写的微服务架构.客户端层主要以现代浏览器为主,适配了PC端和移动端的访问,采用API和应用程序进行交互,同时提供第三方使用的 ...
随机推荐
- android web外壳
参考: 1.https://blog.csdn.net/m0_37201243/article/details/106862817 2.https://www.cnblogs.com/ifaswind ...
- sublime text 3 在Windows下配置sublimelinter-php的路径问题
首先用package control安装sublimelinter和sublimelinter-php,然后依次点击菜单preference-package settings-sublimelinte ...
- Python3入门系列之-----看完这一篇文章我终于学会了类
前言 类顾名思义,就是一类事物.或者叫做实例,它用来描述具有共同特征的一类事物.我们在Python中声明类的关键词是class,类还有功能和属性,属性就是这类事物的特征,而功能就是它能做什么,也是就是 ...
- 🚴♂️全套MySQL数据库教程_Mysql基础入门教程,零基础小白自学MySQL数据库必备教程☔ #002 # 第二单元 MySQL数据类型、操作表#
二.本单元知识点概述 (Ⅰ)知识点概述 二.本单元教学目标 (Ⅰ)重点知识目标 1.Mysql的数据类型2.如何选择数据类型3.创建表4.修改表5.删除表 (Ⅱ)能力目标 1.熟练创建数据库及删除数据 ...
- ansible远程运维操作
1.command 用于查看文件内容,查看磁盘,内存,启动命令等纯命令信息 ansible portal -m command -a "cat /test1/test"2.ping ...
- Dapr + .NET Core实战(十二)服务调用之GRPC
什么是GRPC gRPC 是一种与语言无关的高性能远程过程调用 (RPC) 框架. gRPC 的主要优点是: 高性能轻量级 RPC 框架. 协定优先 API 开发,默认使用协议缓冲区,允许与语言无关的 ...
- 利用水文分析方法提取山脊线和山谷线(ArcPy实现)
一.背景 作为地形特征线的山脊线.山谷线对地形.地貌具有一定的控制作用.它们与山顶点.谷底点以及鞍部点等一起构成了地形起伏变化的骨架结构.同时由于山脊线具有分水性,山谷线具有合水性特征,使得它们在地形 ...
- bzoj3073Journeys(线段树优化最短路)
这里还是一道涉及到区间连边的问题. 如果暴力去做,那么就会爆炸 那么这时候就需要线段树来优化了. 因为是双向边 所以需要两颗线段树来分别对应入边和出边 QwQ然后做就好了咯 不过需要注意的是,这个边数 ...
- SpringBoot整合Mabatis
1.导入 MyBatis 所需要的依赖 <dependency> <groupId>org.mybatis.spring.boot</groupId> <ar ...
- jdbc简单学生管理系统
这个是java连接mysql数据库的一个简单学生系统,通过jdbc连接数据库. 工具类 JDBCuntils. package Student; import java.io.IOException; ...