C#(.NetCore)接入AD域使用者的實現

没有星星的夏季發表於2024-09-26

  很多公司電腦都是windows,而對使用者的管理則很多采用AD域的形式來管理,本文簡單的來介紹一下.NetCore中怎麼接入AD域來實現登入等操作。

  首先,我這裡使用的是.net6,其它版本類似。

  其次,這裡假設你已經對AD域有了基本的瞭解,比如AD域所使用的LDAP、屬性等,如果不瞭解先自行百度下。

  接著,接入AD域自然首先需要一個域,怎麼搭建一個域控制器,網上太多介紹了,可以自行百度下,加入我現在已經有一個域:demo.cn

  基本使用

  這裡我們基於LDAP協議來連線使用AD域,那麼我們需要安裝一個包:Novell.Directory.Ldap.NETStandard

  一個簡單的使用者驗證的例子:

    public static void Main(params string[] args)
    {
        var domain = "demo.cn";              //這裡也可以是ip
        var port = 389;                      //埠預設389
        //使用者名稱有三種形式
        var account = "Common Name";         //使用者姓名全名,即cn屬性(Common Name)
        //var account = "account@demo.cn";   //window 2000之後的版本
        //var account = "DEMO\\account";     //window 2000之前的版本
        var password = "123456";

        //建立例項
        using var connection = new Novell.Directory.Ldap.LdapConnection();
        //連線
        connection.Connect(domain, port);
        //繫結使用者(認證)
        connection.Bind(account, password);
        //得到使用者的賬號資訊
        var res = connection.WhoAmI();
        var authzId = res.AuthzIdWithoutType;
        Console.WriteLine($"當前賬號:{authzId}");
        //獲取根資訊
        var root = connection.GetRootDseInfo();
        //根據authzId的格式來處理一下,方便後續查詢
        if (authzId.Contains("\\"))
        {
            account = authzId.Split('\\', StringSplitOptions.RemoveEmptyEntries).Last();
        }
        else if (authzId.Contains("@"))
        {
            account = authzId.Split(new string[] { "@" }, StringSplitOptions.RemoveEmptyEntries).First();
        }
        //過濾,規則可以自行百度下,這裡根據account來查詢
        var filter = string.Format("(&(|(cn={0})(sAMAccountName={0}))(objectCategory=person)(objectClass=user))", account);
        var result = connection.Search(root.DefaultNamingContext, LdapConnection.ScopeSub, filter, null, false);
        //列出所有屬性
        while (result.HasMore())
        {
            try
            {
                var entry = result.Next();
                var set = entry.GetAttributeSet();
                Console.WriteLine($"entry: {entry.Dn}{Environment.NewLine}attr count: {set.Count}");
                int index = 1;
                foreach (var attr in set)
                {
                    if (attr.StringValueArray.Length <= 1)
                    {
                        Console.WriteLine($"attr{index}: {attr.Name} = {attr.StringValue}");
                    }
                    else
                    {
                        Console.WriteLine($"attr{index}: {attr.Name} = [{string.Join(", ", attr.StringValueArray)}]");
                    }
                    index++;
                }
            }
            catch { }
        }
    }

  這個簡單的例子可以用於列印域使用者的所有資訊。

  這裡簡單說下這裡認證的賬號的三種格式:

  第一種,直接會用CommonName ,就是通用名稱,AD域中要求CommonName 是唯一的,上面的例子輸出後,可以看到有cn屬性,就表示可以使用CommonName 進行授權繫結,那麼CommonName 怎麼設定呢,其實就是我們在建立域使用者的時候由姓名組成的部分,見下圖

  第二種,採用sAMAccountName加域名的形式,如果是windows 2000之前的,可以使用這個格式來登入:[domain]\[account] 比如:DEMO\test ,注意這裡的域名需要大寫,而且是不包含cn、com等字尾,見下圖

  第三種,採用userPrincipalName ,格式:[account]@[domain] ,例如:test@demo.cn,這裡域名要使用全域名,但是不用大寫,見下圖(但是第二種貌似是接受度和使用的最多的)

  C#(.NetCore)接入AD域使用者的實現

  封裝使用

  為了方便使用,我這裡做了一個封裝:

