您好,脚本专家!您是谁?

Microsoft 脚本专家

下载这篇文章的代码: HeyScriptingGuy2007-082007_08.exe (152KB)

不久前,编写此专栏的脚本专家还在通过电视观看棒球比赛。当比赛结束时,他决定阅读杂志,但仍开着电视机。看完杂志后,他抬头看了看电视,结果发现正在播放的是第二次世界大战电影。实际上,当他看电视的时候,影片正好播放这些经典场景之一:深夜,一个年轻美国步兵正在站岗,突然他听到有声音。

“你是谁?”哨兵叫道。

“是我,史密斯中士,”有声音回答道。

“史密斯中士?这个部队就没有史密斯中士这个人。”

“我是新来的,刚从 A 连队转来。”

“噢,是吗?从 A 连队转来的?那好,史密斯:谁赢得了 1934 年世界职业棒球大赛冠军?”

“纽约杨基棒球队。”

回答错误,“史密斯”!(正如您所料,此人迅速被逮捕并关入监狱。)任何一个真正的美国人都知道 St. Louis Cardinals(如您所知,Dizzy Dean 和 Gas House Gang)赢得了 1934 年的世界职业棒球大赛冠军。如果有人不知道这一点,那么只有一种可能:他是间谍。

如果阅读本月专栏的人不知道 St. Louis Cardinals 赢得了 1934 年的世界职业棒球大赛冠军,那么,我们能说的就是:诡计就被识破了;我们知道您是个间谍。我们希望您将为了体面而主动去最近的 FBI 办公室。或至少给他们打个电话;当遇到间谍时,FBI 将带走并移送。

根据编写此专栏的脚本专家所观看的内容,他认识到他已经有回答哨兵第二个问题的大好机会:谁赢得了 1966 年的世界职业棒球大赛冠军?Baltimore Orioles。1960 年的世界职业棒球大赛?Pittsburgh Pirates。1994 年的世界职业棒球大赛?哈!一个陷阱式问题:1994 年没有举行世界职业棒球大赛。

但是,在当今这个时代,他要回答第一个问题可能相当困难:您是谁?回答这个问题并不像在二十世纪四十年代那么简单。毕竟,单独使用 Active Directory®,用户可以拥有任意数目的不同身份,包括:

  • 名 (givenName)。
  • 姓 (sn)。
  • 显示名 (displayName)。
  • 用户主要名称 (userPrincipalName)。
  • 登录名 (samAccountName)。
  • 可分辨名称 (distinguishedName)。

所有这些名称均标识同一个人的身份,需要记住它们以在不同情况下使用。这才是问题所在。大多数用户知道他们的名和姓。但是,如果您问某用户,“您的可分辨名称是什么?”仅有少数用户能够回答上来,“嗯,这个问题简单!我的信息是 CN=Ken.Myer,OU=Finance,DC=fabrikam,DC=com。但是我的朋友们都叫我 CN=Ken.Myer。”

作为系统管理员或服务台人员,您需要知道这些名称。但是您应该如何获得此类信息?嗯,一种方法是引导您的用户最终说出他们的可分辨名称。这种方法可能有效,但现在大多数人力资源部门不提倡这种做法。所以您也许不得不回退到计划 B:脚本。哪种脚本?嗯,对于初学者而言,图 1 中的脚本如何?

Figure 1 使用 ADSystemInfo 检索用户属性

On Error Resume Next

Set objSysInfo = CreateObject("ADSystemInfo")
strUser = objSysInfo.UserName

Set objUser = GetObject("LDAP://" & strUser)
WScript.Echo "First Name: " & objUser.givenName
WScript.Echo "Last Name: " & objUser.sn
WScript.Echo "Display Name: " & objUser.displayName
WScript.Echo "User Principal Name: " & objUser.userPrincipalName
WScript.Echo "SAM Account Name: " & objUser.sAMAccountName
WScript.Echo "Distinguished Name: " & objUser.distinguishedName

朋友,要是史密斯中士学过 VBScript 该多好,是吧?此脚本利用了罕为人知(但很有用)的名为 ADSystemInfo 的 ADSI 对象。这是一个超酷的小对象,它不仅能够返回有关本地计算机本身及其所属域的信息,还能够返回有关当前登录到本地计算机的用户的各种信息。例如,请看图 2

Figure 2 显示各类域信息

On Error Resume Next

