C# 完美操作 Active Directory 详细总结,轻松玩转域管理
前言
嗨,大家好!
在这个数据信息飞速发展的 21 世纪,数据安全成为了每个企业关注的焦点,保护企业数据安全日益成为企业工作中的重中之重。
域服务器,尤其是微软的 Active Directory(AD),因其显著的安全优势,已成为不少企业的首选。
简单来说,域服务器就是一个 “超级管理员”,它可以集中管理网络中的所有用户、计算机和其他资源。
通过域服务器,企业可以轻松地为用户分配和管理权限,确保数据安全;也可以通过组策略,统一管理网络中所有计算机的设置和安全策略,更好地保护敏感信息。
域服务器天然就是一个企业员工信息的数据库,将业务系统的身份鉴权跟域服务器紧密结合,无疑已经成为安全技术发展的趋势。
C# 拥有丰富的类库来与 Active Directory(AD)互动,但使用时很不方便,因此我根据项目的实际业务需求,造了一个轮子,封装了一些常用的操作 AD 的方法,简化了与 AD 的交互,用起来还挺方便的。
今天,我很高兴与大家分享这些便利,希望能让你的开发之旅充满乐趣和效率!
下面,让我们一起来看看具体的实现步骤吧!
Step By Step 代码
1. 创建配置文件
首先,需要创建一个配置文件如 LDAPConfig.config
,用于保存域的相关配置信息,内容如下:
<?xml version="1.0" encoding="utf-8" ?>
<LDAPConfiguration><Host><URL>192.168.0.120:389</URL><LoginDN>CN=corp_test,CN=Users,DC=jacky,DC=com</LoginDN><Password>+6nkUhDs5lmcfMYS/qe7Qw==</Password></Host><UserSearch><SearchBase>OU=某某市软件技术有限公司,DC=corp,DC=com</SearchBase><SearchFilter>(&(objectClass=Person)(sAMAccountName={0}))</SearchFilter><UserAttribute>sAMAccountName,memberOf,displayName</UserAttribute></UserSearch><AdminGroup>AndoErp_Administrators</AdminGroup>
</LDAPConfiguration>
2. 创建配置文件实体类
接下来,创建一个配置文件实体类 LDAPConfigModel
,读取和解析配置文件中的信息,留意注释
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Serialization;
using Common.Util;namespace Common.Model
{[XmlRoot("LDAPConfiguration")]public class LDAPConfigModel{/// <summary>/// AD/LDAP 服务器和绑定帐号配置/// </summary>[XmlElement("Host")]public LdapHostSetting LdapHost { get; set; }/// <summary>/// 用户搜索配置/// </summary>[XmlElement("UserSearch")]public UserSearchSetting UserSearch { get; set; }/// <summary>/// 管理员组/// </summary>public string AdminGroup { get; set; }}public class LdapHostSetting{/// <summary>/// AD/LDAP 服务器 URL/// </summary>public string URL { get; set; }/// <summary>/// 绑定帐号的 distinguished name/// </summary>public string LoginDN { get; set; }/// <summary>/// 绑定帐号的密码(加密状态)/// </summary>public string Password { get; set; }/// <summary>/// 绑定帐号的密码/// </summary>[XmlIgnore]public string SafePassword{get { return EncryptUtil.AESDecode(Password); }set { Password = EncryptUtil.AESEncode(value); }}}public class UserSearchSetting{/// <summary>/// 搜索路径/// </summary>public string SearchBase { get; set; }/// <summary>/// 搜索过滤器/// </summary>public string SearchFilter { get; set; }/// <summary>/// 搜索属性/// </summary>public string UserAttribute { get; set; }}
}
3. 创建一个域常用操作方法的封装类
然后,创建一个名为 LdapUtil
的静态类,封装所有与域相关的操作方法,重点:留意代码注释
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.DirectoryServices.Protocols;
using Common.Model;
using System.Net;
using Ando.ERP.Logger;
using System.IO;
using System.Reflection;namespace Common.Util
{/// <summary>/// 域 LDAP/AD 常用操作方法封装类/// </summary>public static class LdapUtil{static readonly LDAPConfigModel ldapConfig = null;/// <summary>/// 静态构造方法,读取配置文件,初始化 ldapConfig 对象/// </summary>static LdapUtil(){if (ldapConfig != null) return;string ldapConfigPath;if (AppDomain.CurrentDomain.SetupInformation.PrivateBinPath != null)ldapConfigPath = Path.Combine(AppDomain.CurrentDomain.SetupInformation.PrivateBinPath, "ConfigFile", "LDAPConfig.config");elseldapConfigPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ConfigFile", "LDAPConfig.config");if (!File.Exists(ldapConfigPath)){Assembly myAssembly = Assembly.GetExecutingAssembly();FileInfo dllFile = new FileInfo(myAssembly.Location);string path = dllFile.Directory.FullName;ldapConfigPath = Path.Combine(path, "ConfigFile", "LDAPConfig.config");}ldapConfig = ConfigUtil.Deserialize<LDAPConfigModel>(ldapConfigPath);}#region common private method/// <summary>/// 连接域服务器/// </summary>/// <param name="loginDN">域用户</param>/// <param name="loginPassword">域用户密码</param>/// <param name="ldapConnection">LdapConnection 对象</param>private static void Connect(string loginDN, string loginPassword, ref LdapConnection ldapConnection){var networkCredential = new NetworkCredential(loginDN, loginPassword);ldapConnection.SessionOptions.SecureSocketLayer = false;ldapConnection.SessionOptions.ProtocolVersion = 3;ldapConnection.AuthType = AuthType.Basic;ldapConnection.Credential = networkCredential;ldapConnection.Bind();}/// <summary>/// <para>通过 LDAP 配置信息连接域服务器</para>/// <para>如果参数 cLoginDN 不为空,使用 cLoginDN 作为 loginDN</para>/// <para>如果参数 cLoginPassword 不为空,使用 cLoginPassword 作为 loginPassword</para>/// </summary>/// <param name="ldapConfig"></param>/// <param name="ldapConnection"></param>/// <param name="cLoginDN"></param>/// <param name="cLoginPassword"></param>private static void Connect(ref LdapConnection ldapConnection, string cLoginDN = "", string cLoginPassword = ""){string loginDN = string.IsNullOrEmpty(cLoginDN) ? ldapConfig.LdapHost.LoginDN : cLoginDN;string loginPassword = string.IsNullOrEmpty(cLoginPassword) ? ldapConfig.LdapHost.SafePassword : cLoginPassword;AndoErpLogger.DEFAULT.DebugFormat("Connecting to LDAP/AD server [{0}] with account [{1}]", ldapConfig.LdapHost.URL, loginDN);Connect(loginDN, loginPassword, ref ldapConnection);}/// <summary>/// 通过 Action 执行一些自定义操作,LDAP Util 核心方法/// </summary>/// <param name="ldapConfig"></param>/// <param name="cLoginDN"></param>/// <param name="cLoginPassword"></param>/// <param name="func"></param>private static void LDAPCore(string cLoginDN, string cLoginPassword, Action<LdapConnection> action){LdapConnection ldapConnection = null;try{ldapConnection = new LdapConnection(ldapConfig.LdapHost.URL);Connect(ref ldapConnection, cLoginDN, cLoginPassword);action(ldapConnection);}catch{throw;}finally{if (ldapConnection != null) ldapConnection.Dispose();}}/// <summary>/// 通过用户搜索配置搜索域数据/// </summary>/// <param name="ldapConnection"></param>/// <param name="sizeLimit">指定返回的实体数</param>/// <param name="searchPath">搜索路径</param>/// <param name="filter">过滤字符串</param>/// <param name="isSubtree">是否深度搜索</param>/// <param name="attrs">搜索属性</param>private static SearchResultEntryCollection Search(LdapConnection ldapConnection, int sizeLimit, string searchPath, string filter, params string[] attrs){try{SearchRequest request = new SearchRequest(searchPath, filter, SearchScope.Subtree, attrs);if (sizeLimit > 0) request.SizeLimit = sizeLimit;SearchResponse response = (SearchResponse)ldapConnection.SendRequest(request);return response.Entries;}catch (DirectoryOperationException e){// 返回此异常中已处理的所有数据// 因为 LDAP/AD 搜索默认在 1000 以内,超过就会报这个错SearchResponse response = (SearchResponse)e.Response;return response.Entries;}catch (Exception ex){throw;}}#endregion#region business method/// <summary>/// 获取指定域用户的 distinguished name 值/// </summary>/// <param name="ldapConfig"></param>/// <param name="userName"></param>/// <returns></returns>private static LdpaUserInfo GetLDAPUserDN(string userName){var ldpaUser = new LdpaUserInfo();LDAPCore(null, null, (ldapConnection) =>{var userEntries = GetUserEntries(ldapConnection, userName, 50);var userEntry = userEntries[0];ldpaUser.UserDN = userEntry.DistinguishedName;ldpaUser.UserGroupList = GetAttributeValues(userEntry, "memberOf");ldpaUser.UserDisplayName = GetAttributeValues(userEntry, "displayName")[0];});return ldpaUser;}/// <summary>/// 获取用户搜索配置中的所有域帐户实体/// </summary>/// <param name="ldapConfig"></param>/// <param name="ldapConnection"></param>/// <param name="userName"></param>/// <param name="sizeLimit"></param>/// <returns></returns>private static SearchResultEntryCollection GetUserEntries(LdapConnection ldapConnection, string userName, int sizeLimit){string userSearchBasePath = ldapConfig.UserSearch.SearchBase;string userSearchFilter = ldapConfig.UserSearch.SearchFilter;string[] userAttribute = ldapConfig.UserSearch.UserAttribute.Split(',');if (!string.IsNullOrEmpty(userName)){userSearchFilter = string.Format(userSearchFilter, userName);}var userEntries = Search(ldapConnection, sizeLimit, userSearchBasePath, userSearchFilter, userAttribute);if (userEntries == null || userEntries.Count == 0){string exceptionMsg = string.Format("没有找到符合条件的域用户,请检查用户搜索配置。搜索路径: [{0}], 过滤条件: [{1}]", userSearchBasePath, userSearchFilter);throw new LdapException(exceptionMsg);}return userEntries;}/// <summary>/// 获取域帐户实体配置中的属性的值/// </summary>/// <param name="entry"></param>/// <param name="attributeName"></param>/// <returns></returns>private static List<string> GetAttributeValues(SearchResultEntry entry, string attributeName){var attributes = entry.Attributes;var attributeObj = attributes[attributeName];if (attributeObj == null){return null;}List<string> valueList = new List<string>(); var attributeValues = attributes[attributeName].GetValues(typeof(string));foreach (var attributeValue in attributeValues){valueList.Add(CommonUtil.TranNull<string>(attributeValue));}return valueList;}/// <summary>/// 检查登录帐户是否存在域中/// </summary>/// <param name="userName"></param>/// <param name="password"></param>/// <param name="ldapConfig"></param>/// <returns></returns>public static bool IsExistLDAPUser(string userName, string password, out bool isAdministrator, out string userDisplayName){bool result = false;bool userIsAdmin = false;string userShowName = string.Empty;try{var ldpaUser = GetLDAPUserDN(userName);string userDN = ldpaUser.UserDN;userShowName = ldpaUser.UserDisplayName;LDAPCore(userDN, password, (ldapConnection) =>{if (ldpaUser.UserGroupList == null || ldpaUser.UserGroupList.Count == 0)userIsAdmin = false;else{var findResult = ldpaUser.UserGroupList.First(x => x.IndexOf(ldapConfig.AdminGroup, StringComparison.OrdinalIgnoreCase) > 0);if (string.IsNullOrEmpty(findResult))userIsAdmin = false;elseuserIsAdmin = true;}result = true;});}catch (Exception ex){// TODO 可将错误信息写到日志中,方便排查原因result = false;}isAdministrator = userIsAdmin;userDisplayName = userShowName;return result;}/// <summary>/// 获取用户搜索配置中的所有域帐户的 displayName 的值/// </summary>/// <param name="ldapConfig"></param>/// <param name="userName"></param>/// <returns></returns>public static List<string> GetAllUsers(){var list = new List<string>();LDAPCore(null, null, (ldapConnection) =>{var userEntries = GetUserEntries(ldapConnection, "*", 350);foreach (var userEntry in userEntries){var userDisplayName = GetAttributeValues((SearchResultEntry)userEntry, "displayName")[0];list.Add(userDisplayName);}});return list;}#endregion#region Inner class/// <summary>/// 域用户基本信息/// </summary>sealed class LdpaUserInfo{public string UserDN { get; set; }public List<string> UserGroupList { get; set; }public string UserDisplayName { get; set; }}#endregion}
}
4. 使用示例
最后,我们来看一下如何使用这个封装类来执行一些基本操作,比如登录
/// <summary>
/// 登录
/// </summary>
/// <param name="userName"></param>
/// <param name="password"></param>
/// <returns></returns>
public void Login(string userName, string password)
{var hasUser = LdapUtil.IsExistLDAPUser(userName, password, out bool isAdministrator, out string userDisplayName);if (hasUser){// 域用户存在,登录成功,继续处理后续业务}else{// 域用户不存在,登录失败}
}
总结
好了,今天的分享就到这里啦!
通过以上的封装,我们可以更高效地与 Active Directory 进行交互,无论是用户的身份验证、信息查询,还是其他操作,这些方法都能帮助简化代码,提高开发效率,你可以把它用在自己的项目里,根据自己的实际业务需求,继续添加新的业务处理方法或删减其中一些方法!
随着数字化进程的加速,越来越多的企业转向域服务器来高效管理网络环境,C# 的灵活性和强大功能使其与 Active Directory 的结合成为了一种自然的选择,希望这篇教程能够给你提供一些实用的操作方法和思路。
最后,如果你有更好的想法或建议,欢迎留言讨论!
往期精彩
- 闲话 .NET(7):.NET Core 能淘汰 .NET FrameWork 吗?
- 常用的 4 种 ORM 框架(EF Core,SqlSugar,FreeSql,Dapper)对比总结
我是老杨,一个执着于编程乐趣、至今奋斗在一线的 10年+ 资深研发老鸟,是软件项目管理师,也是快乐的程序猿,持续免费分享全栈实用编程技巧、项目管理经验和职场成长心得!欢迎关注老杨的公众号,更多干货等着你!
相关文章:

C# 完美操作 Active Directory 详细总结,轻松玩转域管理
前言 嗨,大家好! 在这个数据信息飞速发展的 21 世纪,数据安全成为了每个企业关注的焦点,保护企业数据安全日益成为企业工作中的重中之重。 域服务器,尤其是微软的 Active Directory(AD)&…...

PCL 点云配准 KD-ICP算法(精配准)
目录 一、概述 1.1原理 1.2实现步骤 1.3应用场景 二、代码实现 2.1关键函数 2.1.1 加载点云函数 2.1.2 构建KD树函数 2.1.3 KD-ICP配准函数 2.1.4 点云可视化函数 2.2完整代码 三、实现效果 PCL点云算法汇总及实战案例汇总的目录地址链接: PCL点云算法…...

uniapp打包安卓apk步骤
然后安装在手机上就可以啦...

Springboot 整合 Java DL4J 实现安防监控系统
🧑 博主简介:历代文学网(PC端可以访问:https://literature.sinhy.com/#/literature?__c1000,移动端可微信小程序搜索“历代文学”)总架构师,15年工作经验,精通Java编程,…...

【数据结构与算法】第1课—算法复杂度
文章目录 1. 数据结构2. 算法3. 算法效率4. 算法复杂度5. 算法时间复杂度5.1 大O的渐进表示法5.2 时间复杂度示例 6. 空间复杂度6.1 练习16.2 练习26.3 练习3 1. 数据结构 数据结构是计算机存储、组织数据的方式,指相互之间存在一种和多种特定关系的数据元素的集合&…...
利用高德API获取整个城市的公交路线并可视化(五)
如果说我比别人看得更远些,那是因为我站在了巨人的肩上。——牛顿 参考:使用高德API获取公交线路数据,无需代码_实时公交api-CSDN博客 记录于2024年10月,因数据获取受网站更新策略等影响可能会失效,故记录写作时间,同时拾人牙慧,优化了后半部分数据直接导出为csv和shp…...
DNS:互联网域名系统的核心
什么是 DNS? DNS(Domain Name System,域名系统)是互联网的一项基础服务,它负责将人类容易记忆的域名(如 www.example.com)转换成计算机可以识别的 IP 地址(如 192.0.2.1)…...

小猿口算炸鱼脚本
目录 写在前面: 一、关于小猿口算: 二、代码逻辑 1.数字识别 2.答题部分 三、代码分享: 补充:软件包下载 写在前面: 最近小猿口算已经被不少大学生攻占,小学生直呼有挂。原本是以为大学生都打着本…...

浅谈云原生--微服务、CICD、Serverless、服务网格
往期推荐 浅学React和JSX-CSDN博客 一文搞懂大数据流式计算引擎Flink【万字详解,史上最全】-CSDN博客 一文入门大数据准流式计算引擎Spark【万字详解,全网最新】_大数据 spark-CSDN博客 目录 1. 云原生概念和特点 2. 常见云模式 3. 云对外提供服务的…...

android app执行shell命令视频课程补充android 10/11适配-千里马android
(https://blog.csdn.net/learnframework/article/details/120103471) https://blog.csdn.net/learnframework/article/details/120103471 hi,有学员在学习跨进程通信专题课程时候,在实战app执行一个shell命令的项目时候,对课程本身的android …...

C++笔记-UTF8和UTF8-dom的区别
在文件格式上,UTF-8 和 UTF-8-BOM 是两种不同的编码方式,其中 UTF-8-BOM 包含字节顺序标记(BOM),而 UTF-8 则不包含。 UTF-8: UTF-8 是一种以字节为单位的可变长度字符编码,常用于以字节为单位…...

“探索Adobe Photoshop 2024:订阅方案、成本效益分析及在线替代品“
设计师们对Adobe Photoshop这款业界领先的图像编辑软件肯定不会陌生。如果你正考虑加入Photoshop的用户行列,可能会对其价格感到好奇。Photoshop的价值在于其强大的功能,而它的价格也反映了这一点。下面,我们就来详细了解一下Adobe Photoshop…...

网页复制粘贴助手,Chrome网页复制插件(谷歌浏览器复制插件)
一款解决网页限制复制问题的插件,当你遇到限制复制粘贴和右键的网页是不是很头痛?安装这个插件后,点下插件按钮就能解决了 碰到这种情况 也是非常头疼 chrome拓展-chrome插件-强制复制 当我们浏览网页的时候,看到感兴趣的内容就…...
【C++刷题】力扣-#118-杨辉三角
题目描述 给定一个非负整数 numRows,生成杨辉三角的前 numRows 行。在杨辉三角中,每个数是它正上方两个数的和。 示例 示例 1: 输入: numRows 5 输出: [[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1]]示例 2: 输入: numRows 1 输出: [[1]]题解 这个问题…...

Linux下的环境变量
目录 1.引言 1.1bash的部分工作 1.2main函数也有参数 1.3我们可以通过给main函数传入不同的参数,让同一份代码实现不同的功能 1.4先认识一个环境变量PATH,帮助Linux找到指令程序的地址 2.环境变量 2.1环境变量的概念 2.2见见其他的环境变量 2…...

Edge论文的创新点
创新点及其来源 1. 从灰度边缘重建RGB图像的方法(EdgRec) 基于的方法:传统的重建方法,如使用自动编码器或生成模型来重建正常样本的图像,并通过对原始图像和重建图像的比较来检测异常。 重建过程: 训练阶…...

ComfyUI 高级实战:实现华为手机的AI消除功能
大家好,我是每天分享AI应用的萤火君! 不知道大家是否还记得华为 Pura 70的「AI消除」事件,当时使用 华为Pura 70 系列手机的智能消除功能时,该功能可以被用来消除照片中女性胸口处的衣物,这一功能曾引发广泛的关注和伦…...

我记得我曾喜欢过冬天
写在前面 1316 字 | 感触 | 世界 | 情感 | 体验 | 经历 | 想法 | 认知 正文 晚上出门,起电单车,很冷。冻得有些发抖。下车,我第一时间和珍发了消息。 我说,居然在四川感受到了哈尔滨的温度。 哈尔滨的夏天很热,但哈尔…...

最新夜间数据集发布LoLI-Street: 33000帧数据,涵盖19000个目标
最新夜间数据集发布LoLI-Street: 33000帧数据,涵盖19000个目标 Abstract 低光照图像增强(LLIE)对于许多计算机视觉任务至关重要,包括目标检测、跟踪、分割和场景理解。尽管已有大量研究致力于提高在低光照条件下捕捉的低质量图像…...

反向传播算法与随机搜索算法的比较
反向传播算法与随机搜索算法的比较 在这篇文章中,我们将通过一个简单的线性回归问题来比较反向传播算法和随机搜索算法的性能。我们将使用Python代码来实现这两种算法,并可视化它们的梯度下降过程。 反向传播算法 反向传播算法是深度学习和神经网络训…...
java调用dll出现unsatisfiedLinkError以及JNA和JNI的区别
UnsatisfiedLinkError 在对接硬件设备中,我们会遇到使用 java 调用 dll文件 的情况,此时大概率出现UnsatisfiedLinkError链接错误,原因可能有如下几种 类名错误包名错误方法名参数错误使用 JNI 协议调用,结果 dll 未实现 JNI 协…...
spring:实例工厂方法获取bean
spring处理使用静态工厂方法获取bean实例,也可以通过实例工厂方法获取bean实例。 实例工厂方法步骤如下: 定义实例工厂类(Java代码),定义实例工厂(xml),定义调用实例工厂ÿ…...
土地利用/土地覆盖遥感解译与基于CLUE模型未来变化情景预测;从基础到高级,涵盖ArcGIS数据处理、ENVI遥感解译与CLUE模型情景模拟等
🔍 土地利用/土地覆盖数据是生态、环境和气象等诸多领域模型的关键输入参数。通过遥感影像解译技术,可以精准获取历史或当前任何一个区域的土地利用/土地覆盖情况。这些数据不仅能够用于评估区域生态环境的变化趋势,还能有效评价重大生态工程…...
LLM基础1_语言模型如何处理文本
基于GitHub项目:https://github.com/datawhalechina/llms-from-scratch-cn 工具介绍 tiktoken:OpenAI开发的专业"分词器" torch:Facebook开发的强力计算引擎,相当于超级计算器 理解词嵌入:给词语画"…...

《基于Apache Flink的流处理》笔记
思维导图 1-3 章 4-7章 8-11 章 参考资料 源码: https://github.com/streaming-with-flink 博客 https://flink.apache.org/bloghttps://www.ververica.com/blog 聚会及会议 https://flink-forward.orghttps://www.meetup.com/topics/apache-flink https://n…...
[Java恶补day16] 238.除自身以外数组的乘积
给你一个整数数组 nums,返回 数组 answer ,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。 题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。 请 不要使用除法,且在 O(n) 时间复杂度…...
Swagger和OpenApi的前世今生
Swagger与OpenAPI的关系演进是API标准化进程中的重要篇章,二者共同塑造了现代RESTful API的开发范式。 本期就扒一扒其技术演进的关键节点与核心逻辑: 🔄 一、起源与初创期:Swagger的诞生(2010-2014) 核心…...
Python 包管理器 uv 介绍
Python 包管理器 uv 全面介绍 uv 是由 Astral(热门工具 Ruff 的开发者)推出的下一代高性能 Python 包管理器和构建工具,用 Rust 编写。它旨在解决传统工具(如 pip、virtualenv、pip-tools)的性能瓶颈,同时…...

Docker 本地安装 mysql 数据库
Docker: Accelerated Container Application Development 下载对应操作系统版本的 docker ;并安装。 基础操作不再赘述。 打开 macOS 终端,开始 docker 安装mysql之旅 第一步 docker search mysql 》〉docker search mysql NAME DE…...

【从零开始学习JVM | 第四篇】类加载器和双亲委派机制(高频面试题)
前言: 双亲委派机制对于面试这块来说非常重要,在实际开发中也是经常遇见需要打破双亲委派的需求,今天我们一起来探索一下什么是双亲委派机制,在此之前我们先介绍一下类的加载器。 目录 编辑 前言: 类加载器 1. …...