脚本的故事

2005 年 4 月

发布日期: 2005年04月01日
*

伙计,我的打印机在哪里:使用脚本在 Active Directory 中执行搜索(第 1 部分)

如果您曾在某个星期六,花一整个下午,在屋里漫无目标地四处寻找您的汽车钥匙或电视遥控器,那么请不要觉得太难过:好多人都和您有同样的经历。实际上,人类的历史就有点像人们不断搜寻各种事物(大多都没找到)的过程,比如找寻迷失的大陆亚特兰蒂斯 (Atlantis)、青春之泉 (Fountain of Youth) 或某位 Microsoft 脚本专家在从车道到客厅的这一小段路上所丢失的一份报纸。

注意:不要多问。我们只能说,针对这位脚本专家,这种情况并不鲜见。

假如您是一名系统管理员,那么您可能已经只身踏上了一些伟大的发现和探索之旅:

“我需要知道我们有多少台计算机在运行 Windows XP,其中又有多少台安装了 Service Pack 2。”

“我们的底特律分部不是已经有了充足的彩色打印机了吗?”

“我们需要一份列有所有员工姓名及其家庭电话号码的名单。”

不幸的是,对于许多人,这些探寻就像寻找埃尔多拉多(El Dorado,传说中的黄金城)那样毫无结果。寻找埃尔多拉多城的探险家们大多在南美的丛林里四处搜寻,希望一不小心能被传说中的宝藏绊倒。不过,至今这种方法也没奏效;事实上,沃尔特·雷利爵士最后被处死了,其中一部分原因就是因为他没找到埃尔多拉多城。(幸好,在 Microsoft,我们不会因某人犯了一个错,就将他处决。不过,想想看,有人最近见过Peter 吗?)

同样,底特律分部的系统管理员(或他们的代理人)也将经常会在屋里徘徊,希望能被彩色打印机绊倒。假如通过这种方法能找到埃尔多拉多城的话,那么通过它也 找到底特律分部的所有彩色打印机;只不过这个过程需要很长的时间,会耗费很多金钱(毕竟,用在让您和您的代理人在底特律分部的走廊里四处搜寻的资金可以用于其它项目),而且很容易受错误的干扰。没有更好的办法吗?

事实上有:只要编写一个简单的小脚本来在 Active Directory 中执行搜索,就行了。

返回页首返回页首

你是说 Active Directory 吗?

对,Active Directory。大家都知道,Active Directory 是存储用户和计算机帐户的地方,但不是每个人都能认识到 Active Directory 中存储(或至少可以存储的)了多少其它类型的重要信息。其中,有些信息是可以随意索取的。譬如,当您将计算机加入域时,诸如计算机上安装的操作系统(以及服务包的版本号)等信息就会自动添加到计算机帐户中;而当您在 Active Directory 中发布打印机时,也会得到这类附加信息,比如:该打印机是否支持彩色打印,以及是否支持装订和双面打印。

其它信息则需要您付出一点劳动才能取得:不用说,当您在 Active Directory 中添加新用户时,目录服务不会自动导入用户的移动电话号码及其所属的部门。但是,已经 在 Active Directory 中设置了用于存储员工编号和职务等信息的空间。假如您要亲自提供这些信息,那么您将得到一个包含大量信息的数据库,可以使用简单的脚本进行反复挖掘。

注意:这条主矿脉究竟 多丰富呢?够丰富的,因为这个 脚本的故事 专栏文章实际上是一套分为两部分的系列文章的第 1 部分。本月,我们将介绍有关 Active Directory 搜索的基础知识;而下个月,我们将进一步介绍您可以搜索的信息,以及如何将搜索功能结合到系统管理脚本中。正好,在这里还得告诉您一下,本月的这篇专栏文章要求读者对 ADSI:Active Directory Service Interfaces 有基本的了解.假如您还不了解的话,那么可以参阅“Microsoft Windows 2000 脚本编写指南”中的ADSI 入门

现在,就让我们来谈谈如何编写脚本,来挖掘这条主矿脉。首先,让我们来看一个用于搜索域中所有用户帐户的脚本,然后再探讨该脚本及其工作原理:

On Error Resume Next

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 objCommandProperties(“Sort On”) = "Name"

objCommand.CommandText = _ "SELECT Name FROM 'LDAP://dc=fabrikam,dc=com' WHERE objectCategory='user'" Set objRecordSet = objCommand.Execute

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