ActiveDirectoryInfo
     public class ActiveDirectoryInfo
    {
        string defaultNamingContext = string.Empty;

        /// <summary>
        /// 域
        /// </summary>
        public string Domain { get; }
        /// <summary>
        /// 埠
        /// </summary>
        public int Port { get; }
        /// <summary>
        /// 過濾
        /// </summary>
        public string Filter { get; set; } = "(&(|(cn={0})(sAMAccountName={0}))(objectCategory=person)(objectClass=user))";

        public ActiveDirectoryInfo(string domain, int port = 389)
        {
            Domain = domain;
            Port = port;
        }

        private string GetAccount(string value)
        {
            if (value.Contains("\\"))
            {
                value = value.Split('\\', StringSplitOptions.RemoveEmptyEntries).Last();
            }
            else if (value.Contains("@"))
            {
                value = value.Split(new string[] { "@" }, StringSplitOptions.RemoveEmptyEntries).First();
            }
            return value;
        }

        public DomainUserInfo Authorize(string account, string password)
        {
            //建立例項
            using var connection = new Novell.Directory.Ldap.LdapConnection();
            //連線
            connection.Connect(Domain, Port);
            //繫結使用者(認證)
            connection.Bind(account, password);
            //得到使用者的賬號資訊
            var res = connection.WhoAmI();
            var authzId = res.AuthzIdWithoutType;

            if (string.IsNullOrEmpty(defaultNamingContext))
            {
                //獲取根資訊
                var root = connection.GetRootDseInfo();
                defaultNamingContext = root.DefaultNamingContext;
            }

            //根據account的格式來處理一下,方便後續查詢
            if (authzId.Contains("\\"))
            {
                account = authzId.Split('\\', StringSplitOptions.RemoveEmptyEntries).Last();
            }
            else if (authzId.Contains("@"))
            {
                account = authzId.Split(new string[] { "@" }, StringSplitOptions.RemoveEmptyEntries).First();
            }
            //過濾,規則可以自行百度下,這裡根據account來查詢
            var filter = string.Format(Filter, account);
            var result = connection.Search(defaultNamingContext, LdapConnection.ScopeSub, filter, null, false);
            LdapAttributeSet attributes = new LdapAttributeSet();
            //列出所有屬性
            if (result.HasMore())
            {
                try
                {
                    var entry = result.Next();
                    attributes = entry.GetAttributeSet();
                }
                catch { }
            }
            return new DomainUserInfo(account, authzId, attributes);
        }
    }
