利用 DSAPI 为 Domino Web 用户定制用户名和口令认证
万维网(World Wide Web)使用的应用层传输协议是超文本传输协议(Hypertext Transfer Protocol,HTTP)。HTTP 协议包含了一个简单的用户名和口令认证(Name-and-password authentication)机制。它使用简单的质询回应(challenge/response)协议,要求用户输入用户名和口令,通过比较服务器中保存的用户名和口令来核实口令的正确性,以达到限制用户访问特定页面的目的。
在 Lotus Domino 服务器上设置用户名和口令认证,一般来说,需要为每个 Web 用户创建个人文档(Person Document),把用户名和口令保存在个人文档中。个人文档可以放在 Domino 目录(Domino Directory)中,或者是次级 Domino 目录(Secondary Domino Directory)中,还可以放在外部的 LDAP 目录中。没有个人文档的用户,只能访问允许匿名的服务器和数据库。
但有时用户的用户名和口令并没有被保存在个人文档中,比如,用户名和口令信息被保存在外部文件或其它系统中,而且你也不想或者也不可能为这些用户重新创建个人文档;或者你想采用自己的一套认证方案。这时,我们就可以利用 Domino 网络服务器应用程序编程接口(Domino Web Server Application Programming Interface, DSAPI)为 Web 用户定制用户名和口令认证。
DSAPI 是一个 C 语言的应用程序编程接口,它使你可以为 Domino 服务器编写自己的扩展(extensions)。在处理 HTTP 请求过程中,每当一个特殊事件发生时,DSAPI 扩展,或者可以称为 DSAPI 过滤器(filter),就会被触发。
使用 DSAPI 几乎可以实现任何的用户名和口令认证标准,而不管这种标准是使用 Domino 目录或者外部 LDAP 目录中的用户名和口令,还是使用保存在 cookie 中的用户名,或者是其它机制。
另外,使用 DSAPI 还可以控制对 Domino 服务器中受保护资源的访问权限。DSAPI 不直接改变 Domino 存取控制(Access Control)。但是,它允许直接设置用户授权的用户名,这个用户名可以被用来访问受保护的资源。这样 DSAPI 的开发者完全可以控制把登录的用户名转换或映射到一个不同的用户名上,从而实现对 Domino 服务器中受保护资源的访问控制。
|
一个DSAPI过滤器的运行过程如下所示:
一个 DSAPI 过滤器的运行过程
一个 DSAPI 过滤器允许你定制 HTTP 请求的处理过程。但是只有 DSAPI 过滤器已经声明支持这个事件,服务器才会把该事件交给 DSAPI 过滤器处理。所以,DSAPI 过滤器首先要注册自己声明的事件。
目前一共有 13 种事件可以被 DSAPI 过滤器所捕捉到。一个 DSAPI 过滤器所能支持的事件数目取决于你的设计和具体实现,理论上它可以支持任意多个事件。DSAPI 接口的实现依赖于过滤器声明它所支持的事件。关于各种事件,请参考 Lotus C API Reference(在 Lotus C API Toolkits 中)。
Web 客户端发起一个 HTTP 请求给 Domino HTTP 服务器。
当 Domino 服务器在接收到一个 HTTP 请求时,它会判断这个请求事件的类型。如果某个 DSAPI 过滤器已经声明支持这个事件,服务器就会把这个 HTTP 事件交给这个 DSAPI 过滤器处理。这样,当一个用户访问服务器上的某个资源时,你就可以使用自定义的认证过程来代替正常的 Domino Web 认证过程。
|
DSAPI 为 Domino 服务器提供了多个不同的入口函数。任何一个 DSAPI 过滤器至少必须实现下面两个入口函数:
1)初始化函数
Domino HTTP 服务被启动的时候,DSAPI 过滤器被装载。当过滤器被装载后,初始化函数被调用。初始化函数的定义如下:
unsigned int FilterInit(FilterInitData* filterInitData)
其中 filterInitData 是指向 FilterInitData 结构的指针。FilterInitData 结构包含了 DSAPI 过滤器初始化需要的几个参数,定义如下:
- serverFilterVersion:传入的参数。过滤器所在的服务器版本。
- appFilterVersion:传出的参数。过滤器版本。需要把这个设置为 kInterfaceVersion。
- eventFlags:传出的参数。声明了过滤器想要处理的事件类型。
- initFlags:目前没有使用。
- filterDesc:传出的参数。过滤器的简短描述。
2)事件通知函数
事件通知函数实现了过滤器的实际功能。当过滤器所声明支持的事件出现的时候,事件通知函数被调用。当 Domino 服务器调用事件通知函数时,它把 HTTP 请求中相应信息传递给该函数,由事件通知函数来决定是否处理给定的事件。事件通知函数的定义如下:
unsigned int HttpFilterProc(FilterContext* pContext, unsigned int eventType, void* pEventData)
其中:
- pContext 是指向 FilterContext 结构的指针。FilterContext 结构中的数据包括:HTTP 请求信息,服务器回调函数的指针,以及一个指向过滤器自身数据上下文的指针。
- eventType 用于指明当前哪种事件出现
- pEventData 是指向一个结构的指针。这个结构的具体类型取决于 eventType 的类型。不同的事件对应不同的事件数据结构。
另外还有一些可选的入口函数,包括用于处理中止的入口函数,以及处理线程的入口函数。可以根据实际的需要,选择实现这些入口函数。相关帮助,请参考 Lotus C API Reference。
所有的入口函数都返回一个 unsigned int 类型的状态编码,它的可能取值如下:
- kFilterHandledRequest:表明 HTTP 请求已经完全被处理。相应的响应已经被发送到客户端,HTTP 栈可以中止对这个请求的处理。
- kFilterHandledEvent:表明这个事件已经被处理了,其它可以进一步处理 HTTP 请求。
- kFilterNotHandled:表明过滤器没有处理事件。
- kFilterError:表明过滤器在处理事件过程中遇到错误。在这种情况下,HTTP 栈将会停止处理请求,并且发送一个相应的错误信息给用户。
|
下面,我们通过一个简单的例子来说明如何利用 DSAPI,为 Web 用户定制用户名和口令认证。用户的用户名和口令信息被保存在一个外部文件中,只有用户名和口令正确的 Web 用户才允许访问 Domino 服务器中受保护的数据库。本例将在 Windows 平台上进行开发,并使用 Microsoft Visual C++ 6.0 作为开发工具。例程的源代码,请参见附件 npauth.c 文件。整个过程包括四个步骤:
- DSAPI 过滤器的实现
- Domino 服务器的配置
- 数据库访问控制的配置
- 运行 DSAPI 过滤器
在 Unix 环境下,DSAPI 过滤器被编译成一个共享程序库,而在 Windows 环境下,它被编译成一个动态连接库。DSAPI 被所有的 Domino 服务器平台所支持。DSAPI 函数被包含在 Lotus C API Toolkits 中,可以从下面这个网址下载:http://www.ibm.com/developerworks/lotus/downloads/toolkits.html?S_TACT=105AGX52&S_CMP=cn-a-ls
首先我们使用 Visual C++ 创建一个工程,并完成相关的配置:
- 使用 Visual C++,创建一个 Win32 动态连接库的工程
- 设定相应的路径,主要包括 Lotus C API 的头文件路径和 Lib 文件路径,可以在下载的 Lotus C API Toolkits 中找到相应的路径。
在这个例子中我们只需要实现最基本的两个入口函数。
1)初始化
unsigned int FilterInit(FilterInitData* filterInitData) { /* 这是必须的代码,用于指定接口版本 */ filterInitData->appFilterVersion = kInterfaceVersion; /* 向服务器声明所支持的事件类型。kFilterAuthenticate 表明该过滤器处理 * 用户用户名和口令的认证。当 HTTP 堆栈处于认证阶段,kFilterAuthenticate 事件出现。 * 如果过滤器支持多个事件,可以采用“|”合并, * 例如,kFilterAuthenticate| kFilterRewriteURL。 */ filterInitData->eventFlags = kFilterAuthenticate; /* 过滤器的描述 */ strcpy(filterInitData->filterDesc, "File System Authentication Filter"); return kFilterHandledEvent; } |
2) 事件通知
unsigned int HttpFilterProc(FilterContext* pContext, unsigned int eventType, void* pEventData) { /* kFilterAuthenticate 事件所对应的结构类型为 FilterAuthenticate, * 详见例程后说明 */ FilterAuthenticate* authData = NULL; /* 判断事件是否是过滤器所支持的。在函数 FilterInit() 中, * 我们声明了过滤器支持的事件类型为 kFilterAuthenticate。 */ if (eventType != kFilterAuthenticate) return kFilterNotHandled; /* 根据事件类型,把 pEventData 转化成对应的数据结构 */ authData = (FilterAuthenticate*)pEventData; /* authData->foundInCache 为 TRUE 时,表明用户已经在 Domino 内部的 cache 中发现。 * 如果过滤器不处理,直接返回 kFilterNotHandled, * Domino 服务器就会基于 cache 中的信息对用户进行认证 */ if (!authData || authData->foundInCache) return kFilterNotHandled; /* 验证用户口令 */ if (authData->userName && authData->password) { char *password = NULL; /* 根据用户的用户名,从外部文件中取到口令。从何处获取口令, * 以及如何获取口令是很自由和灵活的, * 你可以自己自己来实现这个函数。 */ if (0 == GetUserPassword(pContext, (char *)authData->userName, &password)){ /* 把用户名复制到 authName 中,这是 DSAPI 所要求的格式 */ strncpy ((char *)authData->authName, authData->userName, authData->authNameSize); /* 比较口令是否匹配 */ if (strcmp (authData->password, password) == 0) /* 口令验证成功 */ authData->authType = kAuthenticBasic; else /* 口令验证失败 */ authData->authType = kNotAuthentic; return kFilterHandledEvent; } } return kFilterNotHandled; } |
首先程序需要判断事件类型是否是所支持的。根据不同的事件,pEventData 会被转换成不同的结构。kFilterAuthenticate 事件所对应的结构类型为 FilterAuthenticate,该结构的定义可以在头文件 dsapi.h 中找到,其中比较重要的几个变量包括:
- userName:传入的参数。用户输入的用户名。
- passWord:传入的参数。用户输入的口令。
- authName:传入的参数。一个指针,指向授权的用户名。我们可以通过这个变量实现控制 Domino 存取控制。
- authType:传出的参数。用于表明认证是否成功。
foundInCache:传入的参数。如果用户已经在 Domino 内部的 cache 中存在,该变量就会被设置为 TRUE。该变量使得我们可以不重复认证已经认证过的用户。
我们可以根据传入的用户名和口令与保存的用户名和口令相验证。这个过程是很灵活和自由的。用户名和口令可以保存在 Domino 的目录中,也可以保存在数据库中,也可以保存在外部文件中,还可以从 cache 中获得。这完全取决于我们的设计。因此我们可以实现自定义的口令认证方案,以及单点登录功能(SSO)。
我们把代码编译,链接成功后得到的 DLL 文件拷贝到 domino 服务器的程序目录中。注意不是 Domino 服务器的 Data 目录。然后在Domino 服务器上注册 DSAPI 过滤器。
- 打开 Domino 服务器上的目录数据库(names.nsf),选择服务器文档。
- 选择标签“Internet Protocols”- “HTTP”。在“DSAPI filter file names”中输入得到的 DLL 文件的用户名。
为了使我们认证的用户能够访问数据库中受保护的资源,我们需要实现对数据库的访问控制。有两种方法实现:
- 为授权的用户创建个人文档
我们可以使用 Domino Administrator 工具,在 Domino 服务器上注册一个新的用户账号。注意,新的用户账号的 Short name 必须与要授权的用户用户名相一致。例如,我们要授权的用户名为 myuser,那么在 Domino 服务器上注册一个用户帐号,Short name 必须为“myuser”。然后,打开数据库的 ACL 管理,把myuser增加到该数据库的 ACL 中。这样,数据库中授权的用户名和 DSAPI 中通过认证的用户名是相同的,DSAPI 认证成功的用户,就可以访问受保护的数据库了。
- 利用在 DSAPI 中的编程实现
如果我们不想为用户创建个人文档,我们可以利用编程方式实现对访问权限的控制。结构 FilterAuthenticate 中的变量 authName 是用于存放授权的用户名。因此,即使我们没有为用户创建单独的个人文档,我们同样可以让该用户具有访问受保护资源的权限。实现的方法很简单,只要把 authName 设置成一个具有权限的用户名。例如,数据库的 ACL 中存在具有权限的 Notes ID:myuser/China/MyCom。这个 ID 可以不在任何 domino 目录中存在。那么在函数 HttpFilterProc() 中,我们可以把斜体加粗的代码改写为如下的代码:
strncpy ((char *)authData->authName, "cn=myuser/ou=China/o=MyCom ", authData->authNameSize)
重新启动 Domino 服务器,从服务器控制台上我们可以看到如下的信息:
DSAPI File System Authentication Filter Loaded successfully.
在 IE 上输入 URL <Domino-server>/<Domino-server-database> 来打开 Domino 服务器上的数据库。<Domino-server> 是 Domino 服务器的用户名,也可以是IP地址;<Domino-server-database> 是数据库的用户名。例如myserver/npauth.nsf。然后在弹出的用户名和口令对话框中输入用户的用户名和口令,你就可以访问到受限的数据库了。
DSAPI 允许我们实现对 Domino 服务器的扩展,为 Web 用户定制用户名和口令认证。它为将 Domino 整合到现有环境以及生成单点登录(Single Sign-on,SSO)提供了更大的灵活性。