现在,我们知道您在想什么:“我还是去找埃尔多拉多城好了;与水虎鱼和毒蛇搏斗,听上去比对付 ADsDSOObject 这类东西还要轻松一些。”请放松:这个脚本实际上没有看上去那么可怕。大致上,在编写 Active Directory 搜索脚本时,只有一小部分项目需要您操心。其它都属于样板文本,不管您是要搜索域中的所有子网或是 Contoso 公司所有员工的联系信息,都不用加以修改。

我们知道:根据传说的描述,埃尔多拉多城的国王每天醒来后,都会到湖边拿手蘸一点水,然后随处洒下金粉。(很像脚本专家们早上例行的公事。)如今,很少有人会相信有关埃尔多拉多城的传说;那么,为何我们要让您相信有关 Active Directory 的传说——可以用一个基础的搜索脚本(只需进行一些小修改),在 Active Directory 中搜索任何东西。因为,这次的传说是真的。

为了向所有怀疑者和嘲讽者证明这一点,让我们将这个脚本分为四部分:

使用 Active Directory ADO 提供程序,创建到 Active Directory 的连接

指定 Command 对象参数

创建并执行一个查询

循环返回的记录集

掌握这四部分的内容,您就可以很好的掌控 Active Directory 搜索。

返回页首返回页首

使用 Active Directory ADO 提供程序,创建到 Active Directory 的连接

使用 Active Directory ADO(ActiveX 数据对象)提供程序创建到 Active Directory 的连接,这一步骤可以完全用样板脚本来实现;实际上,您可以在每个脚本中使用完全相同的代码,完全不用修改:

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

这个部分就会吓退一些人。但是,真的没有理由害怕:您只要在此创建两个对象——Connection 对象和 Command 对象,然后使用这两个对象连接到目录服务。Connection 对象用于加载 ADO Active Directory 数据库提供程序,并验证用户凭据。建立连接后,使用 Command 对象发出命令,并运行查询。这个步骤需要五行代码,但是正如我们所说的,这五行代码可用于所有 Active Directory 搜索脚本。

附加说明:使用其它凭据

在默认情况下,Active Directory 数据库提供程序使用与您登录时所用的相同安全凭据,连接到 Active Directory。但是,假如您要使用 不同 的安全凭据,连接到 Active Directory,又该怎么办呢?譬如,假设您原先作为普通用户登录,但现在需要作为管理员,连接到 Active Directory?没问题;只需使用类似如下的代码(指定其它凭据的代码与脚本的其它部分用空白行隔开):

Set objConnection = CreateObject("ADODB.Connection") Set objCommand =   CreateObject("ADODB.Command") objConnection.Provider = "ADsDSOObject"

objConnection.Properties("User ID") = "Administrator" objConnection.Properties("Password") = "irte56$#sW" objConnection.Properties("Encrypt Password") = TRUE objConnection.Properties("ADSI Flag") = 1 

objConnection.Open "Active Directory Provider" Set objCommand.ActiveConnection = objConnection

如果您不明白上述属性的具体意义,那么我们在寻找电视遥控器时,碰巧被一个能够提供详细说明的表格绊倒了:

属性

描述

User ID

这就是执行搜索时将用到的安全上下文所属的用户帐户。名称的格式可以有好几种:
kenmyer
fabrikam\kenmyer
kenmyer@fabrikam.com

Password

User ID 属性中指定的用户帐户所对应的密码。

Encrypt Password

用于指定是否对密码进行加密的布尔值。默认为 False。

ADSI Flag

用于指定绑定身份验证选项的一组标志。默认为 0。常用于连接到 Active Directory 的标志包括(具体的值显示在圆括号中)
ADS_SECURE_AUTHENTICATION (1):要求安全身份验证。
ADS_USE_ENCRYPTION (2):要求 ADSI 对网络上的数据交换进行加密。
ADS_USE_SIGNING (40):验证数据完整性。还必须将 ADS_SECURE_AUTHENTICATION 标志设为使用签名。
ADS_USE_SEALING (80):使用 Kerberos 加密数据。还必须将 ADS_SECURE_AUTHENTICATION 标志设为使用密封。

返回页首返回页首

指定 Command 对象参数

与 Active Directory 建立了连接后,使用 Command 对象,发出查询命令,并设定搜索参数。我们将稍候介绍有关发出查询命令的内容;现在,让我们先说说三个需要您处理的 Command 对象:

Page Size

Searchscope

Sort On

返回页首返回页首

Page Size

