最近写了一个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安装后,成功读取数据。
如果在发布时还发生异常,那么再试试属性设置,把目标平台(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用户导入工具下载:
https://files.cnblogs.com/CSharpDevelopers/ADUserImportTool.zip