DomainUserInfo
     public class DomainUserInfo
    {
        ConcurrentDictionary<string, List<byte[]>> entryDict = new ConcurrentDictionary<string, List<byte[]>>(StringComparer.OrdinalIgnoreCase);

        internal DomainUserInfo(string account, string authzId, LdapAttributeSet set)
        {
            Account = account;
            AuthzId = authzId;

            foreach (var attr in set)
            {
                entryDict[attr.Name] = attr.ByteValueArray.Select(f => f.ToArray()).ToList();
            }
        }


        /// <summary>
        /// 屬性個數
        /// </summary>
        public int AttrCount => entryDict.Count;
        /// <summary>
        /// 屬性鍵
        /// </summary>
        public ICollection<string> Keys => entryDict.Keys;
        /// <summary>
        /// Common Name
        /// </summary>
        public string CommonName => Get("cn");
        /// <summary>
        /// SAM Account Name
        /// </summary>
        public string SAMAccountName => Get("sAMAccountName");
        /// <summary>
        /// Dn
        /// </summary>
        public string Dn => Get("distinguishedName");
        /// <summary>
        /// userPrincipalName
        /// </summary>
        public string UserPrincipalName => Get("userPrincipalName");

        /// <summary>
        /// 賬號
        /// </summary>
        public string Account { get; }
        /// <summary>
        /// 使用者登入賬號
        /// </summary>
        public string AuthzId { get; }


        /// <summary>
        /// 獲取字串
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        public string Get(string key)
        {
            if (entryDict.TryGetValue(key, out var list) && list.Any())
            {
                return Encoding.UTF8.GetString(list.First());
            }
            return string.Empty;
        }
        /// <summary>
        /// 獲取字串陣列資料
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        public string[] GetArray(string key)
        {
            if (entryDict.TryGetValue(key, out var list) && list.Any())
            {
                return list.Select(Encoding.UTF8.GetString).ToArray();
            }
            return Array.Empty<string>();
        }
        /// <summary>
        /// 獲取指定基礎值型別的資料
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="key"></param>
        /// <returns></returns>
        public T Get<T>(string key)
        {
            return (T)Get(key, typeof(T));
        }
        /// <summary>
        /// 獲取指定基礎值型別的資料
        /// </summary>
        /// <param name="key"></param>
        /// <param name="type"></param>
        /// <returns></returns>
        public object Get(string key, Type type)
        {
            var value = Get(key);
            if (string.IsNullOrEmpty(value))
            {
                return Activator.CreateInstance(type);
            }

            var underlying = Nullable.GetUnderlyingType(type) ?? type;
            if (underlying.IsEnum)
            {
                if (int.TryParse(value, out var result))
                {
                    return Enum.ToObject(underlying, result);
                }
                else
                {
                    return Enum.Parse(underlying, value, true);
                }
            }

            return Convert.ChangeType(value, type);
        }
        /// <summary>
        /// 獲取指定格式的陣列型別
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="key"></param>
        /// <returns></returns>
        public T[] GetArray<T>(string key)
        {
            var array = GetArray(key);
            var elementType = typeof(T);
            if (!array.Any())
            {
                return Array.Empty<T>();
            }

            var underlying = Nullable.GetUnderlyingType(elementType) ?? elementType;
            if (underlying.IsEnum)
            {
                return array.Select(value =>
                {
                    if (int.TryParse(value, out var result))
                    {
                        return (T)Enum.ToObject(underlying, result);
                    }
                    else
                    {
                        return (T)Enum.Parse(underlying, value, true);
                    }
                }).ToArray();
            }

            return array.Select(value => (T)Convert.ChangeType(value, elementType)).ToArray();
        }
        /// <summary>
        /// 獲取位元組陣列
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        public byte[] GetBuffer(string key)
        {
            if (entryDict.TryGetValue(key, out var bufferList) && bufferList.Any())
            {
                return bufferList.First().ToArray();
            }
            return Array.Empty<byte>();
        }
        /// <summary>
        /// 獲取Dn並返回鍵值對
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        public KeyValuePair<string, string>[] GetDn(string key)
        {
            var value = Get(key);
            if (string.IsNullOrEmpty(value))
            {
                return Array.Empty<KeyValuePair<string, string>>();
            }

            return value.Split(",", StringSplitOptions.RemoveEmptyEntries)
                .Select(f =>
                {
                    var array = f.Split('=');
                    return new KeyValuePair<string, string>(array[0], array[1]);
                }).ToArray();
        }
    }

  使用的簡單例子:

    public static void Main(params string[] args)
    {
        var domain = "demo.cn";              //這裡也可以是ip
        var port = 389;                      //埠預設389
        //使用者名稱有三種形式
        var account = "Common Name";         //使用者姓名全名,即cn屬性(Common Name)
        //var account = "account@demo.cn";   //window 2000之後的版本
        //var account = "DEMO\\account";     //window 2000之前的版本
        var password = "123456";

        var ad = new ActiveDirectoryInfo(domain, port);
        var userInfo = ad.Authorize(account, password);

        Console.WriteLine($"當前賬號:{userInfo.AuthzId}");
        Console.WriteLine($"entry: {userInfo.Dn}{Environment.NewLine}attr count: {userInfo.AttrCount}");
        int index = 1;
        foreach (var attr in userInfo.Keys)
        {
            var array = userInfo.GetArray(attr);
            if (array.Length <= 1)
            {
                Console.WriteLine($"attr{index}: {attr} = {array.FirstOrDefault()}");
            }
            else
            {
                Console.WriteLine($"attr{index}: {attr} = [{string.Join(", ", array)}]");
            }
            index++;
        }
    }

  總結

  最近客戶要求我們系統對接到他們的AD域,避免他們一個個去新增使用者,很麻煩,而且使用者也不願意管理多套賬號密碼,這裡是我的一些筆記。

  另一方面,我這裡只是使用到登入認證,也就是讀資訊,但是不寫,但是有些系統可能還需要修改AD域使用者資訊,這其實使用Novell.Directory.Ldap.NETStandard 這個包也是可以實現的,例如下面的方法,因為我暫時沒有用到,就不過多介紹了,感興趣的可以自己試試:

    //新增屬性
    connection.Modify(dn, new LdapModification(LdapModification.Add, new LdapAttribute("mail","test@demo.com")));
    //修改屬性
    connection.Modify(dn, new LdapModification(LdapModification.Replace, new LdapAttribute("mail", "user@demo.com")));
    //刪除屬性
    connection.Modify(dn, new LdapModification(LdapModification.Delete, new LdapAttribute("mail")));
    //刪除使用者
    connection.Delete(dn);

相關文章