引言
如果一个Exchange的用户群按照部门的形式分散在各地,而人数又很多,让一个管理员管理工作量实在有点大。一个方式是编写一个WEB程序发布到IIS,然后让各个分部门的管理员管理各自的部门。没错,本系列文章就是围绕这个话题展开的。而这个话题的核心是如何通过.NET管理Exchange。嗯,似乎是这样。当时在实现系统的过程中,我碰到了很多其他的问题,也非常有趣。所以我决定,按照实现的流程来记叙,列出碰到的一些问题和解决方式。解决问题的过程中参考了很多网友的博客,同时得到了很多网友的帮助,会在适当的地方给出引用,方便大家参考。
这个系统应该包括以下功能:查询,查看(指定的一些属性),禁用/启用,解锁,删除,新建,编辑(指定的一些属性),统计各个部门人数,为分部门指派管理员。总的来说就是增删查改。
用户查询
Exchange的用户信息是和AD同步的,符合微软企业级产品的风格。所以查询用户信息可以通过查询AD进行。在.NET下可以通过System.DirectoryServices名称空间(处于System.DirectoryServices程序集中)进行活动目录查询。使用到类型是DirectorySearcher。当然System.DirectoryService.AccountManagement名称空间中的PrincipalSearcher也可以进行用户查询,但是在执行一些操作的时候,还是前者方便。所以,这里使用前一个方案。
查询功能设计成按照用户的UPN(登录名)和姓名(环境中使用CN以方便显示)搜索,在列表中显示以下信息:登录名,CN,显示名称,所属部门,是否禁用。
分页
要进行分页就需要获得搜索结果的总条数,我一开始没多在意DirectorySearcehr的属性,直接指定了Filter属性和根结点,然后指定搜索范围为SubTree,结果当搜索条数为1200条的时候,统计个数就要十来秒。而且我还很2X的调用GetDirectoryEntry来访问其属性,结果查询超慢。经过朋友的指点,参考了这篇博客:http://blog.sina.com.cn/s/blog_683424be0100scuz.html,才有效的缩短了查询时间。
主要是PropertiesToLoad属性,通过向这个集合添加键,可以指定加载哪些DirectoryEntry属性,剩余属性不加载,从而缩短了时间。注意,Filter中涉及的属性是需要加载的。以下是一段脚本,和结果,可以看到差别之大。
可用的属性键有:
msexchumenabledflags2,homemdb,legacyexchangedn,usncreated,msexchhomeservername,m sexchrecipientdisplaytype,msexchrbacpolicylink,samaccountname,showinaddressbook, msexchmailboxauditenable,cn,pwdlastset,whencreated,displayname,lastlogon,garbage collperiod,samaccounttype,countrycode,objectguid,msexchmailboxsecuritydescriptor ,msexchmdbrulesquota,usnchanged,msexcharchivewarnquota,msexchversion,whenchanged ,msexchmoderationflags,name,msexchwhenmailboxcreated,protocolsettings,msexchpoli ciesincluded,objectsid,logoncount,internetencoding,mailnickname,msexchuseraccoun tcontrol,msexchtransportrecipientsettingsflags,msexchprovisioningflags,badpasswo rdtime,accountexpires,msexchaddressbookflags,primarygroupid,objectcategory,userp rincipalname,proxyaddresses,msexchdumpsterquota,useraccountcontrol,dscorepropaga tiondata,mdbusedefaults,distinguishedname,msexchmailboxauditlogagelimit,objectcl ass,badpwdcount,msexchrecipienttypedetails,homemta,mail,adspath,msexcharchivequo ta,msexchumdtmfmap,msexchmailboxguid,lastlogoff,msexchbypassaudit,instancetype,c odepage,msexchdumpsterwarningquota,
在我的例子中,使用cn,displayname,mail,samaccountname(和UPN同名),distinguishedname(通过解析获取OU信息),useraccountcontrol。
这里有一点需要注意,上面截图中,没有指定PageSize,所以就算指定SizeLimit,都只会最多返回1000条。
内存分页?LINQ迭代?
对1000条进行测试,速度上,两个区别不是很大。我最开始是用内存分页的,就是转换了(select)之后转为数组(ToArray),后来改为Skip,和Take的查询。在代码中进行测试的时候,设置好PageSize属性,然后使用foreach迭代输出,可以发现输出结果是一段一段的,输出若干个(页长)停顿一小会儿。所以使用Skip和Take方式最好,网上已经有LINQ to AD查询的实现了(LINQ Provider)。
用户操作
操作用户信息的时候有几个小点,这里直接列出。
#更改CN。使用DirectoryEntry修改对象的CN的属性的时候应该使用以下方式。
DirectoryEntry.Rename("CN=" + cmname),需要注意参数的格式。
#如何判断用户是否禁用。
1.通过DirecotryEntry.Properties["UserAccountControl"]的值和2进行位与(&)操作,然后转为bool,含义是Disabled(512&2的结果为0代表正常)
2.通过获取UserPrincipal,然后访问其Enabled属性。
#判断用户是否锁定。
DirectoryEntry.InvokeGet(“IsAccountLocked”)
#重置密码。重置密码后如何把用户置为“下次登录必须修改密码”。
1.通过DirectoryEntry获取UserPrincipal对象,调用ExpirePasswordNow()方法。
2.通过DirectoryEntry自身来处理应该也可行,但是尚未尝试。
#修改属性的正确的方式(防止异常)。这里把键和值对应了一下,不是必要的。
foreach (var kv in propertyValues) { if (!string.IsNullOrEmpty(kv.Value)) if (entry.Properties.Contains(kv.Key)) entry.Properties[kv.Key][0] = kv.Value; else entry.Properties[kv.Key].Add(kv.Value); } entry.CommitChanges();
分页窗口
由于以前学过一点C(都不好意思说出口),考虑问题的时候总是面向过程。就算知道建立类型,一些思考方向还是没有转过来。这里表现的就很明显。面对的问题是:让分页栏保持定长(如果有足够多页的数据)。
#CStyle
获取当前页,获取窗口长度。计算左右距离,验证左侧,验证右侧。然后在不同的情况下取不同的值。比如:
protected int[] getPagerWindow(int current, int max, int width) { //获取分页窗口 int left = (width - 1) / 2; int right = width - left - 1; //左右索引 int rindex; int lindex; List<int> window = new List<int>(); //计算 if (current - left <= 0) { //获取分页窗口左右索引 lindex = 1; rindex = Math.Min(width, max); } else if (current + right >= max) { //获取左右索引 rindex = max; lindex = Math.Max(1, max - width + 1); } else { rindex = current + right; lindex = current - left; } for (int i = lindex; i < rindex + 1; i++) window.Add(i); return window.ToArray(); }
#OOPStyle
将窗口想象成窗口,将页码想象成长胶卷。那么,如果胶卷长度比窗口小,就取所有页码,否则计算窗口中心的位移,然后移动窗口。比如:
public static IEnumerable<int> ComputePageWindow(int count, int current, int size) { current = current < 1 ? 1 : current; count = count < 1 ? 1 : count; var fixedIndex = size%2 == 0 ? size/2 : (size + 1)/2; var offset = current - fixedIndex; offset = current < fixedIndex ? 0 : offset; offset = count - current < fixedIndex ? count - size : offset; var windowStart = 1 + offset; return Enumerable.Range(windowStart, size).Where(i => i > 0 && i <= count); }
#以及数据绑定
<asp:Repeater ID="rptPager" runat="server"
DataSource='<%#getPagerWindow(searcher.CurrentIndex,searcher.PageCount,5) %>'
OnItemCommand="rptPager_ItemCommand">
</asp:Repeater>
很明显,第二种思路简洁很多。这里记下来提醒自己。各位朋友就当是听一个初学者的喃喃自语吧。
自此,AD信息管理的内容结束,其实主要是为了解决搜索的问题。这里贴一个运行截图。由于操作中有数据转换,所以执行搜索花了2~3秒,但是作为一个管理系统还可以接受。下一篇的内容是.net调用PowerShell组件以达到管理Exchange的目的。