using System.Security.Cryptography.X509Certificates;
using Dpz.Core.Infrastructure;
using Dpz.Core.Public.Entity.Auth;
using Dpz.Core.Public.ViewModel;
using Dpz.Core.Service;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.DataProtection.KeyManagement;
using Microsoft.AspNetCore.Identity;
using OpenIddict.Abstractions;
using OpenIddict.Client;
using OpenIddict.Client.AspNetCore;
namespace Dpz.Core.Web.Jobs.Security;
public static class RegisterOpenIddictExtensions
{
public static void AddOpenIddictClient(
this IServiceCollection services,
IConfiguration configuration
)
{
services.AddCookieAuthentication();
var certBase64 = configuration["Client:Jobs:CertBase64"];
var password = configuration["Client:Jobs:CertPassword"];
if (string.IsNullOrWhiteSpace(certBase64) || string.IsNullOrWhiteSpace(password))
{
throw new InvalidConfigurationException("certBase64 or certPassword is null.");
}
var issuer = configuration["Server:Issuer"];
if (string.IsNullOrWhiteSpace(issuer))
{
throw new InvalidConfigurationException("Issuer is null.");
}
var clientId = configuration["Client:Jobs:Id"];
if (string.IsNullOrWhiteSpace(clientId))
{
throw new InvalidConfigurationException("clientId is null.");
}
var clientSecret = configuration["Client:Jobs:Secret"];
if (string.IsNullOrWhiteSpace(clientSecret))
{
throw new InvalidConfigurationException("clientSecret is null.");
}
var redirectUri = configuration["Client:Jobs:RedirectUri"];
if (string.IsNullOrWhiteSpace(redirectUri))
{
throw new InvalidConfigurationException("redirectUri is null.");
}
var postLogoutRedirectValue = configuration["Client:Jobs:PostLogoutRedirectUri"];
var postLogoutRedirectUri = string.IsNullOrWhiteSpace(postLogoutRedirectValue)
? null
: new Uri(postLogoutRedirectValue);
var scopes = configuration.GetSection("Client:Jobs:Scope").Get<HashSet<string>>() ?? [];
services
.AddOpenIddict()
.AddCore(opt =>
{
opt.SetDefaultTokenEntity<JobsToken>();
opt.ReplaceTokenStore<JobsToken, TokenStoreService<JobsToken>>();
})
.AddClient(opt =>
{
opt.UseAspNetCore()
.EnablePostLogoutRedirectionEndpointPassthrough()
.EnableErrorPassthrough()
.EnableRedirectionEndpointPassthrough()
.EnableStatusCodePagesIntegration()
.DisableTransportSecurityRequirement();
// 启用授权码流程
opt.AllowAuthorizationCodeFlow();
var certBytes = Convert.FromBase64String(certBase64);
var cert = X509CertificateLoader.LoadPkcs12(certBytes, password);
// 添加加密密钥
opt.AddEncryptionCertificate(cert);
// 添加签名密钥
opt.AddSigningCertificate(cert);
opt.UseSystemNetHttp();
var registration = new OpenIddictClientRegistration
{
Issuer = new Uri(issuer),
ClientId = clientId,
ClientSecret = clientSecret,
RedirectUri = new Uri(redirectUri),
PostLogoutRedirectUri = postLogoutRedirectUri,
ResponseTypes = { OpenIddictConstants.ResponseTypes.Code },
GrantTypes = { OpenIddictConstants.GrantTypes.AuthorizationCode },
};
scopes.ForEach(x => registration.Scopes.Add(x));
opt.AddRegistration(registration);
})
.AddValidation(options =>
{
options.UseSystemNetHttp();
options.SetIssuer(new Uri(issuer));
options.UseAspNetCore();
});
}
private static void AddCookieAuthentication(this IServiceCollection services)
{
services.AddScoped<
IUserClaimsPrincipalFactory<VmUserInfo>,
VmUserInfoClaimsPrincipalFactory
>();
services
.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = IdentityConstants.ApplicationScheme;
x.DefaultSignInScheme = IdentityConstants.ApplicationScheme;
x.DefaultChallengeScheme = OpenIddictClientAspNetCoreDefaults.AuthenticationScheme;
})
.AddCookie(
IdentityConstants.ApplicationScheme,
options =>
{
options.LoginPath = "/sign.html";
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
}
);
services.AddIdentityCore<VmUserInfo>().AddSignInManager().AddUserStore<VmUserInfoStore>();
const string appName = "Dpz.Core.Web.Jobs";
services.AddDataProtection().SetApplicationName(appName);
services
.AddOptions<KeyManagementOptions>()
.Configure<IServiceScopeFactory>(
(options, factory) =>
{
options.XmlRepository = new XmlRepositoryService(factory, appName + ".Key");
}
);
}
}
public class JobsToken : AuthToken;
⚠⚠ 以下内容为AI分析的结果,请根据实际情况进行判断。
下面按模块逐步解释这段代码的功能和作用,结合它在应用中要达到的整体目的做说明。
总体目的(一句话)
- 为名为 "Jobs" 的客户端配置 OpenIddict 的客户端功能和验证,启用基于授权码(Authorization Code)的 OpenID Connect 登录流程,使用 PCKS#12 证书做签名与加密,使用 Cookie 做本地会话,并定制 token 实体和持久化存储以及数据保护密钥的持久化策略。
主要流程和关键点
- 配置读取与校验
- 从 IConfiguration 中读取一系列必需的配置项(例如:Client:Jobs:CertBase64、Client:Jobs:CertPassword、Server:Issuer、Client:Jobs:Id、Client:Jobs:Secret、Client:Jobs:RedirectUri 等)。
- 如果任一重要配置缺失,会抛出 InvalidConfigurationException,保证运行时配置完整。
- Cookie 与 ASP.NET Identity 基础(AddCookieAuthentication)
- 注册自定义的 IUserClaimsPrincipalFactory
(VmUserInfoClaimsPrincipalFactory)用于生成 ClaimsPrincipal。 - 使用 AddAuthentication 配置:
- 默认 Authenticate/SignIn 使用 IdentityConstants.ApplicationScheme(即 Cookie scheme)。
- 默认 Challenge scheme 指向 OpenIddict 客户端的挑战方案(OpenIddictClientAspNetCoreDefaults.AuthenticationScheme),这样触发登录挑战会走 OpenIddict 客户端流程。
- 添加 Cookie 身份验证(IdentityConstants.ApplicationScheme)并设置:
- LoginPath = "/sign.html"
- Cookie.SecurePolicy = Always(确保 cookie 仅通过 HTTPS 传输)
- 注册 IdentityCore
、SignInManager 和自定义 VmUserInfoStore 作为用户存储(用于本地会话/用户相关操作)。 - Data Protection:
- 设置应用名(SetApplicationName("Dpz.Core.Web.Jobs")),保证不同应用之间可共享或隔离数据保护密钥。
- 使用 KeyManagementOptions.XmlRepository 指定一个自定义 XmlRepository(XmlRepositoryService)来持久化密钥(传入 appName + ".Key"),保证服务器重启或分布式部署时密钥持久化与共享。
- OpenIddict 核心与客户端配置(services.AddOpenIddict().AddCore().AddClient().AddValidation())
AddCore:
- SetDefaultTokenEntity
():将默认 token 实体设置为自定义的 JobsToken(类底部有 public class JobsToken : AuthToken;)。 - ReplaceTokenStore<JobsToken, TokenStoreService
>():替换默认的 Token 存储实现,使用自定义的 TokenStoreService 来持久化 JobsToken。 - 这允许自定义 token 实体字段和持久化策略(例如存入特定数据库表等)。
- SetDefaultTokenEntity
AddClient(客户端端点配置):
- UseAspNetCore() 并启用一系列 passthrough/集成选项:
- EnablePostLogoutRedirectionEndpointPassthrough、EnableErrorPassthrough、EnableRedirectionEndpointPassthrough:这些选项允许框架在遇到相应的重定向/错误时把请求“透传”回应用,以便应用自己处理或进一步定制响应行为。
- EnableStatusCodePagesIntegration:与 ASP.NET Core 的 StatusCodePages 集成。
- DisableTransportSecurityRequirement():禁用对 HTTPS 的强制要求(通常只在开发或特定场景用到;生产环境应慎用)。
- 允许授权码流程:opt.AllowAuthorizationCodeFlow();
- 证书处理:
- 从配置里的 Base64 字符串解码并用密码加载 PKCS#12 证书(X509CertificateLoader.LoadPkcs12)。
- 将该证书分别作为加密证书(AddEncryptionCertificate)和签名证书(AddSigningCertificate)使用。这样客户端在需要时可以使用证书进行请求/响应的签名和加密(例如保护状態/ID tokens 等)。
- 使用 System.Net.Http 作为底层 HTTP 客户端(UseSystemNetHttp)。
- 构造 OpenIddictClientRegistration:
- Issuer(服务器地址)、ClientId、ClientSecret、RedirectUri、PostLogoutRedirectUri
- ResponseTypes = code、GrantTypes = authorization_code(这是标准的 auth code 流)
- 将配置中的 scopes 加入 registration.Scopes
- 最后将该 registration 添加到 OpenIddict 客户端。
- UseAspNetCore() 并启用一系列 passthrough/集成选项:
AddValidation(令牌验证):
- UseSystemNetHttp():使用 System.Net.Http 下载或验证元数据/公钥。
- SetIssuer(new Uri(issuer)):设置签发者,用于验证 token 的 Issuer。
- UseAspNetCore():将验证集成到 ASP.NET Core 管道,可以自动处理验证结果并将 principal 注入 HttpContext.User。
- 自定义 Token 类
- public class JobsToken : AuthToken;
- 表示使用自定义 token 实体(继承自 AuthToken),与 AddCore 中的 SetDefaultTokenEntity 一致,便于扩展 token 属性和持久化结构。
注意与潜在风险/细节
- DisableTransportSecurityRequirement() 会允许非 HTTPS 连接,生产环境中要确保使用 HTTPS,否则可能造成凭证泄露。
- 证书(certBase64 + password)以配置形式出现,需要安全管理(不要把原始凭证直接放在明文配置中,建议使用安全的机密管理如 Azure Key Vault、AWS Secrets Manager、环境变量或受保护的配置源)。
- DataProtection 的 XmlRepositoryService 负责密钥的持久化实现(代码中并未展示该类的实现细节),确保其实现是线程安全且在多实例部署时能共享密钥(否则 Cookie / 数据保护相关功能会失效)。
- 配置项缺失会直接抛出异常,保证早期失败以便发现配置问题。
总结
- 该扩展方法为“Jobs”客户端在 ASP.NET Core 应用中配置了完整的 OpenIddict 客户端功能(授权码流、注册中心 Issuer、客户端凭据、证书的签名/加密、token 持久化替换、验证与 ASP.NET Core 集成),同时配置了本地 Cookie 会话和数据保护密钥的持久化,使客户端能和中心授权服务器进行安全的 OpenID Connect 交互并维护本地登录会话。若需要我可以进一步解释某个具体方法或帮你检查/改进配置示例。
AI 正在分析代码…
评论加载中...