在 ASP.NET 上若要使用 Forms Authentication 和 Active Directory 連接時,我們通常都會直接使用 System.DirectoryServices (System.DirectoryServices.dll) 命名空間中的 DirectoryEntry 以及 DirectorySearcher 來做,不過這樣會有一個問題,尤其是你的應用程式要記錄在 AD 中的使用者資訊到資料庫時,就會有要用哪種名稱的問題。因為在預設的 Windows 登入行為中,允許 Domain\User (NT 格式) 和 UPN 兩種名稱,可能有人說在資料表中開兩個欄位就好啦,不過在儲存時,使用者通常只會給一種格式,那要如何知道另外一種格式 (例如 CORPDOMAIN\Steve 的 UPN 名稱是什麼,或是 steve@mycompany.com 的 NT 格式名稱又是什麼)?這個功能在 .NET 內建的 API 中沒有提供,所以只能針對 ADSI 本身提供的 API 來做。
首先,提供名稱對應的 API 是 DsCrackNames(),必須要傳入 DS 的 Handle,設定來源名稱的格式以及要對應的新格式,傳入要對應的名稱陣列以及大小,最後再取出對應的結果。
DWORD DsCrackNames(
__in HANDLE hDS,
__in DS_NAME_FLAGS flags,
__in DS_NAME_FORMAT formatOffered,
__in DS_NAME_FORMAT formatDesired,
__in DWORD cNames,
__in LPCTSTR* rpNames,
__out PDS_NAME_RESULT* ppResult
);
換成 .NET Framework 宣告就是:
[DllImport("ntdsapi.dll", CharSet = CharSet.Auto)]
static public extern uint DsCrackNames(
IntPtr hDS,
DS_NAME_FLAGS flags,
DS_NAME_FORMAT formatOffered,
DS_NAME_FORMAT formatDesired,
uint cNames,
string[] rpNames,
out IntPtr ppResult // PDS_NAME_RESULT
);
不過因為還要處理回傳的結果 (ppResult,傳回來的是 DS_NAME_RESULT),所以還要額外宣告一些結構與相關函式 (像是 DsBind(), DsUnBind(), DsFreeNameResult() 等),其宣告如下:
public const long NO_ERROR = 0;
[DllImport("activeds.dll", CharSet = CharSet.Unicode)]
public static extern int ADsEncodeBinaryData(byte[] data, int length, ref IntPtr result);
[DllImport("activeds.dll")]
public static extern bool FreeADsMem(IntPtr pVoid);
[DllImport("ntdsapi.dll", CharSet = CharSet.Auto)]
static public extern uint DsCrackNames(
IntPtr hDS,
DS_NAME_FLAGS flags,
DS_NAME_FORMAT formatOffered,
DS_NAME_FORMAT formatDesired,
uint cNames,
string[] rpNames,
out IntPtr ppResult // PDS_NAME_RESULT
);
[DllImport("ntdsapi.dll", CharSet = CharSet.Auto)]
static public extern void DsFreeNameResult(IntPtr pResult /* DS_NAME_RESULT* */);
public enum DS_NAME_ERROR
{
DS_NAME_NO_ERROR = 0,
// Generic processing error.
DS_NAME_ERROR_RESOLVING = 1,
// Couldn't find the name at all - or perhaps caller doesn't have
// rights to see it.
DS_NAME_ERROR_NOT_FOUND = 2,
// Input name mapped to more than one output name.
DS_NAME_ERROR_NOT_UNIQUE = 3,
// Input name found, but not the associated output format.
// Can happen if object doesn't have all the required attributes.
DS_NAME_ERROR_NO_MAPPING = 4,
// Unable to resolve entire name, but was able to determine which
// domain object resides in. Thus DS_NAME_RESULT_ITEM?.pDomain
// is valid on return.
DS_NAME_ERROR_DOMAIN_ONLY = 5,
// Unable to perform a purely syntactical mapping at the client
// without going out on the wire.
DS_NAME_ERROR_NO_SYNTACTICAL_MAPPING = 6,
// The name is from an external trusted forest.
DS_NAME_ERROR_TRUST_REFERRAL = 7
}
[Flags]
public enum DS_NAME_FLAGS
{
DS_NAME_NO_FLAGS = 0x0,
// Perform a syntactical mapping at the client (if possible) without
// going out on the wire. Returns DS_NAME_ERROR_NO_SYNTACTICAL_MAPPING
// if a purely syntactical mapping is not possible.
DS_NAME_FLAG_SYNTACTICAL_ONLY = 0x1,
// Force a trip to the DC for evaluation, even if this could be
// locally cracked syntactically.
DS_NAME_FLAG_EVAL_AT_DC = 0x2,
// The call fails if the DC is not a GC
DS_NAME_FLAG_GCVERIFY = 0x4,
// Enable cross forest trust referral
DS_NAME_FLAG_TRUST_REFERRAL = 0x8
}
public enum DS_NAME_FORMAT
{
// unknown name type
DS_UNKNOWN_NAME = 0,
// eg: CN=User Name,OU=Users,DC=Example,DC=Microsoft,DC=Com
DS_FQDN_1779_NAME = 1,
// eg: Example\UserN
// Domain-only version includes trailing '\\'.
DS_NT4_ACCOUNT_NAME = 2,
// Probably "User Name" but could be something else. I.e. The
// display name is not necessarily the defining RDN.
DS_DISPLAY_NAME = 3,
// obsolete - see #define later
// DS_DOMAIN_SIMPLE_NAME = 4,
// obsolete - see #define later
// DS_ENTERPRISE_SIMPLE_NAME = 5,
// String-ized GUID as returned by IIDFromString().
// eg: {4fa050f0-f561-11cf-bdd9-00aa003a77b6}
DS_UNIQUE_ID_NAME = 6,
// eg: example.microsoft.com/software/user name
// Domain-only version includes trailing '/'.
DS_CANONICAL_NAME = 7,
// eg: usern@example.microsoft.com
DS_USER_PRINCIPAL_NAME = 8,
// Same as DS_CANONICAL_NAME except that rightmost '/' is
// replaced with '\n' - even in domain-only case.
// eg: example.microsoft.com/software\nuser name
DS_CANONICAL_NAME_EX = 9,
// eg: www/www.microsoft.com@example.com - generalized service principal
// names.
DS_SERVICE_PRINCIPAL_NAME = 10,
// This is the string representation of a SID. Invalid for formatDesired.
// See sddl.h for SID binary <--> text conversion routines.
// eg: S-1-5-21-397955417-626881126-188441444-501
DS_SID_OR_SID_HISTORY_NAME = 11,
// Pseudo-name format so GetUserNameEx can return the DNS domain name to
// a caller. This level is not supported by the DS APIs.
DS_DNS_DOMAIN_NAME = 12
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public struct DS_NAME_RESULT_ITEM
{
public DS_NAME_ERROR status;
public string pDomain;
public string pName;
}
[DllImport("ntdsapi.dll", CharSet = CharSet.Auto)]
static public extern uint DsBind(
string DomainControllerName, // in, optional
string DnsDomainName, // in, optional
out IntPtr phDS);
[DllImport("ntdsapi.dll", CharSet = CharSet.Auto)]
static public extern uint DsUnBind(ref IntPtr phDS);
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public struct DS_NAME_RESULT
{
public uint cItems;
public IntPtr rItems; // PDS_NAME_RESULT_ITEM
}
| |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
|
(宣告的來源為 PInvoke.net:http://www.pinvoke.net/default.aspx/ntdsapi/DsCrackNames.html)
然後在程式中就可以這樣用:
public struct DsCrackNamesResult
{
public DsNameErrorEnum Status;
public string Domain;
public string Name;
}
public enum DsNameErrorEnum
{
DS_NAME_NO_ERROR = 0,
DS_NAME_ERROR_RESOLVING = 1,
DS_NAME_ERROR_NOT_FOUND = 2,
DS_NAME_ERROR_NOT_UNIQUE = 3,
DS_NAME_ERROR_NO_MAPPING = 4,
DS_NAME_ERROR_DOMAIN_ONLY = 5,
DS_NAME_ERROR_NO_SYNTACTICAL_MAPPING = 6,
DS_NAME_ERROR_TRUST_REFERRAL = 7
}
public static DsCrackNamesResult[] ResolveNTFormatNamesToUPNs(string[] NTFormatNames)
{
IntPtr hDS;
IntPtr hDsResult;
NativeMethods.DS_NAME_RESULT_ITEM[] nameResultItems = null;
DsCrackNamesResult[] results = null;
long ret = 0;
// bind to default GC.
ret = NativeMethods.DsBind(null, null, out hDS);
if (ret != NativeMethods.NO_ERROR)
throw new InvalidOperationException("Cannot_Bind_To_Default_Global_Catalog_Server");
// execute to crack names.
ret = 0;
ret = NativeMethods.DsCrackNames(
hDS,
NativeMethods.DS_NAME_FLAGS.DS_NAME_NO_FLAGS,
NativeMethods.DS_NAME_FORMAT.DS_NT4_ACCOUNT_NAME,
NativeMethods.DS_NAME_FORMAT.DS_USER_PRINCIPAL_NAME,
(uint)((NTFormatNames == null) ? 0 : NTFormatNames.Length),
NTFormatNames,
out hDsResult);
if (ret != NativeMethods.NO_ERROR)
{
NativeMethods.DsUnBind(ref hDS);
throw new InvalidOperationException("Cannot_Resolve_Names_With_Error:" + ret.ToString());
}
// handling results.
try
{
NativeMethods.DS_NAME_RESULT nameResult = new NativeMethods.DS_NAME_RESULT();
nameResult.cItems = (uint)Marshal.ReadInt32(hDsResult);
nameResult.rItems = (IntPtr)Marshal.ReadInt32(hDsResult, Marshal.SizeOf(nameResult.cItems));
IntPtr pItem = nameResult.rItems;
nameResultItems = new NativeMethods.DS_NAME_RESULT_ITEM[nameResult.cItems];
results = new DsCrackNamesResult[nameResult.cItems];
for (int i = 0; i < (int)nameResult.cItems; i++)
{
nameResultItems[i] =
((NativeMethods.DS_NAME_RESULT_ITEM)Marshal.PtrToStructure(pItem, typeof(NativeMethods.DS_NAME_RESULT_ITEM)));
pItem = (IntPtr)((int)pItem + Marshal.SizeOf(nameResultItems[i]));
}
}
catch (Exception e)
{
NativeMethods.DsFreeNameResult(hDsResult);
throw new InvalidOperationException("Handling_Result_Failed:" + e.Message);
}
finally
{
NativeMethods.DsFreeNameResult(hDsResult);
}
// free bind of DS.
NativeMethods.DsUnBind(ref hDS);
// create result set.
for (int i = 0; i < nameResultItems.Length; i++)
{
results[i].Status = (DsNameErrorEnum)nameResultItems[i].status;
results[i].Domain = nameResultItems[i].pDomain;
results[i].Name = nameResultItems[i].pName;
}
nameResultItems = null;
return results;
}
| |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
|
最後,由程式中呼叫 ResolveNTNameToUPNs(),傳入 NT 格式的登入名稱,就可以得到在 AD 中對應的 UPN 名稱了。
讀者可參考 MSDN,舉一反三,設計出像 ResolveUPNsToNTNames() 的工具函式,來解決 NT Names 和 AD UPNs 的對應問題。

