LDAP/AD 域集成认证
大约 9 分钟约 2751 字
LDAP/AD 域集成认证
简介
企业环境通常使用 Active Directory(AD)或 LDAP 进行统一身份管理。WPF 应用通过 LDAP 协议与 AD 域控通信,实现域账号登录认证、用户信息查询和权限管理。集成 AD 认证可以避免维护独立的用户体系,实现与企业 IT 基础设施的无缝对接。
特点
基础认证
LDAP 认证服务
/// <summary>
/// LDAP/AD 域认证服务
/// </summary>
using System.DirectoryServices;
using System.DirectoryServices.AccountManagement;
public class LdapAuthService
{
private readonly string _domain;
private readonly string _ldapPath;
private readonly ILogger<LdapAuthService> _logger;
public LdapAuthService(IConfiguration config, ILogger<LdapAuthService> logger)
{
_domain = config["ActiveDirectory:Domain"] ?? "company.local";
_ldapPath = config["ActiveDirectory:LdapPath"] ?? $"LDAP://{_domain}";
_logger = logger;
}
// 验证用户名密码
public async Task<LdapUser?> AuthenticateAsync(string username, string password)
{
try
{
using var context = new PrincipalContext(ContextType.Domain, _domain);
// 验证凭据
if (context.ValidateCredentials(username, password, ContextOptions.Negotiate))
{
// 查找用户
var user = UserPrincipal.FindByIdentity(context, IdentityType.SamAccountName, username);
if (user != null)
{
_logger.LogInformation("用户 {Username} 登录成功", username);
return MapToLdapUser(user);
}
}
_logger.LogWarning("用户 {Username} 登录失败:凭据无效", username);
return null;
}
catch (Exception ex)
{
_logger.LogError(ex, "LDAP 认证异常:{Username}", username);
return null;
}
}
// 映射域用户信息
private LdapUser MapToLdapUser(UserPrincipal user)
{
var ldapUser = new LdapUser
{
UserName = user.SamAccountName ?? "",
DisplayName = user.DisplayName ?? user.Name ?? "",
Email = user.EmailAddress ?? "",
Sid = user.Sid.Value,
DistinguishedName = user.DistinguishedName ?? "",
IsEnabled = user.Enabled ?? false,
LastLogon = user.LastLogon
};
// 获取用户组
ldapUser.Groups = GetUserGroups(user);
return ldapUser;
}
}用户信息
查询域用户和组
/// <summary>
/// AD 用户和组查询
/// </summary>
public class LdapUserService
{
private readonly string _domain;
public LdapUserService(string domain)
{
_domain = domain;
}
// 获取用户组
public List<string> GetUserGroups(UserPrincipal user)
{
var groups = new List<string>();
var principalGroups = user.GetAuthorizationGroups();
foreach (var group in principalGroups)
{
if (group is GroupPrincipal gp)
groups.Add(gp.Name);
}
return groups;
}
// 搜索用户
public List<LdapUser> SearchUsers(string keyword)
{
var users = new List<LdapUser>();
using var context = new PrincipalContext(ContextType.Domain, _domain);
using var searcher = new UserPrincipal(context)
{
DisplayName = $"*{keyword}*"
};
using var results = new PrincipalSearcher(searcher);
foreach (var result in results.FindAll())
{
if (result is UserPrincipal user)
{
users.Add(new LdapUser
{
UserName = user.SamAccountName ?? "",
DisplayName = user.DisplayName ?? "",
Email = user.EmailAddress ?? "",
Department = user.GetDepartment()
});
}
}
return users;
}
// 获取 OU 下所有用户
public List<LdapUser> GetUsersByOU(string ouPath)
{
var users = new List<LdapUser>();
using var context = new PrincipalContext(ContextType.Domain, _domain, ouPath);
using var userPrincipal = new UserPrincipal(context) { Enabled = true };
using var searcher = new PrincipalSearcher(userPrincipal);
foreach (var result in searcher.FindAll())
{
if (result is UserPrincipal user)
{
users.Add(new LdapUser
{
UserName = user.SamAccountName ?? "",
DisplayName = user.DisplayName ?? "",
Email = user.EmailAddress ?? ""
});
}
}
return users;
}
}
// 域用户模型
public class LdapUser
{
public string UserName { get; set; } = "";
public string DisplayName { get; set; } = "";
public string Email { get; set; } = "";
public string Department { get; set; } = "";
public string Sid { get; set; } = "";
public string DistinguishedName { get; set; } = "";
public bool IsEnabled { get; set; }
public DateTime? LastLogon { get; set; }
public List<string> Groups { get; set; } = new();
}LDAPS 安全连接
/// <summary>
/// LDAPS(LDAP over SSL/TLS)安全连接
/// </summary>
public class LdapSecureService
{
private readonly string _domain;
private readonly string _ldapSecureUrl;
private readonly int _port = 636; // LDAPS 默认端口
public LdapSecureService(IConfiguration config)
{
_domain = config["ActiveDirectory:Domain"] ?? "company.local";
_ldapSecureUrl = config["ActiveDirectory:LdapSecureUrl"]
?? $"LDAP://{_domain}:636";
}
// 使用 SSL/TLS 连接进行认证
public async Task<LdapUser?> AuthenticateSecureAsync(string username, string password)
{
return await Task.Run(() =>
{
try
{
// 使用 Negotiate + SSL
using var context = new PrincipalContext(
ContextType.Domain,
_domain,
null,
ContextOptions.SecureSocketLayer | ContextOptions.Negotiate);
if (context.ValidateCredentials(username, password,
ContextOptions.SecureSocketLayer | ContextOptions.Negotiate))
{
var user = UserPrincipal.FindByIdentity(
context, IdentityType.SamAccountName, username);
return user != null ? MapToLdapUser(user) : null;
}
return null;
}
catch (Exception ex)
{
Console.WriteLine($"LDAPS 认证失败:{ex.Message}");
return null;
}
});
}
private LdapUser MapToLdapUser(UserPrincipal user)
{
return new LdapUser
{
UserName = user.SamAccountName ?? "",
DisplayName = user.DisplayName ?? "",
Email = user.EmailAddress ?? "",
Sid = user.Sid?.Value ?? "",
IsEnabled = user.Enabled ?? false,
LastLogon = user.LastLogon
};
}
}LDAP 搜索过滤器
/// <summary>
/// LDAP 搜索过滤器构建器
/// </summary>
public class LdapFilterBuilder
{
private readonly List<string> _conditions = new();
// 精确匹配
public LdapFilterBuilder Equals(string attribute, string value)
{
_conditions.Add($"({attribute}={EscapeFilterValue(value)})");
return this;
}
// 通配符匹配
public LdapFilterBuilder Contains(string attribute, string value)
{
_conditions.Add($"({attribute}=*{EscapeFilterValue(value)}*)");
return this;
}
// 以指定值开头
public LdapFilterBuilder StartsWith(string attribute, string value)
{
_conditions.Add($"({attribute}={EscapeFilterValue(value)}*)");
return this;
}
// 以指定值结尾
public LdapFilterBuilder EndsWith(string attribute, string value)
{
_conditions.Add($"({attribute}=*{EscapeFilterValue(value)})");
return this;
}
// NOT 条件
public LdapFilterBuilder NotEquals(string attribute, string value)
{
_conditions.Add($"(!({attribute}={EscapeFilterValue(value)}))");
return this;
}
// 大于等于
public LdapFilterBuilder GreaterOrEqual(string attribute, string value)
{
_conditions.Add($"({attribute}>={EscapeFilterValue(value)})");
return this;
}
// 组合条件(AND)
public string BuildAnd()
{
if (_conditions.Count == 0) return "(objectClass=*)";
if (_conditions.Count == 1) return _conditions[0];
return "(&" + string.Join("", _conditions) + ")";
}
// 组合条件(OR)
public string BuildOr()
{
if (_conditions.Count == 0) return "(objectClass=*)";
if (_conditions.Count == 1) return _conditions[0];
return "(|" + string.Join("", _conditions) + ")";
}
// 转义 LDAP 特殊字符
private static string EscapeFilterValue(string value)
{
return value.Replace("\\", "\\5c")
.Replace("*", "\\2a")
.Replace("(", "\\28")
.Replace(")", "\\29")
.Replace("\0", "\\00")
.Replace("/", "\\2f");
}
}
// 使用示例
// var filter = new LdapFilterBuilder()
// .Equals("objectClass", "user")
// .Contains("displayName", "张")
// .NotEquals("userAccountControl", "514") // 排除禁用账户
// .BuildAnd();
// 结果:(&(objectClass=user)(displayName=*张*)(!(userAccountControl=514)))密码策略检查
/// <summary>
/// 密码策略检查 — 在用户修改密码时进行本地预验证
/// </summary>
public class PasswordPolicyChecker
{
public int MinLength { get; set; } = 8;
public int MaxLength { get; set; } = 128;
public bool RequireUppercase { get; set; } = true;
public bool RequireLowercase { get; set; } = true;
public bool RequireDigit { get; set; } = true;
public bool RequireSpecialChar { get; set; } = true;
public int PasswordHistoryCount { get; set; } = 5;
// 检查密码是否符合策略
public List<string> ValidatePassword(string password)
{
var errors = new List<string>();
if (password.Length < MinLength)
errors.Add($"密码长度不能少于 {MinLength} 个字符");
if (password.Length > MaxLength)
errors.Add($"密码长度不能超过 {MaxLength} 个字符");
if (RequireUppercase && !password.Any(char.IsUpper))
errors.Add("密码必须包含大写字母");
if (RequireLowercase && !password.Any(char.IsLower))
errors.Add("密码必须包含小写字母");
if (RequireDigit && !password.Any(char.IsDigit))
errors.Add("密码必须包含数字");
if (RequireSpecialChar && !password.Any(c => "!@#$%^&*()_+-=[]{}|;':\",./<>?".Contains(c)))
errors.Add("密码必须包含特殊字符");
return errors;
}
// 修改 AD 用户密码
public async Task<bool> ChangePasswordAsync(
string domain, string username,
string oldPassword, string newPassword)
{
return await Task.Run(() =>
{
try
{
using var context = new PrincipalContext(ContextType.Domain, domain);
using var user = UserPrincipal.FindByIdentity(
context, IdentityType.SamAccountName, username);
if (user == null) return false;
// 先验证旧密码
if (!context.ValidateCredentials(username, oldPassword))
return false;
// 设置新密码
user.SetPassword(newPassword);
user.Save();
return true;
}
catch (Exception ex)
{
Console.WriteLine($"修改密码失败:{ex.Message}");
return false;
}
});
}
}账号锁定与解锁
/// <summary>
/// AD 账号锁定管理
/// </summary>
public class AccountLockoutService
{
private readonly string _domain;
public AccountLockoutService(string domain)
{
_domain = domain;
}
// 检查账号是否被锁定
public bool IsAccountLocked(string username)
{
using var context = new PrincipalContext(ContextType.Domain, _domain);
using var user = UserPrincipal.FindByIdentity(
context, IdentityType.SamAccountName, username);
return user?.IsAccountLockedOut() ?? false;
}
// 解锁账号
public bool UnlockAccount(string username)
{
using var context = new PrincipalContext(ContextType.Domain, _domain);
using var user = UserPrincipal.FindByIdentity(
context, IdentityType.SamAccountName, username);
if (user != null)
{
user.UnlockAccount();
user.Save();
return true;
}
return false;
}
// 启用/禁用账号
public bool SetAccountEnabled(string username, bool enabled)
{
using var context = new PrincipalContext(ContextType.Domain, _domain);
using var user = UserPrincipal.FindByIdentity(
context, IdentityType.SamAccountName, username);
if (user != null)
{
user.Enabled = enabled;
user.Save();
return true;
}
return false;
}
// 获取密码过期信息
public DateTime? GetPasswordExpirationDate(string username)
{
using var context = new PrincipalContext(ContextType.Domain, _domain);
using var user = UserPrincipal.FindByIdentity(
context, IdentityType.SamAccountName, username);
return user?.PasswordExpirationDate;
}
// 检查密码是否即将过期
public bool IsPasswordExpiringSoon(string username, int daysThreshold = 7)
{
var expiry = GetPasswordExpirationDate(username);
if (expiry == null) return false;
return expiry.Value <= DateTime.Now.AddDays(daysThreshold);
}
}权限映射
AD 组映射应用角色
/// <summary>
/// AD 组 → 应用角色映射
/// </summary>
public class RoleMappingService
{
// AD 组名 → 应用角色
private readonly Dictionary<string, AppRole> _groupRoleMap = new(StringComparer.OrdinalIgnoreCase)
{
{ "APP_Administrators", AppRole.Admin },
{ "APP_Operators", AppRole.Operator },
{ "APP_Engineers", AppRole.Engineer },
{ "APP_Viewers", AppRole.Viewer },
{ "Domain Admins", AppRole.Admin }
};
// 根据用户组确定角色
public AppRole ResolveRole(LdapUser user)
{
foreach (var group in user.Groups)
{
if (_groupRoleMap.TryGetValue(group, out var role))
return role;
}
return AppRole.Viewer; // 默认最低权限
}
// 获取用户权限列表
public List<string> GetPermissions(AppRole role)
{
return role switch
{
AppRole.Admin => new List<string> { "all" },
AppRole.Engineer => new List<string> { "recipe.edit", "device.configure", "alarm.ack", "report.view" },
AppRole.Operator => new List<string> { "recipe.view", "device.operate", "alarm.ack" },
AppRole.Viewer => new List<string> { "recipe.view", "report.view" },
_ => new List<string>()
};
}
}
public enum AppRole
{
Viewer,
Operator,
Engineer,
Admin
}登录界面集成
WPF 登录窗口
/// <summary>
/// WPF 域认证登录
/// </summary>
public partial class LoginWindow : Window
{
private readonly LdapAuthService _authService;
private readonly RoleMappingService _roleService;
public LoginWindow(LdapAuthService authService, RoleMappingService roleService)
{
InitializeComponent();
_authService = authService;
_roleService = roleService;
}
private async void LoginButton_Click(object sender, RoutedEventArgs e)
{
var username = UsernameBox.Text.Trim();
var password = PasswordBox.Password;
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
{
ErrorText.Text = "请输入用户名和密码";
return;
}
LoginButton.IsEnabled = false;
LoadingOverlay.Visibility = Visibility.Visible;
try
{
var user = await _authService.AuthenticateAsync(username, password);
if (user != null)
{
var role = _roleService.ResolveRole(user);
// 存储登录信息
AppSession.CurrentUser = user;
AppSession.CurrentRole = role;
// 打开主窗口
var mainWindow = new MainWindow();
mainWindow.Show();
Close();
}
else
{
ErrorText.Text = "用户名或密码错误";
}
}
catch (Exception ex)
{
ErrorText.Text = $"连接域控失败:{ex.Message}";
}
finally
{
LoginButton.IsEnabled = true;
LoadingOverlay.Visibility = Visibility.Collapsed;
}
}
}优点
缺点
总结
AD 域集成推荐使用 System.DirectoryServices.AccountManagement(比原始 DirectoryServices 更简洁)。核心流程:PrincipalContext → ValidateCredentials → 查找用户 → 获取组 → 映射角色。建议实现离线缓存机制,域控不可用时允许缓存用户登录。
关键知识点
- 先分清主题属于界面层、ViewModel 层、线程模型还是设备接入层。
- WPF 文章真正的价值在于把 UI、数据、命令、线程和资源关系讲清楚。
- 上位机场景里,稳定性和异常恢复常常比界面花哨更重要。
- 安全类主题的关键不只在认证成功,而在于权限边界、证书信任链和审计链路是否完整。
项目落地视角
- 优先明确 DataContext、绑定路径、命令源和线程切换位置。
- 涉及设备时,补齐超时、重连、日志、告警和资源释放策略。
- 复杂控件最好提供最小可运行页面,便于后续复用和排障。
- 明确令牌生命周期、刷新策略、作用域、Claims 和失败返回模型。
常见误区
- 把大量逻辑堆进 code-behind,导致页面难测难维护。
- 在后台线程直接操作 UI,或忽略 Dispatcher 切换。
- 只验证正常路径,不验证设备掉线、权限缺失和资源泄漏。
- 只验证登录成功,不验证权限收敛和令牌失效场景。
进阶路线
- 继续向 MVVM 工具链、控件可复用性、性能优化和视觉树诊断深入。
- 把主题放回真实设备流程,思考启动、连接、采集、显示、告警和恢复。
- 沉淀成控件库、通信层和诊断工具,提高整套客户端的复用度。
- 继续深入零信任、细粒度授权、证书自动化和密钥轮换。
适用场景
- 当你准备把《LDAP/AD 域集成认证》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合桌面业务系统、监控看板、工控上位机和设备交互界面。
- 当系统需要同时处理复杂 UI、后台任务和硬件通信时,这类主题尤为关键。
落地建议
- 优先明确 View、ViewModel、服务层和设备层边界,避免代码隐藏过重。
- 涉及实时刷新时,提前设计 UI 线程切换、节流和资源释放。
- 对硬件通信、日志、告警和异常恢复建立标准流程。
排错清单
- 先检查 DataContext、绑定路径、INotifyPropertyChanged 和命令状态是否正常。
- 排查 Dispatcher 调用、死锁、后台线程直接操作 UI 的问题。
- 出现内存增长时,优先检查事件订阅、图像资源和窗口生命周期。
复盘问题
- 如果把《LDAP/AD 域集成认证》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《LDAP/AD 域集成认证》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《LDAP/AD 域集成认证》最大的收益和代价分别是什么?
