AD简介
Active Directory(以下简称AD)可以认为是一个大的层次结构数据库,集中存储的内容必须遵循AD当前所定义的Schema。我觉得AD中最重要的内容就是Schema,然后是ADSI。
Schema定义了数据存储的格式。包括类(classSchema),分为抽象类(Abstract)、附属类(Auxiliary)
和结构类(Structure)三种;属性(attributeSchema),分为单值和多值属性;以及类和属性之间的关系,分为可选属性和必要属性。AD中的Schema相当于全局的Catalog,在整个AD的forest中是全局唯一的,任何的修改都会被同步。所以有关Schema的修改需要有Schema Administrators的权限。而且Schema的内容只能增加,不能删除,过程是不可逆的,最多只能禁止一些属性或者类,而且还有诸多限制,详见文档。
在AD中存储的数据被当作一个一个的对象,每一个对象是一个classSchema的实例。都有一个唯一的路径访问。AD中对象的路径由所支持的Provider决定,Windows 2000默认的Provider有四个,安装IIS的话会增加一个IIS的Provider,对IIS进行管理。通常使用比较多的是LDAP,可以通过这个Provider针对域用户以及其他扩展信息进行访问和管理。WinNT是针对NT4的帐号管理,估计是向下兼容。另外两个是对Netware的DN的访问,对我们而言不太会用到,不赘述了。
ADSI(Active Directory Service Interface)是用来对AD中存储数据的访问接口,我认为他是一个架空的框架,与具体的数据访问无关,只是给上层应用提供一个统一的接口。实际工作的就是Provider,应当是Provider访问数据,然后包装成ADSI要求的形式,这些工作对用用户来说完全透明。
另一个比较爽的地方是ADSI提供了比较好的扩展的方式,你能够非常容易的增加新的类,或者给已有的类增添新的方法。
需要解释一下,虽然ADSI和AD经常一起出现,但是AD和ADSI是两码事,ADSI不仅仅能访问AD,还可以访问IIS以及Netware所存储的数据。你只要按照要求提供相应的Provider,ADSI可以干任何事情。
AD编程
AD的编程到目前来看涵盖的内容非常多,从最粗糙的使用ADSI对AD中已有的数据访问,到比较高级的扩展AD。我认为扩展AD才是这部分编程中比较重要的内容,因为针对任何具体的应用多绘有自己特定的信息,而应用AD主要是为了利用MS提供的安全性以及分布式存储,如果将这两方面结合起来,就需要为了自身的应用对AD进行扩展。
如果需要为已有的类(接口类)添加方法,那么需要编写AdsExtension类;如果需要在AD中存储扩展信息,就需要修改Schema,增加新的类(classSchema)或者属性(attributeSchema);更进一步的话,完全可以自己实现一个Provider,实现自己的查询和数据存储的方式,这一部分内容已经不仅仅局限于AD了。
- 通过ADSI访问AD
通过ADSI访问AD比较简单,实际上是应用WinNT和LDAP这两个Provider。除了通用接口IAds、IAdsContainer、IAdsDirectorySearch等以外,Windows默认提供了一些接口类如IAdsUser、IAdsGroup,当安装了一些基于AD的服务之后,又会增加一些专有接口,如安装Exchange 2000之后,会出现Person,同时扩展了User。
在使用AD的过程中比较重要的一个问题是访问者的权限,如果使用GetObject的方式操作,那么应用程序是以当前登录用户的权限访问AD,很多的写操作是被拒绝的。使用IAdsOpenDSObject->OpenDSObject可以指定操作对象的用户,当然这就需要实现得到指定用户的口令。
第二个需要注意的地方就是AD的Path,有两个最常用的前缀(姑且这么叫吧):CN(Common Name)和DC(Domain Controller)。另外对于LDAP从左到右范围增大,而WinNT从左到右范围是减小,比如访问我的帐号,路径分别为LDAP://CN=mittermeyer,CN=Users,DC=cn,DC=corp,DC=company,DC=com,WinNT://cn.corp.company.com/Users/mittermeyer。另外据说AD是区别大小写的,我看下来他有一种数据类型是区别大小写的字符串,但是路径这里好像无所谓,CN=和cn=都行。
第三个需要注意的地方就是查询语法。查询的话,一共提供了两种方式,一个是IAdsSearchDirectory接口,IAdsDirectorySearch完成查询过程和处理查询结果的全部工作,我个人认为这种方式不太适合VB的程序;另一种方式是使用ADO。AD针对ADO有一个Provider(ADsDSOObject),使用这种方式返回一个ADO.Recordset,处理结果和关系型数据库的查询完全一致,这种方式VB比较容易上手。ADO的方式查询可以使用SQL的语法,也可以使用LDAP的语法;而IAdsDirectorySearch只能使用到的是LDAP的语法。
这一部分比较有趣的是扩展接口,就是写一个接口作为已经存在的接口类的扩展。扩展接口本身只是继承IDispatch就可以了,但是如果需要支持后期绑定,那么还需要实现IAdsExtension所要求的一系列方法,看上去是模板,就是一个套路,所以这部分工作还是比较简单的。
关键是把自己编写的接口和已经存在的接口类关联,嘿嘿!也很简单,只要在注册表里加一项就可以了。(MS想到的方法总是比较容易理解,不过在整体框架那里还是花了很多心思的,所以架子有了扩展就容易了。)例如:以及下就是Exchange加的对User地扩展,它表明Exchange针对User有一个扩展的CoClass--Mailbox,其中包含了两个接口IMailRecipient和IMailboxStore。
关键是把自己编写的接口和已经存在的接口类关联,嘿嘿!也很简单,只要在注册表里加一项就可以了。(MS想到的方法总是比较容易理解,不过在整体框架那里还是花了很多心思的,所以架子有了扩展就容易了。)例如:以及下就是Exchange加的对User地扩展,它表明Exchange针对User有一个扩展的CoClass--Mailbox,其中包含了两个接口IMailRecipient和IMailboxStore。
[HKEY_LOCAL_MACHINE/SOFTWARE/Microsoft/ADs/Providers/LDAP/Extensions/User]
[HKEY_LOCAL_MACHINE/SOFTWARE/Microsoft/ADs/Providers/LDAP/Extensions/User/{25150F21-5734-11D2-A593-00C04F990D8A}]
"Interfaces"=hex(7):7b,00,32,00,35,00,31,00,35,00,30,00,46,00,34,00,31,00,2d,/
00,35,00,37,00,33,00,34,00,2d,00,31,00,31,00,44,00,32,00,2d,00,41,00,35,00,/
39,00,33,00,2d,00,30,00,30,00,43,00,30,00,34,00,46,00,39,00,39,00,30,00,44,/
00,38,00,41,00,7d,00,00,00,7b,00,32,00,35,00,31,00,35,00,30,00,46,00,34,00,/
30,00,2d,00,35,00,37,00,33,00,34,00,2d,00,31,00,31,00,44,00,32,00,2d,00,41,/
00,35,00,39,00,33,00,2d,00,30,00,30,00,43,00,30,00,34,00,46,00,39,00,39,00,/
30,00,44,00,38,00,41,00,7d,00,00,00,00,00
"Interfaces"=hex(7):7b,00,32,00,35,00,31,00,35,00,30,00,46,00,34,00,31,00,2d,/
00,35,00,37,00,33,00,34,00,2d,00,31,00,31,00,44,00,32,00,2d,00,41,00,35,00,/
39,00,33,00,2d,00,30,00,30,00,43,00,30,00,34,00,46,00,39,00,39,00,30,00,44,/
00,38,00,41,00,7d,00,00,00,7b,00,32,00,35,00,31,00,35,00,30,00,46,00,34,00,/
30,00,2d,00,35,00,37,00,33,00,34,00,2d,00,31,00,31,00,44,00,32,00,2d,00,41,/
00,35,00,39,00,33,00,2d,00,30,00,30,00,43,00,30,00,34,00,46,00,39,00,39,00,/
30,00,44,00,38,00,41,00,7d,00,00,00,00,00
- 扩展Schema
首先,我们知道attributeSchema和classSchema都存储在同一个层次下,我们可以通过这样的路径访问:LDAP://computername/Schema 之下(另外一种访问路径是CN=Schema,CN=Configuration,<DC=forestroot>),所以在这个Container下我们可以枚举到整个AD中所有的attributeSchema和classSchema。但是我发觉有一个有趣的现象,如果不是以域用户的身份去访问,那么不能得到全集,获得的是Abstract Class和相关的属性,无法得到Structure Class,例如:User。
其次,针对attributeSchema和classSchema都有一些特性:
OID(governsID),LDAP所需要的对象的唯一表示符,这是一个字符串,但是不同于GUID根据本机信息生成,而是逐级分配的属性结构,最上层由ISO分配,逐级授权,所以很麻烦。MS提供了一个工具OIDGEN.exe,随Windows 2000的Resource Kit发布,我不知道即使是用这样的工具生成的新ID能否运行在实际的扩展系统中,还是必须通过MS的认证。
schemaIDGUID,用于访问控制目录中控制访问这个类的对象。通过这个ID而不是名称来访问类的对象实例。GUID还是非常好处理的,可以通过Windows自身的API获得。
其他的就是各类名称(cn,LDAPDisplayName,adminDisplayName),在不同的工具或者场合显示区别类或者属性,这些名字只要保证全局唯一即可。此外classSchema和attributeSchema各有一些特定的必备属性。
OID(governsID),LDAP所需要的对象的唯一表示符,这是一个字符串,但是不同于GUID根据本机信息生成,而是逐级分配的属性结构,最上层由ISO分配,逐级授权,所以很麻烦。MS提供了一个工具OIDGEN.exe,随Windows 2000的Resource Kit发布,我不知道即使是用这样的工具生成的新ID能否运行在实际的扩展系统中,还是必须通过MS的认证。
schemaIDGUID,用于访问控制目录中控制访问这个类的对象。通过这个ID而不是名称来访问类的对象实例。GUID还是非常好处理的,可以通过Windows自身的API获得。
其他的就是各类名称(cn,LDAPDisplayName,adminDisplayName),在不同的工具或者场合显示区别类或者属性,这些名字只要保证全局唯一即可。此外classSchema和attributeSchema各有一些特定的必备属性。
扩展Schema包括以下几部分的工作:新增/禁止attributeSchema,新增/禁止classSchema,修改Property与classSchema的关系。
新增attributeS chema和classSchema,通过IAdsContainer.Create,在Schema存储的路径下新建子节点,然后给必要的属性赋值,最后提交即可。
禁止attributeSchema和classSchema,可以通过“废弃”的方式禁止一个现存的类或者属性。即获得这个classSchema或者attributeSchema,将他的isDefunct属性置为True即可;反之只要将isDefunct属性置为False即可恢复。当然这个操作也存在一系列的限制,例如:禁止一个属性,那么将阻止创建所有所有必须包含该属性的类的实例。
修改Property与classSchema的关系,因为决定每一个classSchema中包含哪些attributeSchema,其实是指定classSchema的“mustContain”和“mayContain”,这两个多值属性(字符串数组)分别表示表示所包含的必要属性和可选属性。反过来,可以通过IAdsClass.MandantoryProperties和IAdsClass.OptionalProperties读取。
新增attributeS chema和classSchema,通过IAdsContainer.Create,在Schema存储的路径下新建子节点,然后给必要的属性赋值,最后提交即可。
禁止attributeSchema和classSchema,可以通过“废弃”的方式禁止一个现存的类或者属性。即获得这个classSchema或者attributeSchema,将他的isDefunct属性置为True即可;反之只要将isDefunct属性置为False即可恢复。当然这个操作也存在一系列的限制,例如:禁止一个属性,那么将阻止创建所有所有必须包含该属性的类的实例。
修改Property与classSchema的关系,因为决定每一个classSchema中包含哪些attributeSchema,其实是指定classSchema的“mustContain”和“mayContain”,这两个多值属性(字符串数组)分别表示表示所包含的必要属性和可选属性。反过来,可以通过IAdsClass.MandantoryProperties和IAdsClass.OptionalProperties读取。
- 实现Provider
暂时没有研究。
实例
- 枚举对象。下面这个例子枚举了所有通过RC的接口添加到AD中的用户组和帐号。此例中IAdsContainer.Filter为一个需要筛选的类名数组,如果为空,则表示返回所有类型的对象。
Public Function EnumGroups() As VBA.Collection
Dim adDomain As IADsContainer
Dim adGroup As IADsGroup
Dim nResult As VBA.Collection
If m_sAdmin <> vbNullString Then
Set adDomain = m_adRoot.OpenDSObject("LDAP://" & m_sExchServer & "/CN=Users," & m_sDomain, _
m_sAdmin, m_sAdminPwd, ADS_SECURE_AUTHENTICATION)
Else
Set adDomain = GetObject("LDAP://CN=Users," & m_sDomain)
End If
If adDomain Is Nothing Then Exit Function
Set nResult = New VBA.Collection
adDomain.Filter = Array("group","user"”)
On Error Resume Next
Dim sName As String
Dim sType As String
For Each adGroup In adDomain
sName = Right(adGroup.Name, Len(adGroup.Name) - 3) ' filter "CN="
Debug.Print sName
sType = adGroup.Get(cPropCustomType)
If Err.Number = 0 And sType = cTypeRC Then
nResult.Add sName, sName
End If
Err.Clear
Next
Set EnumGroups = nResult
End Function
Dim adDomain As IADsContainer
Dim adGroup As IADsGroup
Dim nResult As VBA.Collection
If m_sAdmin <> vbNullString Then
Set adDomain = m_adRoot.OpenDSObject("LDAP://" & m_sExchServer & "/CN=Users," & m_sDomain, _
m_sAdmin, m_sAdminPwd, ADS_SECURE_AUTHENTICATION)
Else
Set adDomain = GetObject("LDAP://CN=Users," & m_sDomain)
End If
If adDomain Is Nothing Then Exit Function
Set nResult = New VBA.Collection
adDomain.Filter = Array("group","user"”)
On Error Resume Next
Dim sName As String
Dim sType As String
For Each adGroup In adDomain
sName = Right(adGroup.Name, Len(adGroup.Name) - 3) ' filter "CN="
Debug.Print sName
sType = adGroup.Get(cPropCustomType)
If Err.Number = 0 And sType = cTypeRC Then
nResult.Add sName, sName
End If
Err.Clear
Next
Set EnumGroups = nResult
End Function
- 添加一个用户以及用户相关的邮箱,这是一个相对复杂的利用ADSI的示例,其他类似的操作就不赘述了。这里用到的就是ADSI和Exchange针对ADSI中IAdsUser对象的扩展。斜体的那一段代码颇值得回味,在VB中非常简单的一句话,背后有一套复杂的逻辑。
添加用户组和组邮箱的操作类似,不同的是组邮箱不是一个物理邮箱,而是一个邮箱列表,通过IMailRecipient.MailboxEnabled使之有效即可。
' add new user to Domain and create mailbox for it
Public Function AddAccountEx(ByVal sAccount As String, ByVal sFullName As String, ByVal sDesc As String, _
ByVal sPassword As String) As Long
Dim adDomain As IADsContainer
Dim adNewUser As IADsUser
Dim oMailStore As CDOEXM.IMailboxStore
Dim oExchServer As CExchageManager
If m_sAdmin <> vbNullString Then
Set adDomain = m_adRoot.OpenDSObject("LDAP://CN=Users," & m_sDomain, _
m_sAdmin, m_sAdminPwd, ADS_SECURE_AUTHENTICATION)
Else
Set adDomain = GetObject("LDAP://CN=Users," & m_sDomain)
End If
Public Function AddAccountEx(ByVal sAccount As String, ByVal sFullName As String, ByVal sDesc As String, _
ByVal sPassword As String) As Long
Dim adDomain As IADsContainer
Dim adNewUser As IADsUser
Dim oMailStore As CDOEXM.IMailboxStore
Dim oExchServer As CExchageManager
If m_sAdmin <> vbNullString Then
Set adDomain = m_adRoot.OpenDSObject("LDAP://CN=Users," & m_sDomain, _
m_sAdmin, m_sAdminPwd, ADS_SECURE_AUTHENTICATION)
Else
Set adDomain = GetObject("LDAP://CN=Users," & m_sDomain)
End If
' create a account
Set adNewUser = adDomain.Create("user", "cn=" & sAccount)
adNewUser.Put "sAMAccountName", sAccount
adNewUser.Put "userPrincipalName", sAccount & "@" & Domain
adNewUser.FullName = sFullName
adNewUser.Description = sDesc
adNewUser.SetInfo
Set adNewUser = adDomain.Create("user", "cn=" & sAccount)
adNewUser.Put "sAMAccountName", sAccount
adNewUser.Put "userPrincipalName", sAccount & "@" & Domain
adNewUser.FullName = sFullName
adNewUser.Description = sDesc
adNewUser.SetInfo
adNewUser.SetPassword sPassword
adNewUser.AccountDisabled = False
' create mailbox for this account
Set oExchServer = New CExchageManager
oExchServer.Connect m_sExchServer ' Get Exchange Server's Information
Set oMailStore = adNewUser
Call oMailStore.CreateMailbox("LDAP://" & m_sExchServer & "/" & oExchServer.DefaultMailboxStore)
adNewUser.SetInfo
adNewUser.AccountDisabled = False
' create mailbox for this account
Set oExchServer = New CExchageManager
oExchServer.Connect m_sExchServer ' Get Exchange Server's Information
Set oMailStore = adNewUser
Call oMailStore.CreateMailbox("LDAP://" & m_sExchServer & "/" & oExchServer.DefaultMailboxStore)
adNewUser.SetInfo
' enable immediate-logon for the user
adNewUser.Put "msExchUserAccountControl", 2
adNewUser.SetInfo
End Function
adNewUser.Put "msExchUserAccountControl", 2
adNewUser.SetInfo
End Function
- 查找。通过ADO查询比较简单,只是属性的类型,特别是一些多值属性需要额外注意。
这个例子是查询所有指定域中所有的组,其中description就是一个多值属性。
Public Function SearchGroup() As ADODB.Recordset
Dim oResult As ADODB.Recordset
Dim oCommand As ADODB.Command
Dim sConnectionStr As String
If m_sAdmin = vbNullString Then
sConnectionStr = "Provider=ADsDSOObject"
Else
sConnectionStr = "Provider=ADsDSOObject;UID=" & m_sAdmin & ";PWD=" & m_sAdminPwd
End If
Set oCommand = New ADODB.Command
With oCommand
.ActiveConnection = sConnectionStr
.CommandTimeout = 15
.CommandText = "SELECT name,description FROM 'LDAP://" & m_sDomain _
& "' WHERE objectCategory='group'"
Debug.Print .CommandText
.Properties("searchscope") = ADS_SCOPE_SUBTREE
.Properties("Chase referrals") = ADS_CHASE_REFERRALS_EXTERNAL
Set oResult = .Execute
End With
If Not oResult Is Nothing Then
Do Until oResult.EOF
Debug.Print oResult("name"), oResult("description")(0)
oResult.MoveNext
Loop
End If
End Function
Dim oResult As ADODB.Recordset
Dim oCommand As ADODB.Command
Dim sConnectionStr As String
If m_sAdmin = vbNullString Then
sConnectionStr = "Provider=ADsDSOObject"
Else
sConnectionStr = "Provider=ADsDSOObject;UID=" & m_sAdmin & ";PWD=" & m_sAdminPwd
End If
Set oCommand = New ADODB.Command
With oCommand
.ActiveConnection = sConnectionStr
.CommandTimeout = 15
.CommandText = "SELECT name,description FROM 'LDAP://" & m_sDomain _
& "' WHERE objectCategory='group'"
Debug.Print .CommandText
.Properties("searchscope") = ADS_SCOPE_SUBTREE
.Properties("Chase referrals") = ADS_CHASE_REFERRALS_EXTERNAL
Set oResult = .Execute
End With
If Not oResult Is Nothing Then
Do Until oResult.EOF
Debug.Print oResult("name"), oResult("description")(0)
oResult.MoveNext
Loop
End If
End Function
PS:很久以前写的东西,望指正。