Set objSysInfo = CreateObject("ADSystemInfo")

Wscript.Echo "User name: " & objSysInfo.UserName
Wscript.Echo "Computer name: " & objSysInfo.ComputerName
Wscript.Echo "Site name: " & objSysInfo.SiteName
Wscript.Echo "Domain short name: " & objSysInfo.DomainShortName
Wscript.Echo "Domain DNS name: " & objSysInfo.DomainDNSName
Wscript.Echo "Forest DNS name: " & objSysInfo.ForestDNSName
Wscript.Echo "PDC role owner: " & objSysInfo.PDCRoleOwner
Wscript.Echo "Schema role owner: " & objSysInfo.SchemaRoleOwner
Wscript.Echo "Domain is in native mode: " & objSysInfo.IsNativeMode

但是,今天我们只关心 UserName 属性。UserName 有何特殊之处?嗯,它正好与 distinguishedName 属性相对应。那么 distinguishedName 又有何特殊之处呢?嗯,distinguishedName 与 UNC 文件路径类似:正如 UNC 路径使我们能够唯一地标识网络的某一位置上的文件一样,distinguishedName(例如,CN=Ken.Myer,OU=Finance,DC=fabrikam,DC=com)使我们能够唯一标识在 Active Directory 中的用户帐户。这样,我们就能够绑定到该用户帐户了。而且,一旦建立该连接,我们就能够检索有关用户的任何信息 — 包括他是谁。

这也正是我们在向您展示的第一个脚本中所执行的操作。首先,我们创建了 ADSystemInfo 对象的实例,然后将 UserName 属性值赋给名为 strUser 的变量:

Set objSysInfo = CreateObject("ADSystemInfo")
strUser = objSysInfo.UserName

请注意,我们不必在此执行任何操作。例如,我们不必指定用户名、用户域或用户帐户所在的 OU;ADSystemInfo 为我们做好了这一切。一旦获得了用户的 distinguishedName,我们就能够使用以下代码行绑定到该用户的用户帐户:

Set objUser = GetObject("LDAP://" & strUser)

同样,一旦建立连接,我们就能够回显该帐户的任一 Active Directory 属性的值。在我们的示例脚本中,我们只回显了用户的某些名称属性,但我们还可以同样轻松地检索电话号码、办公地点和电子邮件地址等。

那太好了。您必须执行的操作就是将此脚本分发给您的所有用户,然后他们将再也不必问自己,“我是谁?”(或者,如果他们问此问题,他们也将有一个轻松快捷的方法获得答案。)但是,如果是相关问题:您是谁?这是能够识别登录到本地计算机的用户的问题。但到底您如何才能确定谁登录到了远程计算机呢?如您所料,这是一个更为棘手的问题。

很抱歉,我们也想到了此问题;遗憾的是,您无法直接将我们的 ADSystemInfo 脚本指向远程计算机。原因是只能在本地创建 ADSystemInfo 对象。这就给予了我们三种可能:

  • 在可轻松访问的位置创建记录登录用户姓名的登录脚本。
  • 使用 WMI class Win32_ComputerSystem 和 UserName 属性。
  • 执行其他操作。

第一个选项听起来非常吸引人。您可能已经注意到,ADSystemInfo 对象能够返回计算机的可分辨名称(ComputerName 属性)以及用户的可分辨名称。如果您愿意的话,请看图 3 中所示的示例脚本。

Figure 3 登录脚本

On Error Resume Next

Set objSysInfo = CreateObject("ADSystemInfo")

strUser = objSysInfo.UserName
strComputer = objSysInfo.ComputerName

Set objUser = GetObject("LDAP://" & _
    strUser)
strUserName = objUser.displayName

Set objComputer = GetObject("LDAP://" & _
    strComputer)
objComputer.Description = strUserName
objComputer.SetInfo

我们正在执行何种操作?首先,我们要获取 UserName 和 ComputerName 属性的值并将它们存储在一对变量(strUser 和 strComputer)中。然后,我们在 Active Directory 中绑定到用户帐户(与前面的操作一样),检索 displayName 属性值(与前面的操作一样)并将该值存储在名为 strUserName 的变量中。非常简单,对吧?

这样,我们就能够使用以下代码行连接到 Active Directory 计算机帐户:

Set objComputer = GetObject("LDAP://" & _
    strComputer)

