使用gSOAP开发实例(8) Phase 1 完结篇 自定义header实现用户名令牌认证(Usernametoken Authentication)

上一节介绍了 怎样实现基本认证 (Basic Authentication ,以下简称 basic 方式 ) ,望文生义,也就是最简单的用户验证方式,本节稍微深入一些,介绍用户名令牌认证 (Usernametoken Authentication ,以下简称 usernametoken 方式 )

 

Usernametoken 方式与 basic 方式不同的地方,在于后者会把用户名和密码以摘要 (digest) 的形式,置于 HTTP 信息头,而前者则把用户名以明文的形式、密码以明文或者摘要的形式,嵌入到一段 XML 文本中,再置于 SOAP 消息头当中。

 

如果使用 soapUI 调试客户端程序的话,会发现以下是 basic 方式发出的完整的 SOAP 消息:

POST https://test2.r-secure.com/Services/ECHO HTTP/0.9

Content-Type: text/xml;charset=UTF-8

SOAPAction: ""

User-Agent: Jakarta Commons-HttpClient/3.1

Content-Length: 292

Authorization: Basic VkYtSEstbVNNST0OdlR42EMZaD1BMyE=

Host: test2.r-secure.com

Cookie: $Version=0; MSP2LB=test2.test2f02; $Path=/

 

  

  

     

         hello

     

  

 

以下是 usernametoken 方式发出的完整的 SOAP 消息:

POST https://test.r-secure.com/4.0/services/SecureEcho HTTP/1.1

Content-Type: text/xml;charset=UTF-8

SOAPAction: ""

User-Agent: Jakarta Commons-HttpClient/3.1

Host: test.r-secure.com

Content-Length: xxx

 

  

     

        

            roy

            liang

            LX4gh+njbEtCNAtkWkXDYA==

            2010-08-11T06:02:25.874Z

        

     

      G06164

  

  

     

         hello

     

  

 

其中,加粗部分表示两者的主要区别,红字部分表示各自必不可少的元素。

 

由此可以看出, usernametoken 方式的特点,是在 SOAP header 中加入一个 Security 标签,把 usernametoken 信息放在这个 Security 标签里。至于 header 里的另外一个 customerId 标签,是该应用自身的额外要求。

 

 

gSOAP 实现 basic 方式相对简单,不过实现 usernametoken 就比较复杂。 gSOAP 的用户指南推荐使用其自带的插件,在 samples/wsse 目录下的官方实例也是使用这个插件。但是,我个人认为这种方法比较累赘,自动生成的代码太多,不易看懂,而且似乎非常依赖于 wsdl 本身的写法。比如, samples/wsse 目录下的官方实例含有下列表示 SOAP header 的结构体,但是,在我实际开发的应用并没有自动产生,即使强行加上去,编译执行通过,运行的时候也出现了相当多的错误。

 

struct SOAP_ENV__Header { struct _wsse__Security *wsse__Security };

 

而且,从理论上讲, gSOAP 不过是一个框架,定义了从 SOAP 对象到 SOAP 消息,以及从 SOAP 消息到 SOAP 对象的序列化过程,并且提供了一套与之相适应的 API ,使用 gSOAP 开发不过是在其框架范围内调用其 API 编程。框架的弊端,可想而知,限制了灵活,也限制了方便,更限制了创新。所以,我们可以使用 gSOAP 编程,但是也许没有必要全部照搬,至少在这个案例中,就没有必要照搬。

 

我们应有的思路是,既然 customerId 可以直接写到 SOAP header 中,那么与之并列的、含有 usernametoken Security 也可以直接写到 SOAP header 中,完全不需要依赖于 gSOAP wsse 插件。

 

 

与上节一样,基于保密原则,本节案例采用的 wsdl 同样是经过裁剪和替换的,内容如下:

 

 

gSOAP wsdl 目录,按以下步骤建立客户端存根程序:

-bash-3.2$ mkdir –p secure_echo

-bash-3.2$ cd secure_echo

-bash-3.2$ ../wsdl2h –c –o secure_echo.h secure_echo.wsdl

-bash-3.2$ ../../src/soapcpp2 –C –L –x secure_echo.h

 

重点来了,此时需要修改 gSOAP 为你自动生成的部分文件,加入 usernametoken 支持,以下是详细步骤,代码中加粗部分即修改的内容:

1.     soapStub.h ,搜索 SOAP_ENV__Header 结构体,本案例中, gSOAP 只为我们自动生成了对应 customerId 的指针,因为需要在 SOAP header 中增加用户名和密码,所以要在这里手动添加这些信息。这样,修改后的 SOAP_ENV__Header 变为:

struct SOAP_ENV__Header

{

        char *wsse__username;   /* mustUnderstand */

        char *wsse__password;   /* mustUnderstand */

        char *ns1__customerId;  /* mustUnderstand */

};

2.     soapC.c ,搜索 soap_out_SOAP_ENV__Header 函数,这是客户端把 SOAP 对象转化为 SOAP 消息相关的函数,由于 gSOAP 只是自动生成了 customerId 属性的转化,我们还需要加入 Security 属性,按照 soapUI 测试好的结果, Security 含有一个 UsernameToken ,而 UsernameToken 又含有用户名和密码,因此 soap_out 函数应当这样写:

SOAP_FMAC3 int SOAP_FMAC4 soap_out_SOAP_ENV__Header(struct soap *soap, const char *tag, int id, const struct SOAP_ENV__Header *a, const char *type)

{

        if (soap_element_begin_out(soap, tag, soap_embedded_id(soap, id, a, SOAP_TYPE_SOAP_ENV__Header), type))

                return soap->error;

        if (soap_element_begin_out(soap, "wsse:Security", -1, "")

                || soap_element_begin_out(soap, "wsse:UsernameToken", -1, "")

                || soap_out_string(soap, "wsse:Username", -1, &a->wsse__username, "")

                || soap_out_string(soap, "wsse:Password", -1, &a->wsse__password, "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText")

                || soap_element_end_out(soap, "wsse:UsernameToken")

                || soap_element_end_out(soap, "wsse:Security")

        )

                return soap->error;

        soap->mustUnderstand = 1;

        if (soap_out_string(soap, "ns1:customerId", -1, &a->ns1__customerId, ""))

                return soap->error;

        return soap_element_end_out(soap, tag);

}

3.     SecureEchoHttpBinding.nsmap ,由于上一步用到了 wsse 这个 namespace ,而它又没有出现在 nsmap 文件中,因此我们需要增加该命名空间的信息,其 URL 同样可以从 soapUI 测试结果中取得:

SOAP_NMAC struct Namespace namespaces[] =

