如何查询AD中近一年未登录用户及排查代码空引用错误
查询近一年未登录AD用户的实现方案及现有代码错误修复
LastLogon属性范围查询的可行性
该需求完全可实现,你之前没找到对应写法是因为UserPrincipal的强类型默认查询过滤器未直接暴露LastLogon的不等值匹配入口,不存在技术层面的限制。
*注意:AD原生的LastLogon属性不会在多台域控之间同步,单域控查询返回的LastLogon值可能不准确;如果不需要100%精确的登录时间,建议改用LastLogonTimeStamp属性做筛查,该属性会按固定周期在域控间同步,查询性能和结果准确率更适合过期账号扫描场景。如果必须用精确的LastLogon值,需要遍历域内所有域控,对每个用户取多域控返回的LastLogon最大值。
现有代码空引用错误的触发原因
你当前遇到的空引用异常、拿不到准确错误信息的问题,由以下几个问题共同导致,按触发概率从高到低排序:
- 内存过滤时未判断LastLogon空值:AD中从未登录过的内置账号、新建未启用账号的LastLogon属性为null,你写的
Where(Function(u) u.LastLogon < Today.AddYears(-1))没有提前判断null,直接做值比较会直接抛出空引用。而且你是先拉取域内所有用户到内存再做过滤,只要结果里存在一个LastLogon为null的账号,整行代码就会报错。 - 异常捕获块本身存在二次空引用问题:你在Catch块中直接调用
ex.InnerException.ToString(),如果当前异常没有内部异常(即ex.InnerException为null),这行代码会抛出新的空引用异常,覆盖原始错误信息,导致你无法看到真实的报错位置和原因。 - 未做AD连接的前置校验:如果站点运行账号没有AD读取权限、域地址填写错误,
PrincipalContext初始化或FindAll()返回结果可能为null,后续直接操作这些对象就会报错。 - 变量未显式声明:你代码中直接使用
dt变量但没有在方法内声明,如果页面类没有定义对应的全局变量,也会触发对象为空的错误。 - 逻辑bug:你在填充用户姓名时,把GivenName和Surname的拼接结果赋值给了
puid字段,而非name字段,会导致最终表格里PUID列显示姓名、Name列始终为默认值"-"。
修复后的可直接部署代码
以下代码修复了上述所有问题,并且改用AD端直接过滤的方式查询,不需要拉取全量用户到内存,性能远高于原实现:
Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load Dim domain As PrincipalContext = Nothing Dim dt As New DataTable() Dim inactiveUsers As New List(Of UserPrincipal) Dim oneYearThreshold As Date = Today.AddYears(-1) Try ' 初始化域连接 domain = New PrincipalContext(ContextType.Domain, "DomainNameHere") ' 初始化表格结构 dt.Columns.Add(New DataColumn("PUID", GetType(String))) dt.Columns.Add(New DataColumn("Name", GetType(String))) dt.Columns.Add(New DataColumn("Role", GetType(String))) dt.Columns.Add(New DataColumn("LastLogon", GetType(String))) dt.Columns.Add(New DataColumn("GroupCount", GetType(String))) ' 直接在AD端完成登录时间过滤,不返回全量用户 Using searchResults = UserPrincipal.FindByLogonTime(domain, oneYearThreshold, MatchType.LessThanOrEquals) ' 过滤空对象、仅保留启用状态的用户 inactiveUsers = searchResults. Cast(Of UserPrincipal)(). Where(Function(u) u IsNot Nothing AndAlso u.Enabled = True). ToList() End Using ' 填充表格,所有属性访问前做null校验 For Each u As UserPrincipal In inactiveUsers Dim puid As String = "-" Dim name As String = "-" Dim role As String = "-" Dim logon As String = "从未登录" Dim groupCount As String = "0" If u.SamAccountName IsNot Nothing Then puid = u.SamAccountName If u.GivenName IsNot Nothing AndAlso u.Surname IsNot Nothing Then name = $"{u.GivenName} {u.Surname}" If u.DisplayName IsNot Nothing Then role = u.DisplayName If u.LastLogon.HasValue Then logon = u.LastLogon.Value.ToString("dd MMM yyyy") dt.Rows.Add(puid, name, role, logon, groupCount) u.Dispose() Next rUsers.DataSource = dt rUsers.DataBind() Catch ex As Exception ' 修复异常输出的空引用问题,所有对象先判空再输出 litError.Text = String.Empty If domain IsNot Nothing Then litError.Text &= $"Connected Domain: {domain.Name}<br /><br />" End If litError.Text &= $"Error Source: {ex.Source}<br />" litError.Text &= $"Error Message: {ex.Message}<br /><br />" If ex.InnerException IsNot Nothing Then litError.Text &= $"Inner Exception: {ex.InnerException.Message}<br />{ex.InnerException.StackTrace}<br /><br />" End If litError.Text &= $"Full Stack Trace:<br />{ex.StackTrace}" Finally ' 手动释放AD连接资源,避免连接泄漏 If domain IsNot Nothing Then domain.Dispose() End Try End Sub
部署前注意事项
- 确认站点运行的应用程序池所用账号,对目标域拥有用户信息的读取权限,否则会出现连接被拒绝的错误。
- 如果域内有多台域控,且需要100%准确的最后登录时间,需要补充遍历所有域控查询LastLogon取最大值的逻辑,避免漏判或误判。
- 所有AD相关操作对象都实现了IDisposable接口,用完必须及时释放,否则长时间运行会占满AD连接池导致服务不可用。
内容的提问来源于stack exchange,提问作者P S
相关产品推荐
相关产品推荐

