You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

修复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

火山引擎 最新活动