大概在脚本历史上,从来没有一个对象像页大小这样受到如此大的误解(或造成如此大的困扰)。下面将解释具体的原因。在默认情况下,每次您编写脚本来在 Active Directory 中执行搜索时,都会得到最多 1,000 个项目。无论在您的域中有 1,001 用户帐户,还是 100,001 个用户帐户:都只会有 1,000 个返回给您。

不过,等一下,别走开;有一个办法可以消除 1,000 个记录的上限。具体的技巧就是在您的脚本中指定一个 Page Size。假设您将页大小指定为 500,所用的代码如下:

objCommand.Properties("Page Size") = 500

设置了这个 Page Size 后,您的脚本将返回所找到的前 500 个记录。该脚本将暂停片刻(通常您感觉不到),然后返回所找到下一批的 500 个记录。这个过程将一直持续到返回了 所有 记录为止。而且,不管您有 1,001 或 100,001 个帐户;最终,您将得到所有帐户的记录。这样 您就可以消除 1,000 个记录的限制了。

注意:更改对 Page Size 所设的值会有什么不同吗?在公认的小网络中测试时,我们并未发现有什么不同。根据您的网络流量和域控制器所承受的压力,您会发现到底是使用较大还是较小的页大小更好一些。(不过,最大值是 1000。)具体数值得由您自己决定。

返回页首返回页首

Searchscope

假如您经常丢东西的话,那么就一定会制定出一套便捷的搜寻计策。而其中最重要的计策之一就是首先确定从何处开始搜索。例如,假设您弄丢了电视遥控器。要是您从未去过北达科他州,那么立即坐飞机去那里寻找遥控器,可能就算不上是一条上策了。(虽然飞往塔希提岛 那里 去寻找,可能会有点价值。)相反,您会确定遥控器最可能出现的地方,然后从那里开始搜寻。

假设您白天还看到了遥控器。此后,您就没有迈出过家门。这样就可以断定遥控器就在家里的某个地方;因此,您就会将寻找范围限制在家中。(当然,除非您有一个十几岁的小儿子。那样的话,就可以大致断定是 拿了遥控器。假如您没有一个十几岁的小儿子的话,那么最好在冰箱附近找一找。)

换句话说,设置搜索范围就相当于确定搜索的起点和限制条件。猜一猜会发生什么呢?您在寻找失踪的遥控器时所用的策略,与用于在 Active Directory 中搜索对象的策略是完全相同的。那么,让我们来具体了解一下可供脚本编写者选择的三种搜索范围。

基本搜索

使用这类搜索,您将仅搜索所谓的“基本”对象(即开始对其进行搜索的 Active Directory 容器);将 对子容器进行搜索。譬如,您在 Active Directory 根中开始搜索,那么将仅对该根进行搜索;而不会对任何组织单位 (OU) 或其它容器进行搜索。当您要从单个 OU 中提取信息(比如:Finance OU 中所有用户帐户的列表)时,基本搜索很有用。

昏了吗?可别。看一看下面的示意图;在域根中开始执行基本搜索时,将仅对域容器(如黄色部分所示)进行搜索。这是因为,执行基本搜索时,搜索的对象仅限于指定的容器(此处为域根)。

Base Search


若要查看大图,请单击此处

若要指定一个基本搜索,请执行两个步骤。首先,创建一个名为 ADS_SCOPE_BASE 的常量,并将其值设为 0:

Const ADS_SCOPE_BASE = 0

然后在您的脚本中,将 Searchscope 属性设为该常量的值:

objCommand.Properties("Searchscope") = ADS_SCOPE_BASE

一级搜索

这是一种奇怪的搜索,但有时您会发现它很有用。假设您有一个名为 Finance 的 OU,其下还有两个子 OU:Accounting 和 Marketing。假定您从 Finance OU 执行一级搜索。这时,将仅对 Accounting 和 Marketing OU 进行搜索;而 不会 对父 OU 进行搜索。另外,假定 Accounting OU 本身有两个子 OU:Receivables 和 Payables。那么,也不会对这两个次子 OU 进行搜索;因此,这种搜索称为 一级 搜索。

这里还有一幅示意图,显示了在域根中执行一级搜索时,将对其执行搜索的容器。正如您所看到的,并未对域根以及任何位于顶级 OU 下的容器进行搜索。

One-Level Search


若要查看大图,请单击此处

若要指定一个一级搜索,请执行两个步骤。首先,创建一个名为 ADS_SCOPE_ONELEVEL 的常量,并将其值设为 1:

Const ADS_SCOPE_ONELEVEL = 1

然后在您的脚本中,将 Searchscope 属性设为该常量的值:

objCommand.Properties("Searchscope") = ADS_SCOPE_ONELEVEL