建立连接后,我们将用户的 displayName 分配给计算机的 Description 属性,然后调用 SetInfo 方法以将此更改写入 Active Directory:

objComputer.Description = strUserName
objComputer.SetInfo

为什么要这样做?道理很简单。假定 Ken Myer 登录计算机 atl-ws-01。请猜测一下,atl-ws-01 的 Description 属性值将是什么?答对了:Ken Myer,当前登录到该计算机的用户。想知道谁登录到了 atl-ws-01?只需查看 Description 属性即可。

现在,总的来说,此方案效果相当好 — 但如果您拥有一个注销脚本,能够在用户注销以后清除 Description 属性,那么效果将更好。但这并不是万无一失的解决方案。为什么呢?因为登录脚本不会始终运行。例如,如果用户使用 RAS 登录,那么通常它们就不会运行。同样,如果用户断开自己的网络连接,使用缓存的凭据登录,然后再将计算机连接到网络,那么登录脚本也不会运行。假定用户未注销就关闭了计算机。这就意味着他的注销脚本将永远不会运行。在此情况下,即使该计算机并未运行,Ken Myer 也仍可能能够登录到 atl-ws-01。换句话说,这是一项有用的技术,但也存在问题。

那么选项 2,使用 Win32_ComputerSystem 类,怎么样?同样,此选项通常有效,但 Win32_ComputerSystem 类的问题是它并不是始终返回已登录用户的名称,尤其是对于不具有管理员权限的用户(以及对于运行 Windows® 2000 的计算机)。图 4 中的脚本也许将告诉您登录到计算机的用户,但同样没有任何保证。

Figure 4 使用 WMI 查找登录的用户

strComputer = "."

Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colItems = objWMIService.ExecQuery("Select * From Win32_ComputerSystem")

For Each objItem in colItems
  Wscript.Echo objItem.UserName
Next

顺便说一下,在此情况下将以 domain\username 的格式返回 UserName 属性。换句话说:FABRIKAM\kenmyer。

噢,我们几乎忘了:即使确实返回了一个名称,也不意味着用户真的登录到了该计算机。当 Ken Myer 注销 atl-ws-01 后,他的名字以 UserName 属性值的形式被保留,并且直到他人登录时其姓名才会被取代。

真是让人厌烦。

但请稍候;这不会使我们成为间谍。我们可以采取下面的另一种方法。如果有人登录到某计算机,那么 Explorer.exe 进程必定正在运行。一般来讲,如果 Explorer.exe 没有运行,则没有人登录该计算机。由于 Explorer.exe 使用已登录用户的凭据运行,所以通过使用类似于图 5 中所示脚本的脚本,我们几乎总是能够确定登录到某计算机的用户。

Figure 5 确定 Explorer.exe 的所有者

strComputer = "atl-ws-01"

Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")

Set colItems = objWMIService.ExecQuery _
  ("Select * from Win32_Process Where Name = 'explorer.exe'")

If colItems.Count = 0 Then
  Wscript.Echo "No one is logged on to the computer."
Else
  For Each objProcess in colItems
    objProcess.GetOwner strUser, strDomain
    Wscript.Echo strDomain & "\" & strUser
  Next
End If

您可以看到,在这种情况下,我们将连接到远程计算机(准确地说,是 atl-ws-01)上的 WMI 服务。然后,我们使用以下代码行检索名为“Explorer.exe”的 Win32_Process 类的所有实例的集合:

Set colItems = objWMIService.ExecQuery _
  ("Select * from Win32_Process Where " & _
  "Name = 'explorer.exe'")

现在要做什么?嗯,正如我们刚刚所说的,如果 Explorer.exe 没有运行,那么很可能没有人登录到该计算机。我们如何知道 Explorer.exe 是否正在运行?一种非常简单的方法是查看该集合的 Count 属性值。如果 Count 等于 0,那么我们拥有的是空集合;而只有在 atl-ws-01 上没有运行 Explorer.exe 实例时,我们才会拥有空集合。如果是这样,我们将回显一条消息,表明没有人登录到该计算机:

Wscript.Echo "No one is logged on " & _
"to the computer."

如果 Count 不等于 0,我们将建立一个 For Each 循环,以遍历名为 Explorer.exe 的进程的集合(我们假定在集合中总是仅包含一个项目)。对于每个 Explorer.exe 实例,我们将调用 GetOwner 方法以确定 Explorer.exe 正在使用谁的帐户运行:

