最近寫了一個AD帳戶導入的小工具(為啥寫作“帳”戶呢?),跟大家分享下相關代碼,歡迎各位高手指教!
首先,我準備一個這樣的Excel文件作為導入模版,并添加了一些測試數據。
然后,我打開Visual Studio 2012,新建一個Windows窗體應用程序。在主窗體界面,我放了一些Label、TextBox、Button控件,還有一個PRogressBar。
開始寫代碼。首先寫從Excel里讀取數據的方法。
private static async Task<DataTable> GetTableFromExcelAsync(string fileName) { return await Task.Factory.StartNew<DataTable>(() => GetTableFromExcel(fileName)); } private static DataTable GetTableFromExcel(string fileName) { DataTable dataTable = new DataTable(); string connectionString = string.Format("Provider = Microsoft.ACE.OLEDB.12.0;Data Source ={0};Extended Properties='Excel 12.0 xml;HDR=YES'", fileName); using (OleDbConnection oleDbConnection = new OleDbConnection(connectionString)) { oleDbConnection.Open(); DataTable schemaTable = oleDbConnection.GetOleDbSchemaTable(OleDbSchemaGuid.Tables, new Object[] { null, null, null, "TABLE" }); string sheetName = schemaTable.Rows[0].Field<string>("TABLE_NAME"); string commandText = string.Format("select * from [{0}]", sheetName); using (OleDbDataAdapter adapter = new OleDbDataAdapter(commandText, oleDbConnection)) { adapter.Fill(dataTable); } } return dataTable; }
這樣調用,將結果保存在一個DataTable里:
private async void btnImport_Click(object sender, EventArgs e) { DataTable dataTable = await GetTableFromExcelAsync(txtUserListPath.Text); }
運行出現異常:“未在本地計算機上注冊 Microsoft.ACE.OLEDB.12.0 提供程序”。
我的系統是X64的Windows 8 ,下載accessDatabaseEngine.exe安裝后,成功讀取數據。
下載地址是:http://download.microsoft.com/download/7/0/3/703ffbcb-dc0c-4e19-b0da-1463960fdcdb/AccessDatabaseEngine.exe
如果在發布時還發生異常,那么再試試屬性設置,把目標平臺(G)改成x86或勾選"首選32位(P)"。
在.NET中訪問AD服務可以用DirectoryEntry類(引用程序集 :System.DirectoryServices(在 System.DirectoryServices.dll 中)、命名空間: System.DirectoryServices)。
創建DirectoryEntry對象要提供LDAP地址,作為我們創建用戶的根OU,當然還要有在這個OU下創建OU和帳戶的權限。
string ldapPath = txtLdapPath.Text; string userName = txtUserName.Text; string passWord = txtPassword.Text;
DirectoryEntry rootDirectoryEntry; if (userName != string.Empty) { rootDirectoryEntry = new DirectoryEntry(ldapPath, userName, password); } else { rootDirectoryEntry = new DirectoryEntry(ldapPath); }
DirectoryEntry 類使用參考:http://msdn.microsoft.com/zh-cn/library/z9cddzaa(v=vs.110).aspx
在創建用戶帳戶前,要先創建它們依賴的上級OU。創建OU的代碼如下:
DirectoryEntry currentOuDirectoryEntry = currentOuDirectoryEntry.Children.Add("OU=" + currentValue, "organizationalUnit");currentOuDirectoryEntry.Properties["name"].Add(currentValue);currentOuDirectoryEntry.CommitChanges();
創建用戶的代碼如下:
DirectoryEntry currentUserDirectoryEntry = currentOuDirectoryEntry.Children.Add("CN=" + displayName, "user");currentUserDirectoryEntry.Properties["sAMAccountName"].Value = sAMAccountName;currentUserDirectoryEntry.Properties["userPrincipalName"].Value = string.Format(@"{0}@{1}", sAMAccountName, domainName);currentUserDirectoryEntry.Properties["displayName"].Value = displayName;currentUserDirectoryEntry.CommitChanges();
DirectoryEntry類的Properties屬性是一個集合,除了一些字符串類型的屬性,還有幾個我覺得操作比較麻煩的。
例如"userAccountControl",看起來它只是一個整型字段,但是實際上它一個字段包含了很多個的狀態信息。每個狀態又對應著一個屬性標志(例如密碼永不過期是65536)。所以我們要從這一個userAccountControl字段讀取或寫入狀態要做次位運算。
private void SetPropertyInUserAccountControl(DirectoryEntry directoryEntry, bool newValue, int propertyflag) { int userAccountControl = (int)directoryEntry.Properties["userAccountControl"].Value; bool oldValue = GetPropertyFromUserAccountControl(directoryEntry, propertyflag); if (oldValue != newValue) { if (newValue) { directoryEntry.Properties["userAccountControl"].Value = userAccountControl | propertyflag; } else { directoryEntry.Properties["userAccountControl"].Value = userAccountControl & ~propertyflag; } } } private bool GetPropertyFromUserAccountControl(DirectoryEntry directoryEntry, int propertyflag) { return Convert.ToBoolean((int)directoryEntry.Properties["userAccountControl"].Value & propertyflag); }
更多userAccountControl屬性標志(propertyflag參數)請參考資料:http://support.microsoft.com/kb/305144/zh-cn、http://msdn.microsoft.com/zh-cn/library/ms680832(VS.85).aspx。那么這些標志屬性是什么意思呢?為什么將"userAccountControl"值“&”一下屬性標志就可以得到對應的狀態呢?我把這些屬性標志轉換為二進制,發現都是只有一個1,其他都是0的。那個1的位置就是狀態的標志位,如果“userAccountControl”字段的這個位置是1,那么對應狀態就是“True”了。再用并運算(&:參考資料:http://msdn.microsoft.com/zh-cn/library/sbf85k1c.aspx)操作,因為0&0等于0,0&1或1&0也等于0,只有1&1才能等于1,所以“userAccountControl”和“只有一位是1其他全是0”的propertyflag并運算,就可以推斷出該狀態對應的標志位是不是1了。
不過我十分討厭這種把多個維度的狀態保存在一個字段中的設計,在曾經的項目中我也遇到過有高人在關系數據庫中這樣設計表字段,但我個人覺得這不符合第一范式的設計(同一列有多個值,應該分為多個IsXX1,IsXX2的bit字段),另外狀態是個比較常用的過濾條件,在這個字段做位運算是否還能索引查找?當然有人覺得這樣做減少了字段數量(在UI顯示給用戶的時候還是要分開吧?不然誰看得懂?。€有就是設計這一個狀態字段以后想再多添加幾個狀態就不用修改表結構了。不過最重要的還是這樣設計能體現出設計者的高水平,因為初級的程序員、數學不好的程序員以及記憶力不好的程序員看到這樣一個整型值是不會馬上知道它代表什么——我就是這樣的程序員。
不過還好,我們可以直接用幾個常用的,我創建的是正常帳戶,不需要禁用,所以userAccountControl直接給512。
還有這些“System.__ComObject”類型的屬性,操作起來太不方便了。我在網上找了一些資料,通常是引用了一個“Interop.ActiveDs.dll”的文件(不清楚是誰寫的)。我這里只是希望新創建的用戶下次登錄時更改密碼就要寫:
currentUserDirectoryEntry.Properties["pwdLastSet"].Value = new LargeInteger() { HighPart = 0, LowPart = 0 };
不過后來我不是用的上面代碼而是這樣寫的,也成功了。
currentUserDirectoryEntry.Properties["pwdLastSet"].Value = 0;
關于ADSI 對象屬性有個參考資料:http://msdn.microsoft.com/zh-cn/library/ms180868(v=vs.90).aspx。
我把幾個常用的字符串類型屬性寫在XML文件里,導入數據時直接賦值即可。
<userProperties> <!--常規--> <property name = "sn" title = "姓"/> <property name = "givenName" title = "名"/>
<property name = "initials" title = "英文縮寫"/> <property name = "displayName" title = "顯示名稱"/> <property name = "telephoneNumber" title = "電話號碼"/> <property name = "otherTelephone" title = "其它電話號碼"/> <property name = "mail" title = "電子郵件"/> <property name = "description" title = "描述"/> <property name = "physicalDeliveryOfficeName" title = "辦公室"/> <property name = "wWWHomePage" title = "網頁"/> <property name = "url" title = "其它網頁"/> •<!--地址--> <property name = "co" title = "國家/地區"/> <property name = "st" title = "省/自治區"/> <property name = "l" title = "市/縣"/> <property name = "streetAddress" title = "街道"/> <property name = "postOfficeBox" title = "郵政信箱"/> <property name = "postalCode" title = "郵政編碼"/> •<!--電話--> <property name = "homePhone" title = "家庭電話"/> <property name = "otherHomePhone" title = "其他家庭電話"/> <property name = "pager" title = "尋呼機"/> <property name = "otherPager" title = "其他尋呼機"/> <property name = "mobile" title = "移動電話"/> <property name = "otherMobile" title = "其他移動電話"/> <property name = "facsimileTelephoneNumber" title = "傳真"/> <property name = "otherFacsimileTelephoneNumber " title = "其他傳真"/> <property name = "ipPhone" title = "IP電話"/> <property name = "otherIpPhone" title = "其他IP電話"/> <property name = "info" title = "注釋"/> •<!--帳戶--> <property name = "userPrincipalName" title = "用戶登錄名"/> <property name = "sAMAccountName" title = "用戶登錄名(Windows 2000 以前版本)"/> •<!--組織--> <property name = "company" title = "公司"/> <property name = "department" title = "部門"/> <property name = "title" title = "職務"/> <property name = "manager" title = "經理"/> <property name = "directReports" title = "直接下屬"/> </userProperties>
如果您一次性把這幾個屬性都提交了,還可能會出現一個很有個性的異常:“該服務器不愿意處理該請求”。
要想讓“她”愿意,可以這樣寫:
using (DirectoryEntry currentUserDirectoryEntry = currentOuDirectoryEntry.Children.Add("CN=" + displayName, "user")) { currentUserDirectoryEntry.Properties["sAMAccountName"].Value = sAMAccountName; currentUserDirectoryEntry.Properties["userPrincipalName"].Value = string.Format(@"{0}@{1}", sAMAccountName, domainName); currentUserDirectoryEntry.Properties["displayName"].Value = displayName; currentUserDirectoryEntry.CommitChanges(); currentUserDirectoryEntry.Properties["userAccountControl"].Value = userAccountControl; currentUserDirectoryEntry.Properties["pwdLastSet"].Value = 0; currentUserDirectoryEntry.Invoke("SetPassword", new object[] { newUserDefaultPassword }); currentUserDirectoryEntry.CommitChanges(); }
因為我想給新導入的用戶一個初始的密碼,修改密碼的操作這樣寫就可以了:
currentUserDirectoryEntry.Invoke("SetPassword", new object[] { newUserDefaultPassword });
當用戶是某個OU的管理員時,需要給它賦予權限。代碼里的ActiveDirectoryRights是個枚舉類型,當然您有時也會用到別的選擇。
if (string.Equals(currentDataRow[_isAdminColumnName] as string, @"是")) { IdentityReference newOwner = new NTAccount(domainName, sAMAccountName).Translate(typeof(SecurityIdentifier)); ActiveDirectoryAccessRule newRule = new ActiveDirectoryAccessRule(newOwner, ActiveDirectoryRights.GenericAll, AccessControlType.Allow); currentOuDirectoryEntry.ObjectSecurity.SetAccessRule(newRule); currentOuDirectoryEntry.CommitChanges(); }
如果要導入的用戶已經存在,就會出現異常。那么如何判斷一個用戶是否已存在呢?這時我們需要用到的是.NET的DirectorySearcher類型。這個類型的一個構造方法需要給一個搜索根路徑、搜索篩選器、要檢索的屬性和搜索范圍。
DirectorySearcher userDirectorySearcher = new DirectorySearcher(currentOuDirectoryEntry, string.Format(@"(&(cn={0})(objectCategory=person)(objectClass=user))", displayName), new[] { "adspath" }, SearchScope.OneLevel);
SearchResult searchResult = userDirectorySearcher.FindOne();
if (searchResult != null)
{
//TODO:......
}
DirectorySearcher 類使用參考:http://msdn.microsoft.com/zh-cn/library/System.DirectoryServices.DirectorySearcher(v=vs.90).aspx
最后將這些零散的代碼組合起來,就是我要做的工具了!
看看導入的效果,算是成功導入了吧。
當然這只是個很簡單的小例子,日后還要繼續完善,各位專家、高手如果看到我做的不好的地方也歡迎指正,多給些高大上的建議,非常感謝!
其他參考資料:
http://msdn.microsoft.com/en-us/library/aa367008(VS.85).aspx
http://msdn.microsoft.com/en-us/library/windows/desktop/ms675085(v=vs.85).aspx
AD用戶導入工具下載:
http://files.VEVb.com/CSharpDevelopers/ADUserImportTool.zip
新聞熱點
疑難解答