子树搜索

子树搜索是最常用的搜索范围;实际上,假如您没有指定搜索范围,它就将是默认使用的范围。在子树搜索中,将对整个子树进行搜索:包括基本容器(如:Finance OU)、子容器(如:Accounting 和 Marketing OU)及其包含的任何容器。这意味着呢?首先,假如您要对整个 Active Directory 进行搜索,那么只需指定在 Active Directory 中执行子树搜索;这种搜索将先对根进行搜索,然后再继续对根中的所有容器进行搜索。而由于所有容器最终都保存在根中,这就意味着您将对整个 Active Directory 进行搜索。

我们也可以通过图示来说明。这里有一幅示意图,显示了在域根中执行子树搜索时,将对其执行搜索的容器(结果搜索了域中的 所有 容器)。

Subtree Search


若要查看大图,请单击此处

若要指定一个子树搜索,请执行两个步骤。 首先,创建一个名为 ADS_SCOPE_SUBTREE 的常量,并将其值设为 2:

Const ADS_SCOPE_SUBTREE = 2

然后在您的脚本中,将 Searchscope 属性设为该常量的值:

objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE
返回页首返回页首

Sort On

Sort On 属性允许您指定返回数据的排序方式。想按用户的姓氏 (SN) 来对数据进行排序吗?请使用以下代码:

objCommand.Properties("Sort On") = "SN"

想按用户所属的部门来对记录进行排序吗?请使用以下代码:

objCommand.Properties("Sort On") = "Department"

请注意,您只能对单个值进行排序:不能同时按部门和姓氏进行排序。

返回页首返回页首

创建一个查询

实际上,有两种不同的方法可用于编写 Active Directory 查询:可以使用 LDAP 语法或 SQL 语法。通常,这两种方法的作用相同:几乎所有可通过 LDAP 语法执行的搜索,也可使用 SQL 语法来实现,反之亦然。我们将主要介绍相关的 SQL 语法,因为我们发现这种方法更易用,而且普通的系统管理员对它也更熟悉一些。(若要了解有关 LDAP 语法的详细信息,请参考有关 Active Directory 搜索的“脚本专家”网络广播 。)

而剩下的人,让我们来回顾一下用于在域中定位所有用户帐户的查询:

objCommand.CommandText = _ "SELECT Name FROM 'LDAP://dc=fabrikam,dc=com' WHERE objectCategory='user'"

注意:我们现在应该提醒您一下,Active Directory 搜索都是只读的;因此,大致上仅限您使用 SELECT 查询。熟悉数据库脚本编写的人可能会感到疑惑:“有没有办法使用 DELETE 查询来删除记录,或者使用 UPDATE 查询来修改记录呢?”这个问题的答案很简单:没有。不过,下个月我们将向您介绍一种方法,让您可以通过结合 Active Directory 查询和某种标准的 ADSI 脚本,完成诸如将 Accounting 部门所有成员的 Department 属性更改为 Finance 部门等操作。

