微软 Credential Providers 详解一《调用原理》

在 XP 时代,微软提供了 GINA 的接口来让我们对系统登录界面进行定制,在 Win7 以上系统中,微软提供了另外一种接口,就是 Credential Providers, Credential Providers 译为凭据提供者/商。所谓凭据就是提供了一种身份认证的方式,原有的 Windows 身份认证方式就是开机的时候需要你输入密码。而通过微软提供这套接口你可以实现丰富的身份认证方式,比如指纹、USB-Key等任意修改 Windows 系统登录时的界面。比较有名的有些一些应该了解过的产品就是 ThinkPad 的指纹解锁功能,通过验证指纹来解锁计算机,当你了解了实现机制以后,硬件条件允许的情况下,你也可以做一套自己的身份认证系统。

它长什么样子?

在了解它之前我们要看看使用它到底能做什么功能。下图是微软官方提供的例子,显示了所有提供的控件,包括图片、下拉框、文本输入框、密码输入框、超链接按钮等。

如何下载示例?

微软提供了两个版本,一个是最初的 Win7 版本,另一个 v2 版本中仅增加了一种动态更新界面数据的接口也就是支持 Win8 以上系统(包含现在的 Win10),下载地址:

Win7: Samples/Win7Samples/security/credentialproviders
Win8: Samples/CredentialProvider

如何编译运行?

我们以 Win7 的 Samples 为例,代码与 v2 版本通用,Clone 下最新代码后,双击运行 Samples\Win7Samples\security\credentialproviders\CredentialProviderSamples.sln,这个项目是一个 vs2008 的工程。工程中一共有 5 个项目:

其中 SampleAllControlsCredentialProvider 项目包含了所有控件的示例,我们也将从这个项目入手来讲解。编译 SampleAllControlsCredentialProvider 项目会得到一个 SampleAllControlsCredentialProvider.dll 的动态库文件。将这个文件拷贝到虚拟机中已经安装好的 Win7 系统中的 System32 目录下(注意如果是 64 位系统请编译成 64 位版本放到 System32 目录下):

拷贝文件到该目录下以后,我们再打开项目的目录,可以看到一个 Register.reg 的注册表文件,这个注册表就是将你编译好的动态库加载到系统的注册表文件。将它也复制到虚拟机的系统中导入,导入完成后,在虚拟机中按下 CTRL+ALT+INSERT 锁屏。此时你就能看到我们文章最前面的效果了。

调用顺序

我们已经看到微软给出示例的运行效果了,作为程序开发人员你还要了解清楚它的工作原理以便我们能进一步的去完善修改它的功能与我们的业务整合。我们首先要了解的是我们自己编译的凭据文件(.dll)文件是如何被调用起来的。这些内容微软官网是有详细介绍的,不过说实话我自己文化程度不高,看着有一些吃力。所以还是自己总结了一下。

我们写好的 dll 文件的路径在写入到注册表 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Providers 下的时候,每次你进行锁屏、开机登录的时候,这个 dll 文件都会被系统的 LogonUI.exe 进程加载起来展现给最终用户,而界面上显示什么内容、显示哪些控件就是由我们自己编写的 dll 决定的了。

你可以理解这就是一个 COM 组件,我们去实现微软规定好的对应的函数,Windows 系统调用起来,在登录、登录成功等不同阶段执行不同的功能。那么我们就要来看一下都有哪些接口,以及这些接口的调用顺序是怎么样的。

接口文档:
ICredentialProvider interface
ICredentialProviderCredential interface
ICredentialProviderCredential2 interface
ICredentialProviderCredentialEvents interface
.. 还有更多不一一列举,点开网页里面就能找到。

其实在我们刚才看的例子中就已经实现了 v1 版本的所有接口了,我们只要拿这个例子来改造就可以了。首先在两个类的所有的函数入口处加上我们的调试信息,如下所示:

加上调试信息以后,我们将编译后的 dll 替换到系统中,执行一次锁屏、和解锁的过程,来看一下系统是如何调用我们编写好的接口的:

通过 dbgview 我们看到调用的函数还是比较乱的,我们稍作整理一下,你就会发现,其实是非常有逻辑的。

