本文接着和大家分享AD账号操作,这次开发一个简单的检查密码将过期用户的小工具。
首先,新建一个用户实体类,属性是我们要取的用户信息。
class="code_img_closed" src="/Upload/Images/2014041602/0015B68B3C38AA5B.gif" alt="" />logs_code_hide('6e911520-6a29-49bb-a18e-cd375b94ef20',event)" src="/Upload/Images/2014041602/2B1B950FA3DF188F.gif" alt="" />public class UserInfo { /// <summary> /// sAM帐户名称 /// </summary> public string SamAccountName { get; set; } /// <summary> /// 名称 /// </summary> public string Name { get; set; } /// <summary> /// 邮箱 /// </summary> public string Mail { get; set; } /// <summary> /// 已禁用 /// </summary> public bool IsDisabled { get; set; } /// <summary> /// 设置为密码永不过期 /// </summary> public bool IsPasswordNeverExpire { get; set; } /// <summary> /// 系统密码过期设置天数 /// </summary> public long MaxPasswordAge { get; set; } /// <summary> /// 剩余过期天数 /// </summary> public double? SurplusPasswordExpirationDays { get { return PasswordExpirationDate.HasValue ? (Math.Round((PasswordExpirationDate.Value.Subtract(DateTime.Now).TotalDays),2)) : default(double?); } } /// <summary> /// 最近密码修改时间 /// </summary> public DateTime PasswordLastSet { get; set; } /// <summary> /// 密码过期时间 /// </summary> public DateTime? PasswordExpirationDate { get; set; } }View Code
然后是搜索用户信息的方法。
private IEnumerable<UserInfo> SearchUsers(string path, string username, string password, string sAMAccountName, string displayName, bool isDisabled, bool IsPasswordNeverExpire, long[] surplusExpirationDays) { using (DirectoryEntry directoryEntry = new DirectoryEntry(path, username, password)) { using (DirectorySearcher directorySearcher = new DirectorySearcher(directoryEntry, @"&(objectCategory=person)(objectClass=user))", new string[] { "name", "sAMAccountName", "userAccountcontrol", "pwdLastSet", "mail" }, SearchScope.Subtree) { PageSize = 1000 }) { using (SearchResultCollection userResultCollection = directorySearcher.FindAll()) { foreach (SearchResult userResult in userResultCollection) { UserInfo userInfo = new UserInfo(); //TODO: 赋值 yield return userInfo; } } } } }
这次我们主要用DirectorySearcher类:SearchRoot是搜索的DirectoryEntry根节点;SearchScope属性是搜索的范围,是个SearchScope枚举:Base(限于基对象)、OneLevel(搜索基对象的直接子对象,但不搜索基对象)、Subtree(搜索整个子树,包括基对象及其所有子对象)。我们要在指定的OU下搜索用户,所以选择子树Subtree。
DirectorySearcher类的Filter属性是过滤条件,搜索用户就是“&(objectCategory=person)(objectClass=user))"。我们可以把一些查询条件放在这里,减少搜索结果的返回行数:
directorySearcher.SearchScope = SearchScope.Subtree; List<string> filterItems = new List<string>(); if (!string.IsNullOrEmpty(sAMAccountName)) { filterItems.Add(string.Format(@"(sAMAccountName={0})", sAMAccountName)); } if (!string.IsNullOrEmpty(displayName)) { filterItems.Add(string.Format(@"(name={0})", displayName)); } if (!isDisabled) { filterItems.Add(@"(!(userAccountcontrol=514))"); } if (!IsPasswordNeverExpire) { filterItems.Add(@"(!(userAccountcontrol=66048))"); } if (surplusExpirationDays != null && surplusExpirationDays.Length > 0) { StringBuilder surplusExpirationDaysFilter = new StringBuilder(@"(|"); DateTime now = DateTime.Now; foreach (long surplusExpirationDay in surplusExpirationDays) { DateTime passwordExpirationDate = now.AddDays(surplusExpirationDay); DateTime passwordLastSet = passwordExpirationDate.AddDays(-1 * maxPwdAge); if (surplusExpirationDay != 0) { surplusExpirationDaysFilter.AppendFormat("(&(pwdLastSet>={0})(pwdLastSet<={1}))", passwordLastSet.ToFileTime().ToString(), passwordLastSet.AddDays(1).AddSeconds(-1).ToFileTime().ToString()); } else { surplusExpirationDaysFilter.AppendFormat("(pwdLastSet<={0})(pwdLastSet=0)", passwordLastSet.AddDays(1).AddSeconds(-1).ToFileTime().ToString()); } } surplusExpirationDaysFilter.Append(@")"); filterItems.Add(surplusExpirationDaysFilter.ToString()); } directorySearcher.Filter = string.Format(@"(&{0}(objectCategory=person)(objectClass=user))", string.Concat(filterItems));
但是帐户已禁用和密码永不过期这些属性是保存在一个userAccountcontrol的整型里,同样的已禁用帐户userAccountcontrol值可能不只一个。我现在还没办法把对它们的过滤写在Filter里(哪位高手知道怎么写望指点),所以我只能用C#代码再在本地内存过滤一下了。
userInfo.IsPasswordNeverExpire = Convert.ToBoolean(userAccountControl & 0x10000); userInfo.IsDisabled = Convert.ToBoolean(userAccountControl & 0x0002); if ((isDisabled == userInfo.IsDisabled) && (IsPasswordNeverExpire == userInfo.IsPasswordNeverExpire)) { yield return userInfo; }
Filter语法请参考:http://msdn.microsoft.com/en-us/library/aa746475.aspx
DirectorySearcher类的PropertiesToLoad属性是要检索的属性列表,这个就相当于我们访问数据库时写SQL语句里SELECT后面的东西,最好按需指定,尽量不写“SELECT *”; 还有DirectorySearcher类的PageSize属性,如果要返回所有数据可以设为1000,否则是只返回1000条的。
directorySearcher.PropertiesToLoad.AddRange(new string[] { "name", "sAMAccountName", "userAccountcontrol", "pwdLastSet", "mail" }); directorySearcher.PageSize = 1000;
更多DirectorySearcher类属性请参考:http://msdn.microsoft.com/zh-cn/library/System.DirectoryServices.DirectorySearcher_properties(v=vs.80).aspx
用户密码的过期日期可以通过DirectoryEntry对象的InvokeGet方法获得,不过要加载一次DirectoryEntry的话,总觉得很浪费!
using (DirectoryEntry resultDirectoryEntry = userResult.GetDirectoryEntry()) { userInfo.PasswordExpirationDate = DateTime.Parse(resultDirectoryEntry.InvokeGet("PasswordExpirationDate").ToString()); }
所以我还是愿意自己算一下,用最近密码设置时间+系统设置的密码过期天数。最近密码设置时间对应“pwdLastSet”,如果用DirectoryEntry对象的Properties取,那是个“System.__ComObject”类型值,幸好SearchResult对象的“pwdLastSet”可以直接取为long,这个值是Windows文件时间,可以再转为本地时间。
long fileTime = (userResult.Properties["pwdLastSet"][0] as long?).GetValueOrDefault(); userInfo.PasswordLastSet = DateTime.FromFileTime(fileTime);
系统密码过期天数是通过组策略设置的,可以在OU路径下通过“maxPwdAge”属性获取,SearchResult对象的“maxPwdAge”也可以直接取为long。
directorySearcher.SearchScope = SearchScope.Base; directorySearcher.Filter = @"(objectClass=*)"; directorySearcher.PropertiesToLoad.Add("maxPwdAge"); SearchResult ouResult = directorySearcher.FindOne(); long maxPwdAge = 0; if (ouResult.Properties.Contains("maxPwdAge")) { maxPwdAge = TimeSpan.FromTicks((long)ouResult.Properties["maxPwdAge"][0]).Days * -1; }
最后,用户的密码过期就可以这么求了!
userInfo.MaxPasswordAge = maxPwdAge; if (!userInfo.IsPasswordNeverExpire) { userInfo.PasswordExpirationDate = userInfo.PasswordLastSet.AddDays(userInfo.MaxPasswordAge); }
查询用户信息OK,剩下贴段导出用户信息的代码:
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(); using (OleDbCommand oleDbCommand = new OleDbCommand()) { oleDbCommand.Connection = oleDbConnection; //const string sqlCreateTable = @"CREATE TABLE [Sheet1$] ([登录名] TEXT,[显示名] TEXT,[邮箱] TEXT,[已禁用] TEXT,[密码永不过期] TEXT,[密码过期设置天数] TEXT,[密码最近设置时间] TEXT,[密码过期时间] TEXT,[剩余密码过期天数] TEXT)"; //oleDbCommand.CommandText = sqlCreateTable; //oleDbCommand.ExecuteNonQuery(); foreach (var user in users) { oleDbCommand.CommandText = string.Format(@"INSERT INTO [Sheet1$]([登录名], [显示名], [邮箱],[已禁用], [密码永不过期], [密码过期设置天数],[密码最近设置时间],[密码过期时间],[剩余密码过期天数]) VALUES ('{0}','{1}','{2}','{3}','{4}','{5}','{6}','{7}','{8}');", user.SamAccountName, user.Name, user.Mail, user.IsDisabled ? "是" : "否", user.IsPasswordNeverExpire ? "是" : "否", user.MaxPasswordAge.ToString(), user.PasswordLastSet.ToString(), user.PasswordExpirationDate.ToString(), user.SurplusPasswordExpirationDays.ToString()); oleDbCommand.ExecuteNonQuery(); } } }View Code
还有使用SmtpClient发送邮件的代码:
using (SmtpClient smtpClient = new SmtpClient()) { if (!string.IsNullOrEmpty(mailMessageInfo.Host)) { smtpClient.Host = mailMessageInfo.Host; } if (!string.IsNullOrEmpty(mailMessageInfo.Port)) { smtpClient.Port = int.Parse(mailMessageInfo.Port); } smtpClient.Credentials = new System.Net.NetworkCredential(); if (!string.IsNullOrEmpty(mailMessageInfo.UserName)) { NetworkCredential networkCredential = new NetworkCredential { UserName = mailMessageInfo.UserName }; if (!string.IsNullOrEmpty(mailMessageInfo.PassWord)) { networkCredential.Password = mailMessageInfo.PassWord; } smtpClient.Credentials = networkCredential; } MailMessage mailMessage = new MailMessage(); if (!string.IsNullOrEmpty(mailMessageInfo.From)) { mailMessage.From = new MailAddress(mailMessageInfo.From); } foreach (string to in mailMessageInfo.ToList) { if (!string.IsNullOrWhiteSpace(to)) { mailMessage.To.Add(to); } } if (mailMessageInfo.CcList != null && mailMessageInfo.CcList.Length > 0) { foreach (string cc in mailMessageInfo.CcList) { if (!string.IsNullOrWhiteSpace(cc)) { mailMessage.To.Add(cc); } } } mailMessage.IsBodyHtml = true; string path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "templates", mailMessageInfo.TemplateFileName); string body = File.ReadAllText(path); Regex regexImg = new Regex(@"<img\s[^>]*>", RegexOptions.IgnoreCase); Regex regexSrc = new Regex( @"src=(?:(['""])(?<src>(?:(?!\1).)*)\1|(?<src>[^\s>]+))", RegexOptions.IgnoreCase | RegexOptions.Singleline); MatchCollection matchCollection = regexImg.Matches(body); Dictionary<string, string> contentIds = new Dictionary<string, string>(); foreach (Match matchImg in matchCollection) { if (regexSrc.IsMatch(matchImg.Groups[0].Value)) { Match matchSrc = regexSrc.Match(matchImg.Groups[0].Value); string srcValue = matchSrc.Groups["src"].Value; if (!srcValue.StartsWith("http:", System.StringComparison.OrdinalIgnoreCase) && !srcValue.StartsWith("file:", System.StringComparison.OrdinalIgnoreCase)) { if (srcValue.IndexOf("/") == 0) { srcValue = srcValue.Substring(1); } string attachmentContentId = Path.GetFileName(srcValue).Replace(".", string.Empty); body = body.Replace(matchSrc.Groups["src"].Value, "cid:" + attachmentContentId); if (!contentIds.ContainsKey(attachmentContentId)) { string inlinePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "templates", srcValue.Replace(@"/", @"\")); Attachment inline = new Attachment(inlinePath); inline.ContentDisposition.Inline = true; inline.ContentDisposition.DispositionType = DispositionTypeNames.Inline; inline.ContentId = attachmentContentId; if (srcValue.EndsWith("gif", StringComparison.OrdinalIgnoreCase)) { inline.ContentType.MediaType = MediaTypeNames.Image.Gif; } else { inline.ContentType.MediaType = MediaTypeNames.Image.Jpeg; } inline.ContentType.Name = Path.GetFileName(inlinePath); mailMessage.Attachments.Add(inline); contentIds.Add(attachmentContentId, null); } } } } mailMessage.Body = body; ; mailMessage.BodyEncoding = Encoding.UTF8; mailMessage.Subject = mailMessageInfo.Subject; mailMessage.SubjectEncoding = Encoding.UTF8; smtpClient.Send(mailMessage); }View Code
最后,将这些代码整合起来,就是检查用户密码过期的小工具了!由于笔者水平有限,文中难免会有些疏漏和错误,代码也有待不断优化,欢迎各位高手提出宝贵的建议!
参考资料:
DirectoryEntry 类使用 http://msdn.microsoft.com/zh-cn/library/z9cddzaa(v=vs.110).aspx
DirectorySearcher 类使用 http://msdn.microsoft.com/zh-cn/library/System.DirectoryServices.DirectorySearcher(v=vs.90).aspx
工具下载地址:http://files.cnblogs.com/CSharpDevelopers/UserPasswordSetChecker.zip