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 实体和持久化存储以及数据保护密钥的持久化策略。

主要流程和关键点

  1. 配置读取与校验
  • 从 IConfiguration 中读取一系列必需的配置项(例如:Client:Jobs:CertBase64、Client:Jobs:CertPassword、Server:Issuer、Client:Jobs:Id、Client:Jobs:Secret、Client:Jobs:RedirectUri 等)。
  • 如果任一重要配置缺失,会抛出 InvalidConfigurationException,保证运行时配置完整。
  1. 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"),保证服务器重启或分布式部署时密钥持久化与共享。
  1. OpenIddict 核心与客户端配置(services.AddOpenIddict().AddCore().AddClient().AddValidation())
  • AddCore:

    • SetDefaultTokenEntity():将默认 token 实体设置为自定义的 JobsToken(类底部有 public class JobsToken : AuthToken;)。
    • ReplaceTokenStore<JobsToken, TokenStoreService>():替换默认的 Token 存储实现,使用自定义的 TokenStoreService 来持久化 JobsToken。
    • 这允许自定义 token 实体字段和持久化策略(例如存入特定数据库表等)。
  • 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 客户端。
  • AddValidation(令牌验证):

    • UseSystemNetHttp():使用 System.Net.Http 下载或验证元数据/公钥。
    • SetIssuer(new Uri(issuer)):设置签发者,用于验证 token 的 Issuer。
    • UseAspNetCore():将验证集成到 ASP.NET Core 管道,可以自动处理验证结果并将 principal 注入 HttpContext.User。
  1. 自定义 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 交互并维护本地登录会话。若需要我可以进一步解释某个具体方法或帮你检查/改进配置示例。
评论加载中...