[932] CSample_CreateInstance                        // 最先被调用的函数,实例化一个 Provider 对象
[932] CSampleProvider::SetUsageScenario             // 设置使用场景,这个函数很关键,后面我们介绍
[932] CSampleCredential::Initialize                 // 初始化 Credential 类中的对象
[932] CSampleProvider::Advise                       // 一个拓展接口,我们一直没有用到过,可以看看文档介绍
[932] CSampleProvider::GetCredentialCount           // 自定义一些凭据的信息,比如是否自动登录等
[932] CSampleProvider::GetCredentialAt              // 一系列初始化字段的函数,暂时无需关心
[932] CSampleProvider::GetFieldDescriptorCount
[932] CSampleProvider::GetFieldDescriptorAt
[932] CSampleProvider::GetFieldDescriptorAt
[932] CSampleProvider::GetFieldDescriptorAt
[932] CSampleProvider::GetFieldDescriptorAt
[932] CSampleProvider::GetFieldDescriptorAt
[932] CSampleProvider::GetFieldDescriptorAt
[932] CSampleProvider::GetFieldDescriptorAt
[932] CSampleProvider::GetFieldDescriptorAt
[932] CSampleProvider::GetFieldDescriptorAt
[932] CSampleCredential::GetBitmapValue             // 初始化图片控件
[932] CSampleCredential::GetFieldState
[932] CSampleCredential::GetStringValue             // 初始化各种文本控件
[932] CSampleCredential::GetFieldState
[932] CSampleCredential::GetStringValue
[932] CSampleCredential::GetFieldState
[932] CSampleCredential::GetStringValue
[932] CSampleCredential::GetFieldState
[932] CSampleCredential::GetStringValue
[932] CSampleCredential::GetFieldState
[932] CSampleCredential::GetSubmitButtonValue       // 初始化提交按钮
[932] CSampleCredential::GetFieldState
[932] CSampleCredential::GetCheckboxValue           // 初始化 Checkbox 控件
[932] CSampleCredential::GetFieldState
[932] CSampleCredential::GetComboBoxValueCount      // 获取 Combobox 最大显示数据的数量,代码里设置为3
[932] CSampleCredential::GetComboBoxValueAt         // 获取第一个数据
[932] CSampleCredential::GetComboBoxValueAt         // 获取第二个数据
[932] CSampleCredential::GetComboBoxValueAt         // 获取第三个数据
[932] CSampleCredential::GetFieldState
[932] CSampleCredential::GetStringValue
[932] CSampleCredential::GetFieldState
[932] CSampleCredential::Advise
[932] CSampleCredential::SetSelected                // 可以控制控件显示状态等其他信息,并可以控制是否自动登录
[932] CSampleCredential::UnAdvise
[932] CSampleCredential::Advise
[932] CSampleCredential::SetStringValue
[932] CSampleCredential::UnAdvise
[932] CSampleCredential::Advise
[932] CSampleCredential::SetStringValue
[932] CSampleCredential::UnAdvise
[932] CSampleCredential::Advise
[932] CSampleCredential::SetStringValue
[932] CSampleCredential::UnAdvise
[932] CSampleCredential::Advise
[932] CSampleCredential::SetStringValue
[932] CSampleCredential::UnAdvise
[932] CSampleCredential::Advise
[932] CSampleCredential::SetStringValue
[932] CSampleCredential::UnAdvise
[932] CSampleCredential::Advise
[932] CSampleCredential::SetStringValue
[932] CSampleCredential::UnAdvise
[932] CSampleCredential::Advise
[932] CSampleCredential::SetStringValue
[932] CSampleCredential::UnAdvise
[932] CSampleCredential::Advise
[932] CSampleCredential::SetStringValue
[932] CSampleCredential::UnAdvise
[932] CSampleCredential::Advise
[932] CSampleCredential::SetStringValue
[932] CSampleCredential::UnAdvise
[932] CSampleCredential::Advise
[932] CSampleCredential::SetStringValue
[932] CSampleCredential::UnAdvise
[932] CSampleCredential::Advise
[932] CSampleCredential::SetStringValue
[932] CSampleCredential::UnAdvise
[932] CSampleCredential::Advise
[932] CSampleCredential::SetStringValue
[932] CSampleCredential::UnAdvise
[932] CSampleCredential::Advise
[932] CSampleCredential::SetStringValue
[932] CSampleCredential::UnAdvise
[932] CSampleCredential::Advise
[932] CSampleCredential::SetStringValue
[932] CSampleCredential::UnAdvise
[932] CSampleCredential::Advise
[932] CSampleCredential::SetStringValue
[932] CSampleCredential::UnAdvise
[932] CSampleCredential::Advise
[932] CSampleCredential::GetSerialization           // 点击登录按钮后出发的函数
[932] CSampleCredential::UnAdvise
[932] CSampleProvider::UnAdvise
[932] CSampleCredential::Advise
[932] CSampleCredential::ReportResult               // 登录成功或失败返回调用的函数
[932] CSampleCredential::UnAdvise

8 评论

      1. 请问,我按照上面进行的编译,替换库,然后使用Register.reg注册,但是按ctrl+alt+insert 返回到登录之后没有反应,还是以前的凭据,不清楚是怎么回事,请给我一个方向怎么查找这个问题,一直做linux下开发,对于windows不甚了解,请谅解。

        1. 注意自己编译的程序是32位还是64位的, 如果你放了一个32位的程序到 system32 下,系统是不会自动加载它的。目前这也是我唯一能想到的可能。

      2. 请问,我按照上面进行的编译,替换库,然后使用Register.reg注册,但是按ctrl alt insert 返回到登录之后没有反应,还是以前的凭据,不清楚是怎么回事,请给我一个方向怎么查找这个问题,一直做linux下开发,对于windows不甚了解,请谅解。

    1. 我们写好的 dll 文件的路径在写入到注册表 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Providers 下的时候,每次你进行锁屏、开机登录的时候,这个 dll 文件都会被系统的 LogonUI.exe 进程加载起来展现给最终用户,而界面上显示什么内容、显示哪些控件就是由我们自己编写的 dll 决定的了。
      

      在这个注册表键值下把我们自己导入的注册表信息删除就可以了。微软给出的示例项目目录下也有 Unregistry.reg 的反注册注册表文件。

      https://github.com/Microsoft/Windows-classic-samples/blob/master/Samples/CredentialProvider/cpp/Unregister.reg

说说你的想法