Storer.ActiveDirectory - Active Directory 用户/组封装类






4.79/5 (15投票s)
处理 Active Directory 中的用户和组的几个类。
引言
近三年来,我一直通过 C# 与 Active Directory 进行交互。在使用 DirectoryEntry
风格的访问时,当处理多个用户时,效率低下且麻烦,尤其是当你的目标仅仅是只读访问时。此外,当你试图编写一个快速的小程序来解决某个问题,而不是一个长达数月的项目时,记住所有不同的参数可能很困难。
因此,对于所有使用 Active Directory(或正在考虑使用)的人来说,这个类库就是为你准备的!
想法是这样的
System.DirectoryServices
有一个很棒的 Active Directory 搜索功能,称为 DirectorySearcher
。这个类比 DirectoryEntry
更快地访问用户或组对象中的数据。
想法是这样的:建立一种方法,使其既快速又简单地访问 Active Directory 用户/组。通过我的类,这可以很简单地做到
static void Main(string[] args)
{
try
{
// Find the User "Administrator" and View/Modify
User _User = Search.ForUser(User.Properties.SAMACCOUNTNAME,
"Administrator");
Console.WriteLine("Username: {0}",
_User.SAMAccountName);
Console.WriteLine("Full Name: {0}",
_User.FullName);
Console.WriteLine("DistinguishedName: {0}",
_User.DistinguishedName);
Console.WriteLine("Logon Count: {0}",
_User.LogonCount);
Console.WriteLine("Object SID (string) {0}",
_User.ObjectSIDString);
foreach (string GroupName in _User.TokenGroups)
Console.WriteLine("Token Group: {0}",
GroupName);
_User.Enabled = false;
_User.FirstName = "Bob";
_User.MiddleInitial = "T";
_User.LastName = "Admin";
_User.SaveChanges();
// Find the Group "Administrators" and View/Modify
Group _Group = Search.ForGroup(Group.Properties.COMMONNAME,
"Administrators");
Console.WriteLine("Group: {0}",
_Group.CommonName);
foreach (string _Member in _Group.Members)
Console.WriteLine("Member: {0}", _Member);
_Group.AddMember(_User.DistinguishedName);
_Group.SaveChanges();
}
catch (Exception Error)
{
Console.WriteLine("Error: {0}", Error);
}
}
对其的扩展是查找用户或组的列表,可以通过 Search.ForUsers(...)
或 Search.ForGroups(...)
方法来完成。
使用代码
只需将 Storer.ActiveDirectory
的引用添加到你的代码中,然后为你的项目使用以下类:
Storer.ActiveDirectory.User
:用户类。Storer.ActiveDirectory.Group
:组类。Storer.ActiveDirectory.Search
:用于查找用户、组和 DirectoryEntries 的静态方法。Storer.ActiveDirectory.Methods
:用于执行有用操作的静态方法,例如获取域名、验证用户(带密码)或将 Byte[] ObjectSID 转换为 ObjectSIDString(这在 User/Group 类中已经为你完成)。
如果你要使用自定义搜索参数和/或移动 DirectoryEntry 对象,你可能还需要添加对 System.DirectoryServices
的引用。
另外:不要担心在 User/Group 代码中处理任何未使用的 COM 对象。任何必要的处理都已为你处理完毕,除非你将 DirectoryEntry
对象传递给 Search.ForUsers(...)
方法,在这种情况下,你将必须处理 Search Root 的释放;我建议为此使用 using
子句。
关注点
如上所述,秘诀在于使用 DirectorySearcher
类而不是 DirectoryEntry
类来访问用户或组对象的属性。
从 DirectorySearcher
访问值与从 DirectoryEntry
访问值相同。我将此过程封装在以下方法中:
private void PopulateFields(ResultPropertyCollection Collection)
{
if (Collection.Contains(Properties.ACCOUNTCONTROL))
AccountControl = (int?)Collection[Properties.ACCOUNTCONTROL][0] ?? 0;
if (Collection.Contains(Properties.ASSISTANT))
Assistant = Collection[Properties.ASSISTANT][0] as string;
if (Collection.Contains(Properties.CELLPHONE))
CellPhone = Collection[Properties.CELLPHONE][0] as string;
...
if (Collection.Contains(Properties.STREETADDRESS))
StreetAddress = Collection[Properties.STREETADDRESS][0] as string;
if (Collection.Contains(Properties.USERPRINCIPALNAME))
UserPrincipalName = Collection[Properties.USERPRINCIPALNAME][0] as
string;
if (Collection.Contains(Properties.ZIPCODE))
ZipCode = Collection[Properties.ZIPCODE][0] as string;
}
保存对用户所做的更改是通过按 ObjectSID 检索 DirectoryEntry
并仅保存已更改的值来处理的。
public void SaveChanges()
{
try
{
using (DirectoryEntry deUser =
Search.ForDirectoryEntry(Properties.OBJECTSID, ObjectSIDString))
{
if (_PropertiesLoaded.Contains(Properties.ACCOUNTCONTROL))
if (!object.Equals(deUser.Properties[Properties.
ACCOUNTCONTROL].Value, AccountControl))
SetPropertyValue(deUser, Properties.ACCOUNTCONTROL,
AccountControl);
if (_PropertiesLoaded.Contains(Properties.ASSISTANT))
if (!object.Equals(deUser.Properties[Properties.
ASSISTANT].Value, Assistant))
SetPropertyValue(deUser, Properties.ASSISTANT,
Assistant);
if (_PropertiesLoaded.Contains(Properties.CELLPHONE))
if (!object.Equals(deUser.Properties[Properties.
CELLPHONE].Value, CellPhone))
SetPropertyValue(deUser, Properties.CELLPHONE,
CellPhone);
...
if (_PropertiesLoaded.Contains(Properties.STREETADDRESS))
if (!object.Equals(deUser.Properties[Properties.
STREETADDRESS].Value, StreetAddress))
SetPropertyValue(deUser, Properties.STREETADDRESS,
StreetAddress);
if (_PropertiesLoaded.Contains(Properties.USERPRINCIPALNAME))
if (!object.Equals(deUser.Properties[Properties.
USERPRINCIPALNAME].Value, UserPrincipalName))
SetPropertyValue(deUser, Properties.USERPRINCIPALNAME,
UserPrincipalName);
if (_PropertiesLoaded.Contains(Properties.ZIPCODE))
if (!object.Equals(deUser.Properties[Properties.
ZIPCODE].Value, ZipCode))
SetPropertyValue(deUser, Properties.ZIPCODE, ZipCode);
deUser.CommitChanges();
if (_PropertiesLoaded.Contains(Properties.COMMONNAME))
if (!object.Equals(deUser.Properties[Properties.
COMMONNAME].Value, CommonName))
{
deUser.Rename("CN=" + CommonName);
deUser.CommitChanges();
}
}
}
catch (Exception Error)
{ throw new Exception("Save Error.", Error); }
}
多值键的处理通常非常简单,并且是只读的。几乎所有 User 属性和所有 Group 属性都可以使用 DirectorySearcher
对象检索,除了 User.TokenGroups
。这需要不同的方法。
...
public List<string> TokenGroups
{
get
{
if (this[Properties.TOKENGROUPS] == null)
this[Properties.TOKENGROUPS] = GetTokenGroups(ObjectSIDString);
return (List<string>)this[Properties.TOKENGROUPS];
}
private set { this[Properties.TOKENGROUPS] = value; }
}
...
public static List<string> GetTokenGroups(string ObjectSIDString)
{
List<string> TokenGroups = new List<string>();
try
{
using (DirectoryEntry deUser =
Search.ForDirectoryEntry(Properties.OBJECTSID, ObjectSIDString))
{
deUser.RefreshCache(new string[] { Properties.TOKENGROUPS });
if (deUser.Properties.Contains(Properties.TOKENGROUPS))
{
if (deUser.Properties[Properties.TOKENGROUPS] != null)
{
foreach (byte[] GroupSID in
deUser.Properties[Properties.TOKENGROUPS])
{
string sGroupSID =
Methods.ConvertBytesToStringSid(GroupSID);
string sGroupName = Search.ForGroupName(sGroupSID);
if (!string.IsNullOrEmpty(sGroupName))
TokenGroups.Add(sGroupName);
}
}
}
}
}
catch
{ throw; }
return TokenGroups;
}
...
你必须使用 DirectoryEntry
对象来检索 Token Groups,因为它是一个计算属性。请注意,需要直接 DirectoryEntry
访问的方法是 static
方法,以将它们与类的其余部分分开。
另一个有趣的点是设置用户标志。在 User
类中,我设置了四个:Enabled
、MustChangePasswordOnNextLogin
、CannotChangePassword
和 PasswordNeverExpires
。除 CannotChangePassword
外,其他都通过 AccountControl
和 PasswordLastSet
属性处理。CannotChangePassword
开关如下所示;它更复杂一些。
public static void SetFlag_CannotChangePassword(string ObjectSIDString,
bool Value)
{
Guid ChangePasswordGUID = new
Guid("{AB721A53-1E2F-11D0-9819-00AA0040529B}");
bool wasModified = false;
try
{
using (DirectoryEntry deUser =
Search.ForDirectoryEntry(Properties.OBJECTSID, ObjectSIDString))
{
ActiveDirectorySecurity ads = deUser.ObjectSecurity;
AuthorizationRuleCollection arc = ads.GetAccessRules(true, true,
typeof(NTAccount));
foreach (ActiveDirectoryAccessRule adar in arc)
{
if (adar.ObjectType == ChangePasswordGUID &&
(adar.IdentityReference.Value == @"EVERYONE" ||
adar.IdentityReference.Value == @"NT
AUTHORITY\SELF"))
{
ActiveDirectoryAccessRule AccessRule = new
ActiveDirectoryAccessRule(adar.IdentityReference,
adar.ActiveDirectoryRights, AccessControlType.Deny,
adar.ObjectType, adar.InheritanceType);
if (!ads.ModifyAccessRule((Value ?
AccessControlModification.Add :
AccessControlModification.Remove),
AccessRule, out wasModified))
throw new Exception("ACE Not Modified: (" +
adar.IdentityReference.Value + ")");
}
}
deUser.ObjectSecurity = ads;
deUser.CommitChanges();
}
}
catch
{ throw; }
}
注意 GUID:我花了好长时间才弄清楚,没有它,用户就会出现非常奇怪的行为,如果你将每个 ADAR 设置为 Disallow/Allow。通常是最简单的事情!
User.Path
属性是从 DistinguishedName
计算得出的,它只是路径各部分的反向列表,非常类似于目录的列出方式(例如 C:\Windows\Somewhere\Somefile.txt,它被列为:“com\company\ouname\ouname\commonname”)。
Search
方法处理 PropertiesToLoad
方法变量的最佳部分。当你搜索用户时,你可以选择仅返回用户的一些属性(例如 FirstName
或 SAMAccountName
),而不是全部。请务必查看它:它将使检索你的用户和组的速度更快!
我鼓励你进行实验和开发代码。我经历了许多次这个类的迭代,这是最好/最快的。我只包含了最常用的属性,但还有其他用户和组属性。添加或删除你需要的任何内容:始终根据你的需求进行定制。如果你这样做,请让我知道,以便我进行自己的调整。
历史
- 2007 年 4 月 1 日,星期日 [2.0]:上传到 CodeProject。
- 2007 年 4 月 9 日,星期日 [2.1]:代码通用更新。发生了很多变化;请参阅源代码。
- 属性更改为允许字符串的
null
值(当对象为null
时,Convert.ToString(object)
返回null
,但Convert.ToString(string)
在null
时返回string.Empty
。 - List<*> 属性现在使用 ?? 来防止
null
,从而减少了抛出的异常。 SaveChanges()
和PopulateValues()
错误修复/更新。User.Path
从List<string>
更改为带 "\" 分隔符的string
。- 各种其他修复/更新。
- 属性更改为允许字符串的