From 19723602075444f68fc0b381f01059dd1c91994b Mon Sep 17 00:00:00 2001 From: MayueCif Date: Tue, 21 Jul 2026 10:25:11 +0800 Subject: [PATCH 1/6] =?UTF-8?q?=E7=BB=86=E5=8C=96LDAP=E8=BF=9E=E6=8E=A5?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E4=B8=8E=E5=BC=82=E5=B8=B8=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增4个LDAP相关异常码至UserFriendlyExceptionCodes,细分连接失败原因。引入LdapAuthenticateHelper,分阶段校验LDAP连接并抛出明确异常,提升问题定位效率。LdapCommandHandler统一调用新工具类,增强代码复用与异常一致性。修正LdapConnectTestCommandValidator泛型参数。多语言资源补充异常码本地化提示。_Imports.cs全局引入Ldap命名空间。 --- .../Constants/UserFriendlyExceptionCodes.cs | 4 + .../LdapConnectTestCommandValidator.cs | 2 +- .../Subjects/LdapCommandHandler.cs | 13 +-- .../Assets/I18n/en-US.json | 4 + .../Assets/I18n/ja-JP.json | 4 + .../Assets/I18n/ru-RU.json | 4 + .../Assets/I18n/zh-CN.json | 4 + .../Ldap/LdapAuthenticateHelper.cs | 100 ++++++++++++++++++ .../Masa.Auth.Service.Admin/_Imports.cs | 1 + 9 files changed, 125 insertions(+), 11 deletions(-) create mode 100644 src/Services/Masa.Auth.Service.Admin/Infrastructure/Ldap/LdapAuthenticateHelper.cs diff --git a/src/Contracts/Masa.Auth.Contracts.Admin/Infrastructure/Constants/UserFriendlyExceptionCodes.cs b/src/Contracts/Masa.Auth.Contracts.Admin/Infrastructure/Constants/UserFriendlyExceptionCodes.cs index aedd917e..4d23a4ab 100644 --- a/src/Contracts/Masa.Auth.Contracts.Admin/Infrastructure/Constants/UserFriendlyExceptionCodes.cs +++ b/src/Contracts/Masa.Auth.Contracts.Admin/Infrastructure/Constants/UserFriendlyExceptionCodes.cs @@ -16,6 +16,10 @@ public static class UserFriendlyExceptionCodes public const string INVALID_EMAIL_CAPTCHA = "InvalidEmailCaptcha"; public const string INVALID_SMS_CAPTCHA = "InvalidSMSCaptcha"; public const string CONNECT_ERROR = "ConnectError"; + public const string LDAP_SERVER_UNREACHABLE = "LdapServerUnreachable"; + public const string LDAP_SSL_MISMATCH = "LdapSslMismatch"; + public const string LDAP_CREDENTIALS_INVALID = "LdapCredentialsInvalid"; + public const string LDAP_BIND_FAILED = "LdapBindFailed"; public const string INVALID_SEND_EMAIL_TYPE = "InvalidSendEmailType"; public const string INVALID_SEND_MSG_CODE_TYPE = "InvalidSendMsgCodeType"; public const string INVALID_RESET_PASSWORD_TYPE = "InvalidResetPasswordType"; diff --git a/src/Services/Masa.Auth.Service.Admin/Application/Subjects/Commands/LdapConnectTestCommandValidator.cs b/src/Services/Masa.Auth.Service.Admin/Application/Subjects/Commands/LdapConnectTestCommandValidator.cs index 0a81f404..6cdd66d6 100644 --- a/src/Services/Masa.Auth.Service.Admin/Application/Subjects/Commands/LdapConnectTestCommandValidator.cs +++ b/src/Services/Masa.Auth.Service.Admin/Application/Subjects/Commands/LdapConnectTestCommandValidator.cs @@ -3,7 +3,7 @@ namespace Masa.Auth.Service.Admin.Application.Subjects.Commands; -public class LdapConnectTestCommandValidator : AbstractValidator +public class LdapConnectTestCommandValidator : AbstractValidator { public LdapConnectTestCommandValidator() { diff --git a/src/Services/Masa.Auth.Service.Admin/Application/Subjects/LdapCommandHandler.cs b/src/Services/Masa.Auth.Service.Admin/Application/Subjects/LdapCommandHandler.cs index 998bcf60..7938701d 100644 --- a/src/Services/Masa.Auth.Service.Admin/Application/Subjects/LdapCommandHandler.cs +++ b/src/Services/Masa.Auth.Service.Admin/Application/Subjects/LdapCommandHandler.cs @@ -31,12 +31,7 @@ public LdapCommandHandler( [EventHandler] public async Task LdapConnectTestAsync(LdapConnectTestCommand ldapConnectTestCommand) { - var ldapOptions = ldapConnectTestCommand.LdapDetailDto.Adapt(); - var ldapProvider = _ldapFactory.CreateProvider(ldapOptions); - if (!await ldapProvider.AuthenticateAsync(ldapOptions.RootUserDn, ldapOptions.RootUserPassword)) - { - throw new UserFriendlyException(errorCode: UserFriendlyExceptionCodes.CONNECT_ERROR); - } + await LdapAuthenticateHelper.AuthenticateOrThrowAsync(ldapConnectTestCommand.LdapDetailDto); } [EventHandler] @@ -44,12 +39,10 @@ public async Task LdapUpsertAsync(LdapUpsertCommand ldapUpsertCommand) { var ldapIdpDto = ldapUpsertCommand.LdapDetailDto; + await LdapAuthenticateHelper.AuthenticateOrThrowAsync(ldapIdpDto); + var ldapOptions = ldapIdpDto.Adapt(); var ldapProvider = _ldapFactory.CreateProvider(ldapOptions); - if (!await ldapProvider.AuthenticateAsync(ldapOptions.RootUserDn, ldapOptions.RootUserPassword)) - { - throw new UserFriendlyException(errorCode: UserFriendlyExceptionCodes.CONNECT_ERROR); - } var ldapIdp = new LdapIdp( ldapIdpDto.ServerAddress, diff --git a/src/Services/Masa.Auth.Service.Admin/Assets/I18n/en-US.json b/src/Services/Masa.Auth.Service.Admin/Assets/I18n/en-US.json index 6c0dbd81..26c758f9 100644 --- a/src/Services/Masa.Auth.Service.Admin/Assets/I18n/en-US.json +++ b/src/Services/Masa.Auth.Service.Admin/Assets/I18n/en-US.json @@ -14,6 +14,10 @@ "InvalidSMSCaptcha": "The sms captcha is invalid", "InvalidCaptcha": "Invalid Captcha", "ConnectError": "connect error", + "LdapServerUnreachable": "[Address/Port] Unable to reach LDAP server {0}:{1}. Please verify the server address and port", + "LdapSslMismatch": "[LDAP/LDAPS or TLS error] The {0} handshake failed for {1}:{2}. Verify the protocol, port, and server certificate", + "LdapCredentialsInvalid": "[Credentials] Invalid RootUserDn or password. Please check and retry", + "LdapBindFailed": "[LDAP authentication] The server rejected the bind or the connection was interrupted. Check the account permissions and LDAP server policy", "InvalidSendEmailType": "Invalid SendEmailType", "InvalidSendMsgCodeType": "Invalid SendMsgCodeType", "InvalidResetPasswordType": "Invalid ResetPasswordType", diff --git a/src/Services/Masa.Auth.Service.Admin/Assets/I18n/ja-JP.json b/src/Services/Masa.Auth.Service.Admin/Assets/I18n/ja-JP.json index 87389a56..43606dab 100644 --- a/src/Services/Masa.Auth.Service.Admin/Assets/I18n/ja-JP.json +++ b/src/Services/Masa.Auth.Service.Admin/Assets/I18n/ja-JP.json @@ -14,6 +14,10 @@ "InvalidSMSCaptcha": "SMS認証コードが無効です", "InvalidCaptcha": "認証コードが無効です", "ConnectError": "接続エラー", + "LdapServerUnreachable": "【アドレス/ポートエラー】LDAPサーバー {0}:{1} に接続できません。アドレスとポートを確認してください", + "LdapSslMismatch": "【LDAP/LDAPSまたはTLSエラー】{0} で {1}:{2} に接続する際にハンドシェイクに失敗しました。プロトコル、ポート、サーバー証明書を確認してください", + "LdapCredentialsInvalid": "【アカウント/パスワードエラー】RootUserDnまたはパスワードが正しくありません", + "LdapBindFailed": "【LDAP認証失敗】LDAPサーバーがBind要求を拒否したか、接続が中断されました。アカウント権限とLDAPサーバーの認証ポリシーを確認してください", "InvalidSendEmailType": "SendEmailTypeが無効です", "InvalidSendMsgCodeType": "SendMsgCodeTypeが無効です", "InvalidResetPasswordType": "パスワードリセットタイプが無効です", diff --git a/src/Services/Masa.Auth.Service.Admin/Assets/I18n/ru-RU.json b/src/Services/Masa.Auth.Service.Admin/Assets/I18n/ru-RU.json index 978dfae9..77cf0906 100644 --- a/src/Services/Masa.Auth.Service.Admin/Assets/I18n/ru-RU.json +++ b/src/Services/Masa.Auth.Service.Admin/Assets/I18n/ru-RU.json @@ -14,6 +14,10 @@ "InvalidSMSCaptcha": "Неверный SMS-код", "InvalidCaptcha": "Неверный код подтверждения", "ConnectError": "Ошибка соединения", + "LdapServerUnreachable": "[Адрес/порт] Не удалось подключиться к LDAP-серверу {0}:{1}. Проверьте адрес и порт", + "LdapSslMismatch": "[LDAP/LDAPS или TLS] Не удалось выполнить рукопожатие {0} с {1}:{2}. Проверьте протокол, порт и сертификат сервера", + "LdapCredentialsInvalid": "[Учетные данные] Неверный RootUserDn или пароль", + "LdapBindFailed": "[Аутентификация LDAP] Сервер отклонил запрос Bind или соединение было прервано. Проверьте права учетной записи и политику LDAP-сервера", "InvalidSendEmailType": "Неверный тип SendEmailType", "InvalidSendMsgCodeType": "Неверный тип SendMsgCodeType", "InvalidResetPasswordType": "Неверный тип ResetPasswordType", diff --git a/src/Services/Masa.Auth.Service.Admin/Assets/I18n/zh-CN.json b/src/Services/Masa.Auth.Service.Admin/Assets/I18n/zh-CN.json index 9171db97..c8196f3f 100644 --- a/src/Services/Masa.Auth.Service.Admin/Assets/I18n/zh-CN.json +++ b/src/Services/Masa.Auth.Service.Admin/Assets/I18n/zh-CN.json @@ -14,6 +14,10 @@ "InvalidSMSCaptcha": "短信验证码无效", "InvalidCaptcha": "验证码无效", "ConnectError": "连接错误", + "LdapServerUnreachable": "【地址/端口错误】无法连接到 LDAP 服务器 {0}:{1},请检查服务器地址和端口", + "LdapSslMismatch": "【LDAP/LDAPS 或 TLS 错误】使用 {0} 连接 {1}:{2} 时握手失败,请检查协议、端口和服务器证书", + "LdapCredentialsInvalid": "【域账号/密码错误】RootUserDn 或密码不正确,请检查后重试", + "LdapBindFailed": "【LDAP 认证失败】LDAP 服务器拒绝了 Bind 请求或连接被中断,请检查账号权限及服务器认证策略", "InvalidSendEmailType": "SendEmailType无效", "InvalidSendMsgCodeType": "SendMsgCodeType无效", "InvalidResetPasswordType": "ResetPasswordType无效", diff --git a/src/Services/Masa.Auth.Service.Admin/Infrastructure/Ldap/LdapAuthenticateHelper.cs b/src/Services/Masa.Auth.Service.Admin/Infrastructure/Ldap/LdapAuthenticateHelper.cs new file mode 100644 index 00000000..7675cba7 --- /dev/null +++ b/src/Services/Masa.Auth.Service.Admin/Infrastructure/Ldap/LdapAuthenticateHelper.cs @@ -0,0 +1,100 @@ +// Copyright (c) MASA Stack All rights reserved. +// Licensed under the Apache License. See LICENSE.txt in the project root for license information. + +using System.Net.Sockets; + +namespace Masa.Auth.Service.Admin.Infrastructure.Ldap; + +/// +/// LDAP 连接校验,按失败阶段返回明确提示: +/// 1. TCP 地址/端口不可达 → +/// 2. LDAP/LDAPS 握手失败 → +/// 3. LDAP result code 49 → +/// 4. 其它 Bind 失败 → +/// +public static class LdapAuthenticateHelper +{ + /// + /// LDAP result code: invalidCredentials (RFC 4511). + /// + const int LdapInvalidCredentials = 49; + + /// + /// Connect timeout for LDAP connect-test / save validation (milliseconds). + /// + const int ConnectionTimeoutMs = 10_000; + + public static async Task AuthenticateOrThrowAsync(LdapDetailDto ldapDetailDto) + { + await EnsureTcpConnectionAsync(ldapDetailDto); + + using var connection = new LdapConnection + { + SecureSocketLayer = ldapDetailDto.IsLdaps, + ConnectionTimeout = ConnectionTimeoutMs + }; + + // Stage 1: TCP / TLS connect — address、port、LDAP vs LDAPS + try + { + await connection.ConnectAsync(ldapDetailDto.ServerAddress, ldapDetailDto.ServerPort); + } + catch (Exception ex) + { + // TCP 已经连通,此时失败发生在 LDAP 明文协议或 TLS 握手阶段。 + // 不再把它误报为“地址/端口不可达”。 + throw CreateProtocolException(ldapDetailDto); + } + + // Stage 2: Bind — credentials + try + { + await connection.BindAsync(ldapDetailDto.RootUserDn, ldapDetailDto.RootUserPassword); + } + catch (LdapException ex) when (ex.ResultCode == LdapInvalidCredentials) + { + throw new UserFriendlyException(errorCode: UserFriendlyExceptionCodes.LDAP_CREDENTIALS_INVALID); + } + catch (LdapException) + { + // 只有 result code 49 才能确定是账号或密码错误。 + throw new UserFriendlyException(errorCode: UserFriendlyExceptionCodes.LDAP_BIND_FAILED); + } + catch (Exception) + { + // Bind 阶段连接中断、服务端策略异常等,不能归类为密码错误。 + throw new UserFriendlyException(errorCode: UserFriendlyExceptionCodes.LDAP_BIND_FAILED); + } + } + + static async Task EnsureTcpConnectionAsync(LdapDetailDto ldapDetailDto) + { + using var timeout = new CancellationTokenSource(TimeSpan.FromMilliseconds(ConnectionTimeoutMs)); + using var tcpClient = new TcpClient(); + + try + { + await tcpClient.ConnectAsync( + ldapDetailDto.ServerAddress, + ldapDetailDto.ServerPort, + timeout.Token); + } + catch (Exception ex) when (ex is SocketException or OperationCanceledException) + { + throw new UserFriendlyException( + UserFriendlyExceptionCodes.LDAP_SERVER_UNREACHABLE, + ldapDetailDto.ServerAddress, + ldapDetailDto.ServerPort.ToString()); + } + } + + static UserFriendlyException CreateProtocolException(LdapDetailDto ldapDetailDto) + { + var mode = ldapDetailDto.IsLdaps ? "LDAPS" : "LDAP"; + return new UserFriendlyException( + UserFriendlyExceptionCodes.LDAP_SSL_MISMATCH, + mode, + ldapDetailDto.ServerAddress, + ldapDetailDto.ServerPort.ToString()); + } +} diff --git a/src/Services/Masa.Auth.Service.Admin/_Imports.cs b/src/Services/Masa.Auth.Service.Admin/_Imports.cs index 9ab59f57..072b1ac4 100644 --- a/src/Services/Masa.Auth.Service.Admin/_Imports.cs +++ b/src/Services/Masa.Auth.Service.Admin/_Imports.cs @@ -81,6 +81,7 @@ global using Masa.Auth.Service.Admin.Infrastructure.CacheModels; global using Masa.Auth.Service.Admin.Infrastructure.Email; global using Masa.Auth.Service.Admin.Infrastructure.Extensions; +global using Masa.Auth.Service.Admin.Infrastructure.Ldap; global using Masa.Auth.Service.Admin.Infrastructure.Message; global using Masa.Auth.Service.Admin.Infrastructure.Middleware; global using Masa.Auth.Service.Admin.Infrastructure.Models; From ff99fd3ffc41f0fdb89712890ec13baf209c6615 Mon Sep 17 00:00:00 2001 From: MayueCif Date: Tue, 21 Jul 2026 11:12:40 +0800 Subject: [PATCH 2/6] =?UTF-8?q?=E7=BB=9F=E4=B8=80=E5=B8=B8=E9=87=8F?= =?UTF-8?q?=E5=91=BD=E5=90=8D=E9=A3=8E=E6=A0=BC=EF=BC=8C=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E5=8F=8A=E6=A0=BC=E5=BC=8F=E5=8C=96=E4=BB=A3?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - LdapAuthenticateHelper 常量命名调整为全大写下划线风格,相关引用同步更新,移除未用异常变量 - AuthDbContext 新增忽略 PendingModelChangesWarning 配置 - AuthDbContextModelSnapshot 条件配置顺序调整,无实质变更 - 主要为代码风格统一、配置优化及格式化,无核心逻辑变动 --- .../Migrations/AuthDbContextModelSnapshot.cs | 81 +++++++++---------- .../AuthDbContext.cs | 2 + .../Ldap/LdapAuthenticateHelper.cs | 12 +-- 3 files changed, 48 insertions(+), 47 deletions(-) diff --git a/src/Infrastructure/Masa.Auth.EntityFrameworkCore.PostgreSql/Migrations/AuthDbContextModelSnapshot.cs b/src/Infrastructure/Masa.Auth.EntityFrameworkCore.PostgreSql/Migrations/AuthDbContextModelSnapshot.cs index 6bb7617e..efeda0dd 100644 --- a/src/Infrastructure/Masa.Auth.EntityFrameworkCore.PostgreSql/Migrations/AuthDbContextModelSnapshot.cs +++ b/src/Infrastructure/Masa.Auth.EntityFrameworkCore.PostgreSql/Migrations/AuthDbContextModelSnapshot.cs @@ -1,6 +1,5 @@ // using System; -using Masa.Auth.Domain.Subjects.Aggregates; using Masa.Auth.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -2547,6 +2546,46 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Masa.Auth.Domain.DynamicRoles.Aggregates.DynamicRole", b => { + b.OwnsMany("Masa.Auth.Domain.DynamicRoles.Aggregates.DynamicRuleCondition", "Conditions", b1 => + { + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b1.Property("DataType") + .HasColumnType("integer"); + + b1.Property("DynamicRoleId") + .HasColumnType("uuid"); + + b1.Property("FieldName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b1.Property("LogicalOperator") + .HasColumnType("integer"); + + b1.Property("OperatorType") + .HasColumnType("integer"); + + b1.Property("Order") + .HasColumnType("integer"); + + b1.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b1.HasKey("Id"); + + b1.HasIndex("DynamicRoleId"); + + b1.ToTable("DynamicRuleCondition", "auth"); + + b1.WithOwner() + .HasForeignKey("DynamicRoleId"); + }); + b.OwnsMany("Masa.Auth.Domain.DynamicRoles.Aggregates.ControlPolicy", "ControlPolicies", b1 => { b1.Property("Id") @@ -2652,46 +2691,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b1.Navigation("Resources"); }); - b.OwnsMany("Masa.Auth.Domain.DynamicRoles.Aggregates.DynamicRuleCondition", "Conditions", b1 => - { - b1.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b1.Property("DataType") - .HasColumnType("integer"); - - b1.Property("DynamicRoleId") - .HasColumnType("uuid"); - - b1.Property("FieldName") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b1.Property("LogicalOperator") - .HasColumnType("integer"); - - b1.Property("OperatorType") - .HasColumnType("integer"); - - b1.Property("Order") - .HasColumnType("integer"); - - b1.Property("Value") - .IsRequired() - .HasColumnType("text"); - - b1.HasKey("Id"); - - b1.HasIndex("DynamicRoleId"); - - b1.ToTable("DynamicRuleCondition", "auth"); - - b1.WithOwner() - .HasForeignKey("DynamicRoleId"); - }); - b.Navigation("Conditions"); b.Navigation("ControlPolicies"); diff --git a/src/Infrastructure/Masa.Auth.EntityFrameworkCore/AuthDbContext.cs b/src/Infrastructure/Masa.Auth.EntityFrameworkCore/AuthDbContext.cs index 1c61e55a..b1f4c304 100644 --- a/src/Infrastructure/Masa.Auth.EntityFrameworkCore/AuthDbContext.cs +++ b/src/Infrastructure/Masa.Auth.EntityFrameworkCore/AuthDbContext.cs @@ -15,6 +15,8 @@ public AuthDbContext(MasaDbContextOptions options) : base(options protected override void OnConfiguring(MasaDbContextOptionsBuilder optionsBuilder) => optionsBuilder.DbContextOptionsBuilder //.LogTo(Console.WriteLine, LogLevel.Warning) + .ConfigureWarnings(w => w.Ignore( + Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.PendingModelChangesWarning)) .EnableSensitiveDataLogging() .EnableDetailedErrors(); diff --git a/src/Services/Masa.Auth.Service.Admin/Infrastructure/Ldap/LdapAuthenticateHelper.cs b/src/Services/Masa.Auth.Service.Admin/Infrastructure/Ldap/LdapAuthenticateHelper.cs index 7675cba7..667d96dc 100644 --- a/src/Services/Masa.Auth.Service.Admin/Infrastructure/Ldap/LdapAuthenticateHelper.cs +++ b/src/Services/Masa.Auth.Service.Admin/Infrastructure/Ldap/LdapAuthenticateHelper.cs @@ -17,12 +17,12 @@ public static class LdapAuthenticateHelper /// /// LDAP result code: invalidCredentials (RFC 4511). /// - const int LdapInvalidCredentials = 49; + const int LDAP_INVALID_CREDENTIALS = 49; /// /// Connect timeout for LDAP connect-test / save validation (milliseconds). /// - const int ConnectionTimeoutMs = 10_000; + const int CONNECTION_TIMEOUT_MS = 10_000; public static async Task AuthenticateOrThrowAsync(LdapDetailDto ldapDetailDto) { @@ -31,7 +31,7 @@ public static async Task AuthenticateOrThrowAsync(LdapDetailDto ldapDetailDto) using var connection = new LdapConnection { SecureSocketLayer = ldapDetailDto.IsLdaps, - ConnectionTimeout = ConnectionTimeoutMs + ConnectionTimeout = CONNECTION_TIMEOUT_MS }; // Stage 1: TCP / TLS connect — address、port、LDAP vs LDAPS @@ -39,7 +39,7 @@ public static async Task AuthenticateOrThrowAsync(LdapDetailDto ldapDetailDto) { await connection.ConnectAsync(ldapDetailDto.ServerAddress, ldapDetailDto.ServerPort); } - catch (Exception ex) + catch (Exception) { // TCP 已经连通,此时失败发生在 LDAP 明文协议或 TLS 握手阶段。 // 不再把它误报为“地址/端口不可达”。 @@ -51,7 +51,7 @@ public static async Task AuthenticateOrThrowAsync(LdapDetailDto ldapDetailDto) { await connection.BindAsync(ldapDetailDto.RootUserDn, ldapDetailDto.RootUserPassword); } - catch (LdapException ex) when (ex.ResultCode == LdapInvalidCredentials) + catch (LdapException ex) when (ex.ResultCode == LDAP_INVALID_CREDENTIALS) { throw new UserFriendlyException(errorCode: UserFriendlyExceptionCodes.LDAP_CREDENTIALS_INVALID); } @@ -69,7 +69,7 @@ public static async Task AuthenticateOrThrowAsync(LdapDetailDto ldapDetailDto) static async Task EnsureTcpConnectionAsync(LdapDetailDto ldapDetailDto) { - using var timeout = new CancellationTokenSource(TimeSpan.FromMilliseconds(ConnectionTimeoutMs)); + using var timeout = new CancellationTokenSource(TimeSpan.FromMilliseconds(CONNECTION_TIMEOUT_MS)); using var tcpClient = new TcpClient(); try From 451fbfcc39ab0e758078bc75876ce2d758efaa0c Mon Sep 17 00:00:00 2001 From: MayueCif Date: Tue, 21 Jul 2026 13:27:38 +0800 Subject: [PATCH 3/6] =?UTF-8?q?=E4=BE=9D=E8=B5=96=E6=B3=A8=E5=85=A5?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0IClientMessageTemplateProvider=E6=B3=A8?= =?UTF-8?q?=E5=86=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在 Program.cs 中,将 IClientMessageTemplateProvider 接口及其实现 ClientMessageTemplateProvider 以 Scoped 生命周期注册到依赖注入容器,便于后续依赖解析。其他代码未做修改。 --- src/Services/Masa.Auth.Service.Admin/Program.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Services/Masa.Auth.Service.Admin/Program.cs b/src/Services/Masa.Auth.Service.Admin/Program.cs index 64256bb1..bc0e95a6 100644 --- a/src/Services/Masa.Auth.Service.Admin/Program.cs +++ b/src/Services/Masa.Auth.Service.Admin/Program.cs @@ -18,6 +18,7 @@ #endif builder.Services.AddAutoInject(); +builder.Services.AddScoped(); builder.Services.AddDaprClient(); builder.Services.AddObjectStorage(option => option.UseAliyunStorage()); From b331bdc4de0f4e180c689001def79a598bac5c92 Mon Sep 17 00:00:00 2001 From: MayueCif Date: Tue, 21 Jul 2026 14:49:26 +0800 Subject: [PATCH 4/6] =?UTF-8?q?=E5=AF=86=E7=A0=81=E8=A7=84=E5=88=99?= =?UTF-8?q?=E6=A0=A1=E9=AA=8C=E7=94=B1=E5=BC=82=E6=AD=A5=E6=94=B9=E4=B8=BA?= =?UTF-8?q?=E5=90=8C=E6=AD=A5=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 扩展了 IPasswordRuleProvider 接口,新增同步方法 GetFailure,并在 PasswordRuleProvider 中实现。PasswordRuleValidatorExtensions 的 PasswordRule 扩展方法改为同步校验,提升 FluentValidation 校验链中的效率和一致性。Program.cs 注册了 PasswordRuleProvider,_Imports.cs 增加全局 using。 --- .../Password/IPasswordRuleProvider.cs | 2 ++ .../PasswordRuleValidatorExtensions.cs | 4 +-- .../Commands/ResetPasswordCommandValidator.cs | 2 -- .../Password/PasswordRuleProvider.cs | 26 +++++++++++++++++++ .../Masa.Auth.Service.Admin/Program.cs | 3 +++ .../Masa.Auth.Service.Admin/_Imports.cs | 1 + 6 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/Contracts/Masa.Auth.Contracts.Admin/Infrastructure/Password/IPasswordRuleProvider.cs b/src/Contracts/Masa.Auth.Contracts.Admin/Infrastructure/Password/IPasswordRuleProvider.cs index 0f41cdce..dbd5c414 100644 --- a/src/Contracts/Masa.Auth.Contracts.Admin/Infrastructure/Password/IPasswordRuleProvider.cs +++ b/src/Contracts/Masa.Auth.Contracts.Admin/Infrastructure/Password/IPasswordRuleProvider.cs @@ -16,6 +16,8 @@ public interface IPasswordRuleProvider /// Designed to be called from a FluentValidation rule (see ) /// so client-aware password validation stays inside the single, framework-invoked validation pipeline. /// + string? GetFailure(string? password, string? clientId); + Task GetFailureAsync(string? password, string? clientId); /// diff --git a/src/Contracts/Masa.Auth.Contracts.Admin/Infrastructure/Password/PasswordRuleValidatorExtensions.cs b/src/Contracts/Masa.Auth.Contracts.Admin/Infrastructure/Password/PasswordRuleValidatorExtensions.cs index e3da97f2..1a36430e 100644 --- a/src/Contracts/Masa.Auth.Contracts.Admin/Infrastructure/Password/PasswordRuleValidatorExtensions.cs +++ b/src/Contracts/Masa.Auth.Contracts.Admin/Infrastructure/Password/PasswordRuleValidatorExtensions.cs @@ -15,9 +15,9 @@ public static class PasswordRuleValidatorExtensions IPasswordRuleProvider passwordRuleProvider, Func clientId) { - return ruleBuilder.CustomAsync(async (password, context, cancellation) => + return ruleBuilder.Custom((password, context) => { - var failure = await passwordRuleProvider.GetFailureAsync(password, clientId(context.InstanceToValidate)); + var failure = passwordRuleProvider.GetFailure(password, clientId(context.InstanceToValidate)); if (failure is not null) { context.AddFailure(failure); diff --git a/src/Services/Masa.Auth.Service.Admin/Application/Subjects/Commands/ResetPasswordCommandValidator.cs b/src/Services/Masa.Auth.Service.Admin/Application/Subjects/Commands/ResetPasswordCommandValidator.cs index 2a6163f3..f9c9beb6 100644 --- a/src/Services/Masa.Auth.Service.Admin/Application/Subjects/Commands/ResetPasswordCommandValidator.cs +++ b/src/Services/Masa.Auth.Service.Admin/Application/Subjects/Commands/ResetPasswordCommandValidator.cs @@ -16,8 +16,6 @@ public ResetPasswordCommandValidator(PhoneNumberValidator phoneValidator, IPassw RuleFor(x => x.Voucher).Required().Email(); }); RuleFor(x => x.Captcha).Required(); - // Anonymous forgot-password flow (no login) - the client rule follows whatever ClientId - // the caller passes for this request; falls back to the global DCC rule when absent. RuleFor(x => x.Password).PasswordRule(passwordRuleProvider, x => x.ClientId); RuleFor(x => x.ConfirmPassword).Equal(x => x.Password); } diff --git a/src/Services/Masa.Auth.Service.Admin/Infrastructure/Password/PasswordRuleProvider.cs b/src/Services/Masa.Auth.Service.Admin/Infrastructure/Password/PasswordRuleProvider.cs index dc4a372a..6ae4fef8 100644 --- a/src/Services/Masa.Auth.Service.Admin/Infrastructure/Password/PasswordRuleProvider.cs +++ b/src/Services/Masa.Auth.Service.Admin/Infrastructure/Password/PasswordRuleProvider.cs @@ -26,6 +26,12 @@ public PasswordRuleProvider( _logger = logger; } + public string? GetFailure(string? password, string? clientId) + { + var (rule, prompt) = GetEffectiveRule(clientId); + return ValidatePasswordWithRule(password, rule, prompt); + } + public async Task GetFailureAsync(string? password, string? clientId) { var (rule, prompt) = await GetEffectiveRuleAsync(clientId); @@ -38,6 +44,26 @@ public Task GenerateNewPasswordAsync() return Task.FromResult(GenerateNewPassword(passwordRule)); } + private (string rule, string prompt) GetEffectiveRule(string? clientId) + { + var (rule, prompt) = GetGlobalPasswordRule(); + + if (!string.IsNullOrEmpty(clientId)) + { + var clientConfig = _clientConfigRepository.FindByClientIdAsync(clientId).Result; + if (clientConfig is not null && !string.IsNullOrEmpty(clientConfig.PasswordRule)) + { + rule = clientConfig.PasswordRule; + if (!string.IsNullOrEmpty(clientConfig.PasswordPrompt)) + { + prompt = clientConfig.PasswordPrompt; + } + } + } + + return (rule, prompt); + } + private async Task<(string rule, string prompt)> GetEffectiveRuleAsync(string? clientId) { // 全局 DCC 配置作为回退默认值 diff --git a/src/Services/Masa.Auth.Service.Admin/Program.cs b/src/Services/Masa.Auth.Service.Admin/Program.cs index bc0e95a6..90d13276 100644 --- a/src/Services/Masa.Auth.Service.Admin/Program.cs +++ b/src/Services/Masa.Auth.Service.Admin/Program.cs @@ -1,6 +1,8 @@ // Copyright (c) MASA Stack All rights reserved. // Licensed under the Apache License. See LICENSE.txt in the project root for license information. +using Masa.Auth.Service.Admin.Infrastructure.Password; + var builder = WebApplication.CreateBuilder(args); ValidatorOptions.Global.LanguageManager = new MasaLanguageManager(); @@ -19,6 +21,7 @@ builder.Services.AddAutoInject(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddDaprClient(); builder.Services.AddObjectStorage(option => option.UseAliyunStorage()); diff --git a/src/Services/Masa.Auth.Service.Admin/_Imports.cs b/src/Services/Masa.Auth.Service.Admin/_Imports.cs index 072b1ac4..9f0b92f6 100644 --- a/src/Services/Masa.Auth.Service.Admin/_Imports.cs +++ b/src/Services/Masa.Auth.Service.Admin/_Imports.cs @@ -85,6 +85,7 @@ global using Masa.Auth.Service.Admin.Infrastructure.Message; global using Masa.Auth.Service.Admin.Infrastructure.Middleware; global using Masa.Auth.Service.Admin.Infrastructure.Models; +global using Masa.Auth.Service.Admin.Infrastructure.Password; global using Masa.Auth.Service.Admin.Infrastructure.Sms; global using Masa.Auth.Service.Admin.Infrastructure.Utils; global using Masa.BuildingBlocks.Authentication.Identity; From d0a00b2323f11725281bf337f5f3a4c774ef97ef Mon Sep 17 00:00:00 2001 From: MayueCif Date: Tue, 21 Jul 2026 15:17:15 +0800 Subject: [PATCH 5/6] =?UTF-8?q?=E4=BE=9D=E8=B5=96=E6=B3=A8=E5=85=A5?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E7=BB=A7=E6=89=BFIScopedDependency=E6=96=B9?= =?UTF-8?q?=E5=BC=8F=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将IPasswordRuleProvider和IClientMessageTemplateProvider接口继承IScopedDependency,统一声明为Scoped服务;实现类中移除对IScopedDependency的显式实现,生命周期声明集中在接口层,简化实现类声明并规范依赖注入注册方式。 --- .../Infrastructure/Password/IPasswordRuleProvider.cs | 2 +- .../Infrastructure/Message/ClientMessageTemplateProvider.cs | 2 +- .../Infrastructure/Message/IClientMessageTemplateProvider.cs | 2 +- .../Infrastructure/Password/PasswordRuleProvider.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Contracts/Masa.Auth.Contracts.Admin/Infrastructure/Password/IPasswordRuleProvider.cs b/src/Contracts/Masa.Auth.Contracts.Admin/Infrastructure/Password/IPasswordRuleProvider.cs index dbd5c414..b0f558e6 100644 --- a/src/Contracts/Masa.Auth.Contracts.Admin/Infrastructure/Password/IPasswordRuleProvider.cs +++ b/src/Contracts/Masa.Auth.Contracts.Admin/Infrastructure/Password/IPasswordRuleProvider.cs @@ -8,7 +8,7 @@ namespace Masa.Auth.Contracts.Admin.Infrastructure.Password; /// A client-level rule (stored on ClientConfig) takes precedence; when absent it falls back to /// the global DCC configuration. /// -public interface IPasswordRuleProvider +public interface IPasswordRuleProvider : IScopedDependency { /// /// Validate a password against the client's effective rule. diff --git a/src/Services/Masa.Auth.Service.Admin/Infrastructure/Message/ClientMessageTemplateProvider.cs b/src/Services/Masa.Auth.Service.Admin/Infrastructure/Message/ClientMessageTemplateProvider.cs index 6f27dc5b..1cb22f8f 100644 --- a/src/Services/Masa.Auth.Service.Admin/Infrastructure/Message/ClientMessageTemplateProvider.cs +++ b/src/Services/Masa.Auth.Service.Admin/Infrastructure/Message/ClientMessageTemplateProvider.cs @@ -3,7 +3,7 @@ namespace Masa.Auth.Service.Admin.Infrastructure.Message; -public class ClientMessageTemplateProvider : IClientMessageTemplateProvider, IScopedDependency +public class ClientMessageTemplateProvider : IClientMessageTemplateProvider { private readonly IClientConfigRepository _clientConfigRepository; diff --git a/src/Services/Masa.Auth.Service.Admin/Infrastructure/Message/IClientMessageTemplateProvider.cs b/src/Services/Masa.Auth.Service.Admin/Infrastructure/Message/IClientMessageTemplateProvider.cs index f461dcd8..807a3647 100644 --- a/src/Services/Masa.Auth.Service.Admin/Infrastructure/Message/IClientMessageTemplateProvider.cs +++ b/src/Services/Masa.Auth.Service.Admin/Infrastructure/Message/IClientMessageTemplateProvider.cs @@ -8,7 +8,7 @@ namespace Masa.Auth.Service.Admin.Infrastructure.Message; /// (channel type, scene). Returns null when the client has no matching configuration, /// signalling the caller to fall back to the global DCC SmsOptions/EmailOptions. /// -public interface IClientMessageTemplateProvider +public interface IClientMessageTemplateProvider : IScopedDependency { Task<(string channelCode, string templateCode)?> ResolveAsync(string? clientId, ChannelTypes channelType, int scene); } diff --git a/src/Services/Masa.Auth.Service.Admin/Infrastructure/Password/PasswordRuleProvider.cs b/src/Services/Masa.Auth.Service.Admin/Infrastructure/Password/PasswordRuleProvider.cs index 6ae4fef8..b3e21c64 100644 --- a/src/Services/Masa.Auth.Service.Admin/Infrastructure/Password/PasswordRuleProvider.cs +++ b/src/Services/Masa.Auth.Service.Admin/Infrastructure/Password/PasswordRuleProvider.cs @@ -5,7 +5,7 @@ namespace Masa.Auth.Service.Admin.Infrastructure.Password; -public class PasswordRuleProvider : IPasswordRuleProvider, IScopedDependency +public class PasswordRuleProvider : IPasswordRuleProvider { public const string PASSWORDRULECONFIGNAME = "$public.AppSettings:PasswordRule"; public const string PASSWORDPROMPTCONFIGNAME = "$public.AppSettings:PasswordPrompt"; From ca9c3aaab693ce372c13a25b07b86d8cbb7e50a9 Mon Sep 17 00:00:00 2001 From: MayueCif Date: Tue, 21 Jul 2026 15:19:24 +0800 Subject: [PATCH 6/6] =?UTF-8?q?=E7=A7=BB=E9=99=A4=E5=AF=B9=20Masa.Auth.Ser?= =?UTF-8?q?vice.Admin.Infrastructure.Password=20=E7=9A=84=20using?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移除了对 Masa.Auth.Service.Admin.Infrastructure.Password 命名空间的 using 引用,优化了代码依赖。 --- src/Services/Masa.Auth.Service.Admin/Program.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Services/Masa.Auth.Service.Admin/Program.cs b/src/Services/Masa.Auth.Service.Admin/Program.cs index d924c53a..64256bb1 100644 --- a/src/Services/Masa.Auth.Service.Admin/Program.cs +++ b/src/Services/Masa.Auth.Service.Admin/Program.cs @@ -1,8 +1,6 @@ // Copyright (c) MASA Stack All rights reserved. // Licensed under the Apache License. See LICENSE.txt in the project root for license information. -using Masa.Auth.Service.Admin.Infrastructure.Password; - var builder = WebApplication.CreateBuilder(args); ValidatorOptions.Global.LanguageManager = new MasaLanguageManager();