AD帐户操作C#示例代码(一)——导入用户信息
最近写了一个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[].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 = , LowPart = };
不过后来我不是用的上面代码而是这样写的,也成功了。
currentUserDirectoryEntry.Properties["pwdLastSet"].Value = ;
关于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 = ;
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.cnblogs.com/CSharpDevelopers/ADUserImportTool.zip
AD帐户操作C#示例代码(一)——导入用户信息的更多相关文章
- AD帐户操作C#示例代码(二)——检查密码将过期的用户
本文接着和大家分享AD帐户操作,这次开发一个简单的检查密码将过期用户的小工具. 首先,新建一个用户实体类,属性是我们要取的用户信息. public class UserInfo { /// <s ...
- php操作mysqli(示例代码)
<?php define("MYSQL_OPEN_LOGS",true); class mysqliHelp { private $db; public function _ ...
- JQuery -- Dom操作, 示例代码
1.内部插入节点 * append(content) :向每个匹配的元素的内部的结尾处追加内容 * appendTo(content) :将每个匹配的元素追加到指定的元素中的内部结尾处 * ...
- PHP程序中使用PDO对象实现对数据库的增删改查操作的示例代码
PHP程序中使用PDO对象实现对数据库的增删改查操作(PHP+smarty) dbconn.php <?php //------------------------使用PDO方式连接数据库文件- ...
- java poi操作excel示例代码
import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io ...
- 如何在C#程序中模拟域帐户进行登录操作 (转载)
.NET Core .NET Core也支持用PInvoke来调用操作系统底层的Win32函数 首先要在项目中下载Nuget包:System.Security.Principal.Windows 代码 ...
- 解析大型.NET ERP系统 电子邮件系统帐户集成
为保证ERP系统的信息流准确快速的传递,需要给系统设计一个消息盒子机制.当系统中发生业务操作后,需要提醒下一个环节的操作人员,以保证ERP信息流快速准确传递.比如生产任务单(工作单,加工单,制单)过帐 ...
- PJSUA2开发文档--第五章 帐户(号)Accounts
第五章 帐户(号) 帐户提供正在使用该应用程序的用户的身份(或身份).一个帐户有一个与之相关的SIP统一资源标识符(URI).在SIP术语中,该URI用作该人的记录地址( Address of Rec ...
- SharePoint 事件 7363:对象缓存:缓存使用的超级读者帐户没有足够的权限访问SharePoint数据库。
转自MSND:http://technet.microsoft.com/zh-cn/library/ff758656(v=office.14) 对象缓存存储 Microsoft SharePoint ...
随机推荐
- vue-cli webpack 引入jquery
首先在package.json里的dependencies加入"jquery" : "^2.2.3",然后install 在webpack.base.conf. ...
- Perl爬取铁路违章旅客信息
#! /usr/bin/perl use strict; use Encode qw(encode decode); binmode(STDIN,":encoding(utf8)" ...
- subversion(SVN)安装配置
简介subversion(简称svn)是近年来崛起的版本管理软件系统,是cvs的接班人.目前,绝大多数开源软件都使用svn作为代码版本管理软件.Subversion是一个版本控制系统,相对于的RCS. ...
- [转]配置sonar、jenkins进行持续审查
本文以CentOS操作系统为例介绍Sonar的安装配置,以及如何与Jenkins进行集成,通过pmd-cpd.checkstyle.findbugs等工具对代码进行持续审查. 一.安装配置sonar ...
- 解决SOAPCLIENT访问WebSerivce外网发布端口
猫用vs2010写了一个webservice,并写了一个盘点程序客户端,PDA盘点机用C#开发,笔记本用VFP开发,发布在本地局域网IIS服务器,用了两年一直很稳定.后面仓库搬迁,需要外网进行访问,在 ...
- 【转】NVelocity模板引擎初学总结
转自:http://sunxitao88.blog.163.com/blog/static/68314439200861963326251/ 前不久,接触到.NET下的MVC-MonoRail,它推荐 ...
- c语言中三个点的解释 : variadic
3.6 Variadic Macros A macro can be declared to accept a variable number of arguments much as a funct ...
- String和string的区别(C#)
从位置讲: 1.String是.NET Framework里面的String,小写的string是C#语言中的string 2.如果把using System;删掉,没有大写的String了,Sys ...
- UISegmentedControl 控件
一.创建 UISegmentedControl* mySegmentedControl = [[UISegmentedControl alloc]initWithItems:nil]; 是不是很奇怪没 ...
- 一个login
login 1.获取提交表单,保存到变量中.2.判断用户密码是否正确,利用Model类.3.验证用户是否激活.3.判断用户是否记住登录状态,是的话,将其用cookie和session分别保存.没有的话 ...