65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.79/5 (15投票s)

2007 年 4 月 3 日

CDDL

4分钟阅读

viewsIcon

236218

downloadIcon

545

处理 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 类中,我设置了四个:EnabledMustChangePasswordOnNextLoginCannotChangePasswordPasswordNeverExpires。除 CannotChangePassword 外,其他都通过 AccountControlPasswordLastSet 属性处理。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 方法变量的最佳部分。当你搜索用户时,你可以选择仅返回用户的一些属性(例如 FirstNameSAMAccountName),而不是全部。请务必查看它:它将使检索你的用户和组的速度更快!

我鼓励你进行实验和开发代码。我经历了许多次这个类的迭代,这是最好/最快的。我只包含了最常用的属性,但还有其他用户和组属性。添加或删除你需要的任何内容:始终根据你的需求进行定制。如果你这样做,请让我知道,以便我进行自己的调整。

历史

  • 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.PathList<string> 更改为带 "\" 分隔符的 string
    • 各种其他修复/更新。
© . All rights reserved.