Share via


嗨,Scripting Guy!你是誰?

Microsoft Scripting Guy

下載本文程式碼: HeyScriptingGuy2007-082007_08.exe (152KB)

不久前,撰寫本專欄的 Scripting Guy 看了一場電視棒球賽。比賽播完後,他打算看一下雜誌,不過電視仍開著。看完雜誌,抬頭一看電視正在播放一部二戰舊片,他剛好看見查驗身分那一幕:一名美國大兵守夜哨時聽見聲響。

「你是誰?」哨兵厲聲說道。

「是我啊,史密斯中士。」對方回答。

「史密斯中士?部隊裡沒有什麼史密斯中士。」

「我是新來的,剛從第一連調來。」

「是嗎?從第一連調來?那好吧,史密斯:誰贏了 1934 年世界大賽?」

「紐約洋基隊。」

答錯了,「史密斯」!(想也知道,史密斯立刻被逮,丟進大牢)。任何正港的美國人都知道是聖路易紅雀隊 (就是迪恩 [Dizzy Dean] 與瓦斯房幫 [Gas House Gang]) 贏了 1934 年世界大賽。而不知道的人肯定都是間諜。

如果本月專欄的讀者中有人不曉得聖路易紅雀隊贏了 1934 年世界大賽,我們只能說你玩完了,我們看穿你就是間諜。希望你行行好,向最近的 FBI 機關自首。要不然至少也撥通電話給 FBI,只要涉及間諜事宜,FBI 都願意提供接送服務。

撰寫這篇專欄的 Scripting Guy 一邊看著電視,一邊想著他一定可以答對哨兵的第二個問題:誰贏了 1966 年世界大賽?巴爾的摩金鶯隊。1960 年世界大賽?匹茲堡海盜隊。1994 年世界大賽?嘿!這題有陷阱:1994 年沒有世界大賽。

不過,在現在這個世代,他卻很難回答第一個問題:你是誰?這個問題再也不像 1940 年代那時一樣容易回答。畢竟,使用者只要利用 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 就好了,不是嗎?這段指令碼利用鮮為人知 (但十分有用) 的 ADSI 物件,稱為 ADSystemInfo。這個小巧的物件可以傳回有關目前登入本機電腦的使用者的各種資訊,也可以傳回有關本機電腦本身以及所屬網域的所有資訊。舉例來說,請看一下 [圖 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 類別 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。換言之,這個技術很實用,只不過....

那麼第二種使用 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。輸出參數只是我們命名及提供給方法的變數而已;此方法接著會將值指定給這些輸出參數。此時,strUser 會被指定為已登入使用者的登入名稱 (kenmyer),而 strDomain 則被指定為已登入使用者的網域名稱 (FABRIKAM)。接下來我們只需要回應這兩個輸出參數的值即可:

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 的人都是這樣說話的,只要對 Scripting Guy 說:「我們需要分類所有關係人的心得重點、交辦事項和非目標事項。」,肯定會把他逼瘋)。首先,我們已經知道如何取得登入本機或遠端電腦的使用者的相關資訊。更重要的是,萬一我們不小心穿越時空隧道,回到第二次世界大戰,至少我們知道該怎麼做:記得印出本專欄並隨身攜帶 (以便回答「你是誰?」這個問題),而且無論如何都要帶著一份世界大賽歷屆冠軍名單。畢竟您永遠猜不到,何時會有人上前問您:「誰贏了 1903 年世界大賽?」

附註:答案是波士頓紅襪隊。附帶一提,這正是第一屆世界大賽。那麼,您認為誰贏了 1904 年世界大賽?您說不知道是什麼意思?請容我們失陪一下,得打個電話...。

Microsoft Scripting Guy 為 Microsoft 做事,也就是受雇於 Microsoft。在比賽、訓練、看棒球賽 (以及其他各種活動) 之餘,他們也負責管理 TechNet Script Center。請造訪他們的網站 www.scriptingguys.com

© 2008 Microsoft Corporation and CMP Media, LLC. 保留所有權利;未經允許,嚴禁部分或全部複製.