objProcess.GetOwner strUser, strDomain

请注意,我们将一对输出参数传递给 GetOwner:strUser 和 strDomain。输出参数仅仅是我们命名并提供给某一方法的变量;然后该方法将为这些输出参数赋值。在此例中,会将已登录用户的登录名 (kenmyer) 赋给 strUser,将已登录用户的域名 (FABRIKAM) 赋给 strDomain。我们接下来要做的是回显这两个输出参数的值:

Wscript.Echo strDomain & "\" & strUser

您知道吗?这相当好。但我们可以更进一步。当我们使用 GetOwner 方法时,我们将获得登录到该计算机的用户的登录名 (samAccountName)。这很好,但是,正如我们前面提到的,用户除了 samAccountName 外还拥有各种名称。要如实地回答“您是谁?”这个问题,最好知道用户的 displayName。但我们无法使用 GetOwner 确定用户的其他名称,是吧?

是的,无法确定。不过,我们可以获得 samAccountName,将其插入到 Active Directory 搜索脚本,然后使用该登录名查找并绑定到用户帐户(由于 samAccountNames 在域中必须是唯一的,该任务变得更容易)。而且,一旦绑定到用户帐户,我们就能够回显任一 Active Directory 属性的值,包括 displayName。

我们没有时间详细介绍图 6 中的脚本;有关搜索 Active Directory 的更多信息,请访问“都德,我的打印机在哪儿?”。只需说明此脚本确定已登录用户的登录名,搜索 Active Directory 以查找具有该登录名 (samAccountName) 的用户,绑定到所涉及的用户帐户,然后回显该用户的 displayName 就足够了。所有这一切可谓轻而易举,不费吹灰之力!

Figure 6 绑定到已登录用户

strComputer = "atl-ws-01"

Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")

Set colItems = objWMIService.ExecQuery _
  ("Select * from Win32_Process Where Name = 'explorer.exe'")

If colItems.Count = 0 Then
  Wscript.Echo "No one is logged on to the computer."
Else
  For Each objProcess in colItems
    objProcess.GetOwner strUser,strDomain
  Next
End If

Const ADS_SCOPE_SUBTREE = 2

Set objConnection = CreateObject("ADODB.Connection")
Set objCommand = CreateObject("ADODB.Command")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"
Set objCommand.ActiveConnection = objConnection

objCommand.Properties("Page Size") = 1000
objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE 

objCommand.CommandText = "SELECT displayName FROM " & _
  "'LDAP://DC=wingroup,DC=fabrikam,DC=com' WHERE " & _
    "objectCategory='user' " & _
    "AND samAccountName = '" & strUser & "'"
Set objRecordSet = objCommand.Execute

objRecordSet.MoveFirst

Do Until objRecordSet.EOF
  Wscript.Echo objRecordSet.Fields("displayName").Value
  objRecordSet.MoveNext
Loop

那么从今天开始,我们的主要结论是什么?(顺便说一下,也就是 Microsoft 人员的意见。如果您希望将脚本专家逼得发疯,仅仅走近他,然后对他说一些如“我们需要排列所有利益相关方的主要结论、操作项和非目标的优先级”的话。)嗯,首先,现在我们知道如何获取有关登录到计算机(本地计算机或远程计算机)的用户的信息。更重要的是,如果我们能穿越时空,然后发现自己置身于第二次世界大战中,我们知道该干什么:确保您阅读了本专栏(所以您就可以回答“您是谁”这个问题了),还有 — 无论做什么事 — 要始终随身携带一个世界职业棒球大赛冠军的列表。毕竟,您不知道何时有人将问到您,“谁赢得了 1903 年的世界职业棒球大赛冠军?”

注意:Boston Red Sox。顺便说一下,这是第一届世界职业棒球大赛。现在,你认为谁赢得了 1904 年的世界职业棒球大赛?您什么意思,您不知道吗?请稍等我们片刻,我们需要打个电话....

Microsoft 脚本专家为 Microsoft 工作,也就是受雇于 Microsoft。在玩、教或看棒球(以及各种其他活动)的闲暇之余,他们还负责维护 TechNet 脚本中心。请访问 www.scriptingguys.com 进行查看。

© 2008 Microsoft Corporation 与 CMP Media, LLC.保留所有权利;不得对全文或部分内容进行复制.