修复PowerShell脚本LastLogon显示1/01/1601异常问题
修复后的PowerShell脚本:获取Domain Admins成员并导出正确的登录时间
先直接给你修复好的脚本,之后我会逐一解释问题出在哪以及怎么解决的:
$ADGroupType = 'Security' $ADGroupNamePattern = 'Domain Admins' $ResultFile = "C:\Result.csv" # 修复后的DN转CanonicalName函数,直接返回字符串而非对象 function ConvertFrom-DN { [cmdletbinding()] param( [Parameter(Mandatory,ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$True)] [ValidateNotNullOrEmpty()] [string[]]$DistinguishedName ) process { foreach ($DN in $DistinguishedName) { $OU = @() $DC = "" $CN = "" Write-Verbose $DN foreach ($item in ($DN.replace('\,','~').split(","))) { switch ($item.TrimStart().Substring(0,2)) { 'CN' {$CN = '/' + $item.Replace("CN=","")} 'OU' {$OU += $item.Replace("OU=","")} 'DC' {$DC += $item.Replace("DC=","") + '.'} } } # 构建CanonicalName $CanonicalName = $DC.Substring(0,$DC.length - 1) # 反向拼接OU路径 if ($OU.Count -gt 0) { [array]::Reverse($OU) $CanonicalName += '/' + ($OU -join '/') } # 添加CN部分 if ($DN.Substring(0,2) -eq 'CN') { $CanonicalName += $CN.Replace('~','\,') } # 直接返回字符串 Write-Output $CanonicalName } } } # 修复递归函数:传递根组名称,确保所有成员的Group列显示根组 Function Get-ADGroupMemberRecursive { [CmdletBinding()] Param( [Parameter(ValueFromPipeline=$true)] $Identity, [string[]]$Property, # 新增参数:存储根组名称 [string]$RootGroupName ) Begin { $splat = @{} If ($Property) {$splat['Property'] = $Property} # 如果是第一次调用,设置根组名称 if (-not $RootGroupName) { $RootGroupName = $Identity.Name } } Process { Get-ADGroupMember -Identity $Identity | ForEach-Object { If ($_.objectClass -eq 'User') { Get-ADUser -Identity $_ @splat | Select-Object -Property @{n='Group'; e={$RootGroupName}}, Name, SamAccountName, Mail, whenCreated, DistinguishedName, lastLogon, lastLogonTimeStamp } ElseIf ($_.objectClass -eq 'Group') { # 递归调用时传递根组名称 Get-ADGroupMemberRecursive -Identity $_ @splat -RootGroupName $RootGroupName } } } } # 获取所有域控制器,用于查询每个用户的lastLogon最大值 $allDCs = Get-ADDomainController -Filter * # 获取Domain Admins组并递归获取成员 Get-ADGroup -Filter "(groupCategory -eq '$ADGroupType') -AND (name -like '$ADGroupNamePattern')" | Get-ADGroupMemberRecursive -Property Mail | ForEach-Object { $user = $_ # 查询所有DC上的lastLogon值,取最大的那个 $maxLastLogon = $allDCs | ForEach-Object { Get-ADUser -Identity $user.SamAccountName -Server $_.HostName -Properties lastLogon | Select-Object -ExpandProperty lastLogon } | Measure-Object -Maximum | Select-Object -ExpandProperty Maximum # 处理lastLogonTimeStamp的空值 $lastLogonTimeStamp = if ($user.lastLogonTimeStamp) { [datetime]::FromFileTime($user.lastLogonTimeStamp) } else { $null } # 处理maxLastLogon的空值(用户从未登录过) $lastLogon = if ($maxLastLogon -and $maxLastLogon -ne 0) { [datetime]::FromFileTime($maxLastLogon) } else { $null } # 生成输出对象 [PSCustomObject]@{ Group = $user.Group Name = $user.Name SamAccountName = $user.SamAccountName Mail = $user.Mail whenCreated = $user.whenCreated 'Last Logon' = $lastLogon 'Last Logon TimeStamp' = $lastLogonTimeStamp 'OU Location' = ConvertFrom-DN $user.DistinguishedName } } | Export-Csv -Path $ResultFile -NoTypeInformation -Encoding UTF8 # 打开结果文件 ii $ResultFile
关键修复点说明
1. 解决lastLogon显示1601年的问题
lastLogon属性是非复制属性,每个域控制器只存储用户在该DC上的登录记录。如果用户从未在当前连接的DC登录过,这个值就是0,转成时间就是1601年(Windows时间戳的起始点)。
修复方案:
- 获取所有域控制器,查询每个用户在所有DC上的
lastLogon值,取最大值作为用户的最后登录时间。 - 增加空值判断,如果所有DC的
lastLogon都是0,就返回$null而不是1601年的时间。
2. 修复OU Location列的输出问题
原ConvertFrom-DN函数返回的是PSCustomObject,但在Select-Object的计算属性中直接调用会把整个对象写入CSV,导致列内容混乱。
修复方案:
- 修改函数直接返回
CanonicalName字符串,而不是包装成对象。 - 调整OU的拼接逻辑,避免原脚本中的数组索引错误。
3. 修正递归组的Group名称显示
原递归函数中,当处理子组时,Group列会显示子组的名称,而不是根组Domain Admins,导致结果中Group列不一致。
修复方案:
- 给递归函数新增
RootGroupName参数,第一次调用时传入根组名称,后续递归调用时持续传递这个值,确保所有成员的Group列都显示根组名称。
4. 处理lastLogonTimeStamp的空值
如果用户从未登录过域,lastLogonTimeStamp会为空,直接调用[datetime]::FromFileTime()会报错。修复时增加判断,为空则返回$null。
内容的提问来源于stack exchange,提问作者Senior Systems Engineer