{

        {"SOAP-ENV", "http://schemas.xmlsoap.org/soap/envelope/", "http://www.w3.org/*/soap-envelope", NULL},

        {"SOAP-ENC", "http://schemas.xmlsoap.org/soap/encoding/", "http://www.w3.org/*/soap-encoding", NULL},

        {"xsi", "http://www.w3.org/2001/XMLSchema-instance", "http://www.w3.org/*/XMLSchema-instance", NULL},

        {"xsd", "http://www.w3.org/2001/XMLSchema", "http://www.w3.org/*/XMLSchema", NULL},

        {"ns1", "http://echo.ws.rsecure.com", NULL, NULL},

        {"wsse", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", NULL, NULL},

        {NULL, NULL, NULL, NULL}

};

 

 

存根程序修改完成,就可以开始编写客户端程序代码了。这个 web service 提供了两个接口,一个是 secure_echo ,也就是客户端送任意信息上来,服务端就返回相同的字符串,代码如下:

 

#include "soapH.h" #include "SecureEchoHttpBinding.nsmap" int main(int argc, char **argv) { if ( argc != 5 && argc != 6 ) { printf("Usage: %s username password customer_id message [end_point]/n", argv[0]); exit(-1); } struct soap soap; soap_init(&soap); struct _ns1__sendEcho request; struct _ns1__sendEchoResponse response; soap_ssl_init(); if ( soap_ssl_client_context(&soap, SOAP_SSL_NO_AUTHENTICATION, NULL, NULL, NULL, NULL, NULL) ) { soap_print_fault(&soap, stderr); exit(-1); } struct SOAP_ENV__Header header; header.wsse__username = argv[1]; header.wsse__password = argv[2]; header.ns1__customerId = argv[3]; soap.header = &header; //soap_write_SOAP_ENV__Header(&soap, &header); request.message = argv[4]; char *endpoint = NULL; if ( argc == 6 ) endpoint = argv[5]; printf("username : %s/n", header.wsse__username); printf("password : %s/n", header.wsse__password); printf("customer id : %s/n", header.ns1__customerId); printf("message : %s/n", request.message); if ( endpoint ) printf("end point : %s/n", endpoint); if ( soap_call___ns1__sendEcho(&soap, endpoint, NULL, &request, &response) == SOAP_OK ) { printf("%s/n", response.out); } else { soap_print_fault(&soap, stderr); } soap_destroy(&soap); soap_end(&soap); soap_done(&soap); return 0; }

 

另外一个是显示版本信息,代码如下:

 

#include "soapH.h" #include "SecureEchoHttpBinding.nsmap" int main(int argc, char **argv) { if ( argc != 4 && argc != 5 ) { printf("Usage: %s username password customer_id [end_point]/n", argv[0]); exit(-1); } struct soap soap; soap_init(&soap); struct _ns1__showVersionInformation request; struct _ns1__showVersionInformationResponse response; soap_ssl_init(); if ( soap_ssl_client_context(&soap, SOAP_SSL_NO_AUTHENTICATION, NULL, NULL, NULL, NULL, NULL) ) { soap_print_fault(&soap, stderr); exit(-1); } struct SOAP_ENV__Header header; header.wsse__username = argv[1]; header.wsse__password = argv[2]; header.ns1__customerId = argv[3]; soap.header = &header; //soap_write_SOAP_ENV__Header(&soap, &header); char *endpoint = NULL; if ( argc == 5 ) endpoint = argv[4]; printf("username : %s/n", header.wsse__username); printf("password : %s/n", header.wsse__password); printf("customer id : %s/n", header.ns1__customerId); if ( endpoint ) printf("end point : %s/n", endpoint); if ( soap_call___ns1__showVersionInformation(&soap, endpoint, NULL, &request, &response) == SOAP_OK ) { printf("%s/n", response.out); } else { soap_print_fault(&soap, stderr); } soap_destroy(&soap); soap_end(&soap); soap_done(&soap); return 0; }

 

两个客户端程序都是一样的结构,仅仅是调用的接口不一样。与上一节的 basic 方式的客户端相比, usernametoken 方式的用户密码不是保存在 soap 结构体的 userid 变量和 passwd 变量中,而是保存在 header 指针指向的 SOAP_ENV__Header 结构体中,这就是刚才我们为什么要修改 SOAP_ENV__Header 结构体的原因。

 

两个客户端程序分别保存为 secure_echo.c show_version.c ,编译命令分别是:

gcc -DWITH_OPENSSL -O2 -o secure_echo secure_echo.c soapC.c soapClient.c ../../stdsoap2.c -I../.. -L../.. -lgsoap -lssl

 

gcc -DWITH_OPENSSL -O2 -o show_version show_version.c soapC.c soapClient.c ../../stdsoap2.c -I../.. -L../.. -lgsoap –lssl

 

由此可见,编译的源代码和链接的库文件都与上一节的没什么区别,没有使用额外的插件。

 

客户端程序搞掂,然后就是服务端了。

 

回到 gSOAP wsdl 目录,按以下步骤建立服务端存根程序:

-bash-3.2$ mkdir –p secure_echo_server

-bash-3.2$ cd secure_echo_server

-bash-3.2$ cp –p ../secure_echo/secure_echo.h .

-bash-3.2$ ../../src/soapcpp2 –S –L –x secure_echo.h

 

与客户端程序一样,服务端同样要进行一些修改,步骤如下:

1.     soapStub.h ,与客户端的修改是一样的

2.     soapC.c ,搜索 soap_in_SOAP_ENV__Header 函数,注意客户端修改的是 soap_out 函数,这里是 soap_in 函数,是服务端把 SOAP 消息转化为 SOAP 对象的函数。由于客户端送上来的 SOAP header 消息含有 Security 属性,我们需要把它转为 username password 。修改后的代码如下,加粗部分是修改内容:

SOAP_FMAC3 struct SOAP_ENV__Header * SOAP_FMAC4 soap_in_SOAP_ENV__Header(struct soap *soap, const char *tag, struct SOAP_ENV__Header *a, const char *type)

{

        size_t soap_flag_wsse__security = 1;

        size_t soap_flag_ns1__customerId = 1;

        if (soap_element_begin_in(soap, tag, 0, type))

                return NULL;

        a = (struct SOAP_ENV__Header *)soap_id_enter(soap, soap->id, a, SOAP_TYPE_SOAP_ENV__Header, sizeof(struct SOAP_ENV__Header), 0, NULL, NULL, NULL);

        if (!a)

                return NULL;

        soap_default_SOAP_ENV__Header(soap, a);

        if (soap->body && !*soap->href)

        {

                for (;;)

                {

                        if ( soap_flag_wsse__security ) {

                                if ( soap_element_begin_in(soap, NULL, 0, NULL) )

                                         return NULL;

                                if ( soap_element_begin_in(soap, NULL, 0, NULL) )

                                        return NULL;

                                if ( soap_in_string(soap, "", &a->wsse__username, "")

                                         && soap_in_string(soap, "", &a->wsse__password, "") ) {

                                        soap_flag_wsse__security--;

                                        soap_element_end_in(soap, NULL);

                                         soap_element_end_in(soap, NULL);

                                }

                        }

                        soap->error = SOAP_TAG_MISMATCH;

                        if (soap_flag_ns1__customerId && (soap->error == SOAP_TAG_MISMATCH || soap->error == SOAP_NO_TAG))

                                if (soap_in_string(soap, "ns1:customerId", &a->ns1__customerId, "xsd:string"))

                                {       soap_flag_ns1__customerId--;

                                        continue;

                                }

                        if (soap->error == SOAP_TAG_MISMATCH)

                                soap->error = soap_ignore_element(soap);

                        if (soap->error == SOAP_NO_TAG)

                                 break;

                        if (soap->error)

                                return NULL;

                }

                if (soap_element_end_in(soap, tag))

                        return NULL;

        }

        else

        {       a = (struct SOAP_ENV__Header *)soap_id_forward(soap, soap->href, (void*)a, 0, SOAP_TYPE_SOAP_ENV__Header, 0, sizeof(struct SOAP_ENV__Header), 0, NULL);

                if (soap->body && soap_element_end_in(soap, tag))

                        return NULL;

         }

        return a;

}

 

 

服务端程序如下,到这里就没什么难度了,基本上与上一节的服务端结构差不多,注意要把几个 pem 证书拷贝过来(因为 usernametoken 方式通常都是基于 HTTPS 的), echo 接口和 show_version 接口都要编写:

 

#include #include "soapH.h" #include "SecureEchoHttpBinding.nsmap" void *process_request(void *soap) { pthread_detach(pthread_self()); if ( soap_ssl_accept((struct soap *) soap) != SOAP_OK ) soap_print_fault((struct soap *) soap, stderr); else soap_serve((struct soap *) soap); soap_end((struct soap *) soap); soap_free((struct soap *) soap); return NULL; } int main(int argc, char **argv) { if ( argc != 2 ) { printf("Usage: %s port/n", argv[0]); exit(-1); } int port = atol(argv[1]); pthread_t tid; struct soap *tsoap; struct soap soap; soap_init(&soap); soap_ssl_init(); if ( soap_ssl_server_context(&soap, SOAP_SSL_DEFAULT, "server.pem", "password", "cacert.pem", NULL, "dh512.pem", NULL, argv[0]) ) { soap_print_fault(&soap, stderr); exit(-1); } int m, s; if ( (m = soap_bind(&soap, NULL, port, 100)) < 0 ) { soap_print_fault(&soap, stderr); } else { printf("Socket connect successfully: master socket = %d/n", m); int i = 0; while ( 1 ) { if ( (s = soap_accept(&soap)) < 0 ) { soap_print_fault(&soap, stderr); break; } printf("Connection %d accepted from IP = %d.%d.%d.%d, slave socket = %d/n", ++i, (soap.ip >> 24) & 0xff, (soap.ip >> 16) & 0xff, (soap.ip >> 8) & 0xff, soap.ip & 0xff, s); tsoap = soap_copy(&soap); if ( !tsoap ) { soap_closesock(&soap); continue; } pthread_create(&tid, NULL, &process_request, (void *) tsoap); } } soap_done(&soap); return 0; } int __ns1__sendEcho( struct soap *soap, struct _ns1__sendEcho *request, struct _ns1__sendEchoResponse *response) { if ( !soap->header || !soap->header->wsse__username || !soap->header->wsse__password || !soap->header->ns1__customerId || strcmp(soap->header->wsse__username, "roy") || strcmp(soap->header->wsse__password, "liang") || strcmp(soap->header->ns1__customerId, "G06164")) return 401; int len = strlen(request->message); response->out = (char *) malloc(sizeof(char) * (len + 1)); strcpy(response->out, request->message); return SOAP_OK; } int __ns1__showVersionInformation( struct soap *soap, struct _ns1__showVersionInformation *request, struct _ns1__showVersionInformationResponse *response) { if ( !soap->header || !soap->header->wsse__username || !soap->header->wsse__password || !soap->header->ns1__customerId || strcmp(soap->header->wsse__username, "roy") || strcmp(soap->header->wsse__password, "liang") || strcmp(soap->header->ns1__customerId, "G06164")) return 401; response->out = (char *) malloc(sizeof(char) * 100); strcpy(response->out, "Username token (text) test server version 1.0"); return SOAP_OK; }

 

服务端程序保存为 secure_echo_server.c ,编译命令是

gcc -DWITH_OPENSSL -O2 -o secure_echo_server secure_echo_server.c soapC.c soapServer.c ../../stdsoap2.c -I../.. -L../.. -lgsoap -lssl –lcrypto

 

运行测试,在 6883 端口启动服务端,然后执行客户端

-bash-3.2$ ./show_version roy liang G06164

username    : roy

password    : liang

customer id : G06164

Username token (text) test server version 1.0

 

-bash-3.2$ ./secure_echo roy liang G06164 hello

username    : roy

password    : liang

customer id : G06164

message     : hello

hello

 

以上就是 gSOAP 实现 Usernametoken 认证的方法,而且是通过自定义 SOAP header 实现的。个人认为,与使用 wsse 插件相比,这种方法更为简单直接。

 

另外,本案例的方法适用于以明文传送密码的情况,如果需要以摘要 (digest) 形式传送密码,请参考 plugin 目录 wsseapi.c 里面的 soap_wsse_add_UsernameTokenDigest 函数。

 

最后,感谢 Codejie's C++ Space 为本节的编写提供思路:

http://www.cppblog.com/codejie/archive/2010/04/07/89972.html

 

 

你可能感兴趣的:(C/C++)