这段代码大体上就像是一个典型的 SQL 查询。我们根据指定条件 (WHERE objectCategory = ‘user’),从某个地方 (‘LDAP://dc=fabrikam,dc=com’) 选择了某个对象 (Name)。非常简单。

实际上,在编写 SQL 查询来对 Active Directory 进行搜索时(假设您不大会编写 SQL 查询),您只需知道几件事情。首先,您必须指定想要让查询返回的所有属性的名称。假如您已经很熟悉 SQL,或者编写过很多 WMI 查询,那么您一定常用类似如下的查询,作为返回某个对象所有属性的快捷方法:

SELECT * FROM ...

而在查询 Active Directory 时,这种方法却行不通;SELECT * FROM 是一个有效的查询,但它不会返回某个对象的所有属性,而仅仅是其中的 ADsPath 属性。假如您想返回用户 CN(常用名)、部门或职称,那该怎么办呢?这时,您必须在查询中包含所有这三种属性,具体如下:

objCommand.CommandText = _ "SELECT CN, Department, Title FROM 'LDAP://dc=fabrikam,dc=com' " & _ "WHERE objectCategory='user'"

注意: 假如您想返回几百个用户帐户的属性,又该怎么办呢?虽然您 可以 编写一个 SQL 查询来返回所有这些信息,但我们并不推荐这么做。相反,仅返回 ADsPath,使用 ADsPath 连接到每个对象,然后分别返回每个对象的所有信息,这种方法要更好一些。我们将在这套系列文章的第 2 部分讨论这方面的内容。

其次,必须使用 ADsPath,指定搜索的起始位置。通常,您会从域的根中开始执行搜索。若要执行该步骤,只需将 ADsPath 指定到域根,确保提供了以单引号注明的路径

'LDAP://dc=fabrikam,dc=com'

但是,假如您不想从域根,而要在特定的 OU 中开始执行搜索,又该如何呢?没问题;只需对相应的 OU 使用 ADsPath:

'LDAP://ou=Finance,dc=fabrikam,dc=com'

假如您想对整个 进行搜索,又该怎么办呢?在这种情况下,只需将 ADsPath 指定到全局编录:

'GC://dc=fabrikam,dc=com'

注意:提供程序的名称需区分大小写。不要键入 ldap 或 gc;结果会让您感到很失望的。

最后(但绝对不是最次要的)就是 WHERE 子句了。您需要在该子句中,指定所要搜索的对象。有一个对象将(或至少 应该)包含在您所编写的每一个 WHERE 子句中,它就是 objectCategory。ObjectCategory 用于指示我们所要搜索的对象类型。例如,在以下查询中,我们仅搜寻用户对象:

objCommand.CommandText = _ "SELECT CN, Department, Title FROM 'LDAP://dc=fabrikam,dc=com' " & _ "WHERE objectCategory='user'"

而在以下查询中,我们将搜寻用户对象 计算机对象:

objCommand.CommandText = _ "SELECT CN, Department, Title FROM 'LDAP://dc=fabrikam,dc=com' " & _ "WHERE objectCategory='user' OR objectCategory='computer'"

请注意,您会经常看到搜索脚本指定的是 objectClass,而不是 objectCategory。ObjectClass 和 objectCategory 之间的差异源自于 Active Directory 的继承因素。今天,我们将不详细讨论有关继承的内容;我们只提一下:大多数 Active Directory 类都派生自其它的类。譬如,下图(根据我们的叙述目的进行了一些简化)显示了 Active Directory 中的一个类继承树。该树的顶部是 Top 类。接下来是 Person 类。Person 类包含 Top 类的所有属性,外加 一些不属于 Top 类的属性。Person 类的下面是 Organizational Person 类;正如您可能预见的,Organizational Person 同时包含 Top 类和 Person 类中的所有属性,外加 一些自身的特殊属性。

objCategory vs. objectClass


为什么这对我们很重要?在搜索 objectClass 时,您将得到在继承树中找到的所有对象;因此,搜索 objectClass='user' 将返回用户和组织成员,以及从计算机继承的一切对象。相反,搜索 objectCategory='user' 返回计算机对象。这就是 为什么对我们来说很重要的原因。

返回页首返回页首

执行查询

只要完成了查询,您要做的就是调用 Execute 方法,启动数据检索过程:

Set objRecordSet = objCommand.Execute

正如您所看到的,只要您指定了所有属性和参数,搜索本身就很简单了。

返回页首返回页首

循环返回的记录集

在执行了查询后,您将收到以记录集返回的数据。若要处理这些数据,只需循环记录集中的记录。要实现 这个 步骤的话,请使用 MoveFirst 方法,确保由第一个记录开始执行循环。(老实说,我们不能确定您 必须 这么做,但为了保险起见,我们推荐您使用这种方法。)执行 Do Until 循环,直至到达记录集的末尾 (EOF),并在循环过程中处理每个记录。譬如,以下代码仅传回记录集中每个项目的 Name 属性的值:

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

在这里,请记住两点。其一,您所做的就是传回 Name 字段的值。假如您要传回 Title 字段的值(假设您在查询中包含了 Title),又该怎么办呢?没问题:

Wscript.Echo objRecordSet.Fields("Title").Value

其二,请注意这行重要的代码:

objRecordSet.MoveNext

这行代码用于告诉脚本转移到记录集中的下一个记录。如果您忘了包含该代码(任何脚本专家都未曾用过),您的脚本将永远仅传回记录集中第一个记录的值。这是因为,您必须明确指明让脚本移动到下一个记录。

返回页首返回页首

敬请关注令人兴奋的下半部分专栏文章

阅读了这套系列文章的下半部分后,您就可以开始编写脚本来对 Active Directory 进行搜索了。介绍完相关的基础知识后,下个月我们将进一步探讨您所能搜索的对象。在此之前,您可能想参考有关在 Active Directory 中执行搜索的“脚本专家”网络广播 ,并查看脚本中心资源库中的示例脚本。 而我们这些脚本专家则决定立即前往南美洲的丛林。为什么呢?噢,没什么原因,真的…。


返回页首返回页首