微软 Credential Providers 详解二《关键函数》
- 2020 年 1 月 5 日
- 筆記
上一篇中我们介绍了凭据的加载和代码中函数的调用顺序,接下来我们就要了解一下一些关键函数在代码中起到什么作用了。了解清楚这些以后我们才能定制出我们自己需要功能。
CSampleProvider::SetUsageScenario
这个函数非常重要,在凭据被加载起来以后,由微软调用,我们实现这个函数里面的功能,微软调用时会给函数传递两个参数,如下所示:
HRESULT CSampleProvider::SetUsageScenario( __in CREDENTIAL_PROVIDER_USAGE_SCENARIO cpus, __in DWORD dwFlags );
其中 dwFlags
函数我们不需要关心,着重要关注的是 cpus
参数,这个参数标志了系统是锁屏、还是开机时登录而调用的凭据。如果是锁屏,那么 cpus 的值等于 CPUS_UNLOCK_WORKSTATION
,而如果是开机登陆(或切换用户)则 cpus 的值等于 CPUS_LOGON
。通过判断不同的登录类型,我们来给使用者显示不同的界面。而微软的例子中是将两中登录类型都同时创建了一个凭据,看如下代码:
// SetUsageScenario is the provider's cue that it's going to be asked for tiles // in a subsequent call. HRESULT CSampleProvider::SetUsageScenario( __in CREDENTIAL_PROVIDER_USAGE_SCENARIO cpus, __in DWORD dwFlags ) { UNREFERENCED_PARAMETER(dwFlags); HRESULT hr; // Decide which scenarios to support here. Returning E_NOTIMPL simply tells the caller // that we're not designed for that scenario. switch (cpus) { case CPUS_LOGON: case CPUS_UNLOCK_WORKSTATION: _cpus = cpus; // Create and initialize our credential. // A more advanced credprov might only enumerate tiles for the user whose owns the locked // session, since those are the only creds that wil work _pCredential = new CSampleCredential(); if (_pCredential != NULL) { hr = _pCredential->Initialize(_cpus, s_rgCredProvFieldDescriptors, s_rgFieldStatePairs); if (FAILED(hr)) { _pCredential->Release(); _pCredential = NULL; } } else { hr = E_OUTOFMEMORY; } break; case CPUS_CHANGE_PASSWORD: case CPUS_CREDUI: hr = E_NOTIMPL; break; default: hr = E_INVALIDARG; break; } return hr; }
示例中在登录和锁屏的两种情况都创建创建了 CSampleCredential
对象,这个对象就是实现凭据页面具体功能的对象。如果你需要区分登录和锁屏,那么在这里做区分创建不同的凭据对象,或者在凭据对象中判断 _cpus 的值(这个值被用作第一个参数传递到凭据对象中了)来显示不同的控件。
CSampleCredential::Initialize
注意,这里我们切换到了 CSampleCredential 类中,因为在上面介绍的方法中创建了一个 CSampleCredential 对象,并调用了该对象的 Initialize 方法,这个方法就实现了初始化凭据页面控件文字和数据的功能。同时,在调用这个方法时传递了三个参数,第一个参数就是我们刚才说的 _cpus,第二个参数描述了要创建的控件类型及控件初始化文字,第三个参数描述了创建的这些控件的初始状态,是显示、隐藏、还是具备焦点等。
// Initializes one credential with the field information passed in. // Set the value of the SFI_LARGE_TEXT field to pwzUsername. HRESULT CSampleCredential::Initialize( __in CREDENTIAL_PROVIDER_USAGE_SCENARIO cpus, __in const CREDENTIAL_PROVIDER_FIELD_DESCRIPTOR* rgcpfd, // 类型,控件的类型及默认显示文字 __in const FIELD_STATE_PAIR* rgfsp // 状态,是否显示、是否是焦点等 )
在 CSampleCredential::Initialize 函数中,遍历了这两个参数,并将这两个参数传递的内容保存到了自己类中的成员变量 _rgCredProvFieldDescriptors 和 _rgFieldStatePairs 中,这两个变量在初始化时与 CSampleProvider 初始化使用的都是相同的枚举。所以长度、成员类型、数量都是一样的。
// Initializes one credential with the field information passed in. // Set the value of the SFI_LARGE_TEXT field to pwzUsername. HRESULT CSampleCredential::Initialize( __in CREDENTIAL_PROVIDER_USAGE_SCENARIO cpus, __in const CREDENTIAL_PROVIDER_FIELD_DESCRIPTOR* rgcpfd, // 类型,控件的类型及默认显示文字 __in const FIELD_STATE_PAIR* rgfsp // 状态,是否显示、是否是焦点等 ) { HRESULT hr = S_OK; _cpus = cpus; // Copy the field descriptors for each field. This is useful if you want to vary the field // descriptors based on what Usage scenario the credential was created for. for (DWORD i = 0; SUCCEEDED(hr) && i < ARRAYSIZE(_rgCredProvFieldDescriptors); i++) { _rgFieldStatePairs[i] = rgfsp[i]; hr = FieldDescriptorCopy(rgcpfd[i], &_rgCredProvFieldDescriptors[i]); } // Initialize the String value of all the fields. if (SUCCEEDED(hr)) { hr = SHStrDupW(L"Large Text", &_rgFieldStrings[SFI_LARGE_TEXT]); } if (SUCCEEDED(hr)) { hr = SHStrDupW(L"Small Text", &_rgFieldStrings[SFI_SMALL_TEXT]); } if (SUCCEEDED(hr)) { hr = SHStrDupW(L"Edit Text", &_rgFieldStrings[SFI_EDIT_TEXT]); } if (SUCCEEDED(hr)) { hr = SHStrDupW(L"", &_rgFieldStrings[SFI_PASSWORD]); } if (SUCCEEDED(hr)) { hr = SHStrDupW(L"Submit", &_rgFieldStrings[SFI_SUBMIT_BUTTON]); } if (SUCCEEDED(hr)) { hr = SHStrDupW(L"Checkbox", &_rgFieldStrings[SFI_CHECKBOX]); } if (SUCCEEDED(hr)) { hr = SHStrDupW(L"Combobox", &_rgFieldStrings[SFI_COMBOBOX]); } if (SUCCEEDED(hr)) { hr = SHStrDupW(L"Command Link", &_rgFieldStrings[SFI_COMMAND_LINK]); } return S_OK; }
代码中我们可以看到,还有一个 _rgFieldStrings 的成员,是一个字符串指针数组变量,它是为了存储每个控件的文字信息,与 _rgCredProvFieldDescriptors 变量配合使用。给每隔字符串指针数组成员赋值后,初始化结束了。
CSampleCredential::SetSelected
在初始化完成后,我们后续会看到一系列对控件初始化的一些操作,这些函数我们不必过度的去关心他,自己下个断点跟踪一下,就知道具体的执行过程了。接下来我们要介绍的这个函数就是在控件都初始化完毕后,你可能要在控件显示之前根据业务的不同情况对控件做一些改变,比如我们希望如果当前是锁屏而调用的凭据,那么我们只显示一个密码输入框,不需要显示用户名输入框了,因为锁屏的时候你可以通过代码判断出当前会话锁屏的用户信息。而如果是登录或切换用户而调用的凭据,那么我们要显示用户名和密码的输入框。当然这只是一个简单的业务场景描述,大家根据自己业务需求的不同即可在这个函数对控件的显示和隐藏做手脚。在这个函数操作控件前,你要先判断 _pCredProvCredentialEvents 成员是否是有效的,接着调用 _pCredProvCredentialEvents 的一些方法来对控件设置状态或文字等信息。如下所示:
// LogonUI calls this function when our tile is selected (zoomed) // If you simply want fields to show/hide based on the selected state, // there's no need to do anything here - you can set that up in the // field definitions. But if you want to do something // more complicated, like change the contents of a field when the tile is // selected, you would do it here. HRESULT CSampleCredential::SetSelected(__out BOOL* pbAutoLogon) { if (NULL != _pCredProvCredentialEvents) { // 设置 Combobox 控件为显示状态 _pCredProvCredentialEvents->SetFieldState(this, SFI_COMBOBOX, CPFS_DISPLAY_IN_SELECTED_TILE); // 修改 SFI_LARGE_TEXT 控件的文字 _pCredProvCredentialEvents->SetFieldString(this, SFI_LARGE_TEXT, L"Modify Large Text"); // 设置密码输入控件具备焦点 _pCredProvCredentialEvents->SetFieldInteractiveState(this, SFI_PASSWORD, CPFIS_FOCUSED); } *pbAutoLogon = FALSE; return S_OK; }
上面代码仅作示例,可能并没有什么实际作用。大家可能也注意到了 pbAutoLogon 参数,这个参数是一个传出参数,当你将它的值设置为 TRUE 的时候,系统将会尝试自动登录。这也是一个非常重要的特性,这里自动登录后,将直接触发我们下面要介绍的函数 GetSerialization。
CSampleCredential::GetSerialization
该函数就是界面上点击登录按钮,或者上面我们提到自动登录后触发的函数,再这里,你需要将界面上输入的用户名及密码等信息传递给系统,让操作系统去执行登录的操作。如下代码所示:
// Collect the username and password into a serialized credential for the correct usage scenario // (logon/unlock is what's demonstrated in this sample). LogonUI then passes these credentials // back to the system to log on. HRESULT CSampleCredential::GetSerialization( __out CREDENTIAL_PROVIDER_GET_SERIALIZATION_RESPONSE* pcpgsr, __out CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION* pcpcs, __deref_out_opt PWSTR* ppwszOptionalStatusText, __in CREDENTIAL_PROVIDER_STATUS_ICON* pcpsiOptionalStatusIcon ) { UNREFERENCED_PARAMETER(ppwszOptionalStatusText); UNREFERENCED_PARAMETER(pcpsiOptionalStatusIcon); HRESULT hr; WCHAR wsz[MAX_COMPUTERNAME_LENGTH+1]; DWORD cch = ARRAYSIZE(wsz); if (GetComputerNameW(wsz, &cch)) { PWSTR pwzProtectedPassword; hr = ProtectIfNecessaryAndCopyPassword(_rgFieldStrings[SFI_PASSWORD], _cpus, &pwzProtectedPassword); if (SUCCEEDED(hr)) { KERB_INTERACTIVE_UNLOCK_LOGON kiul; hr = KerbInteractiveUnlockLogonInit(wsz, _rgFieldStrings[SFI_EDIT_TEXT], pwzProtectedPassword, _cpus, &kiul); if (SUCCEEDED(hr)) { // We use KERB_INTERACTIVE_UNLOCK_LOGON in both unlock and logon scenarios. It contains a // KERB_INTERACTIVE_LOGON to hold the creds plus a LUID that is filled in for us by Winlogon // as necessary. hr = KerbInteractiveUnlockLogonPack(kiul, &pcpcs->rgbSerialization, &pcpcs->cbSerialization); if (SUCCEEDED(hr)) { ULONG ulAuthPackage; hr = RetrieveNegotiateAuthPackage(&ulAuthPackage); if (SUCCEEDED(hr)) { pcpcs->ulAuthenticationPackage = ulAuthPackage; pcpcs->clsidCredentialProvider = CLSID_CSample; // At this point the credential has created the serialized credential used for logon // By setting this to CPGSR_RETURN_CREDENTIAL_FINISHED we are letting logonUI know // that we have all the information we need and it should attempt to submit the // serialized credential. *pcpgsr = CPGSR_RETURN_CREDENTIAL_FINISHED; } } } CoTaskMemFree(pwzProtectedPassword); } } else { DWORD dwErr = GetLastError(); hr = HRESULT_FROM_WIN32(dwErr); } return hr; }
函数中调用了获取计算机名的 API,并调用几个功能函数填充了登录系统所需的结构体,传递给系统进行登录。填充结构体的几个功能函数大家可以自己看一看,并不复杂。
CSampleCredential::ReportResult
ReportResult 函数是我们点击确定按钮登录系统后,操作登录反馈给我们结果的函数。你的登录成功了、密码过期了、密码错误了等信息都可以通过这个函数捕获到,配合上面的 GetSerialization 函数你可以完成一系列非常严谨的身份认证功能。ReportResult 函数有 4 个参数。
HRESULT CSampleCredential::ReportResult( __in NTSTATUS ntsStatus, // 错误代码 __in NTSTATUS ntsSubstatus, // 附加错误代码 __deref_out_opt PWSTR* ppwszOptionalStatusText, // 错误提示文字,系统会给我们写好,我们也可以自己修改 __out CREDENTIAL_PROVIDER_STATUS_ICON* pcpsiOptionalStatusIcon // 界面上显示的错误图标 );
当你在使用的时候,建议你在该函数的入口处增加一处日志,打印出 ntsStatus 和 ntsSubStatus 的值。这样在遇到一些没遇到过的错误时,可以通过日志来分析问题。示例代码中给我们提供了两种错误示例:
static const REPORT_RESULT_STATUS_INFO s_rgLogonStatusInfo[] = { { STATUS_LOGON_FAILURE, STATUS_SUCCESS, L"Incorrect password or username.", CPSI_ERROR, }, { STATUS_ACCOUNT_RESTRICTION, STATUS_ACCOUNT_DISABLED, L"The account is disabled.", CPSI_WARNING }, };
一种是登录失败的错误码,一种是用户被禁用的错误码。如果想知道更多的错误码,比如密码过期等,可以从这两个宏跟进去就能看到所有的错误码了。最终你可以根据这些错误码给出不同的提示,当然提示的字符串 ppwszOptionalStatusText 也是可以修改的,你只需要调用 SHStrDupW 函数向这个字符串填充一些你想提示的字符串即可。调用前别忘记释放这个字符串的内存哦。