原文:使用WSE实现Web Service安全----我的第一篇
WSE(Web Services Enhancements)是微软为了使开发者通过.NET创建出更强大,更好用的Web Services而推出功能增强插件。现在最新的版本是WSE2.0(SP2).本文描述了如何使用WSE2.0中的安全功能增强部分来实现安全的Web Services。WSE的安全功能增强实现的是WS-Security标准,此标准是WebService自己的安全协议,由IBM, BEA, Microsoft等联合制定,所以在.NET和Java系统上都可实现。
我主要是根据WSE的文档说明和自己使用体会(其实多半也是按照文档画瓢,呵呵)而写,有错误之处请指出。另外,这是用的都是2.0,它与WSE1.0相比,变化很大,尤其在安全方面。
还有一点注意,其实通过WSE实现安全有两种途径:一种就是我们下面要介绍的通过编写代码的方法;另外一种是直接编写策略文件(XML文档),这两种方法本质上都是通过对传递的SOAP消息进行设置,如增加用户消息,加解密,签名验证等,来实现安全功能的。但本人对第二种方法不太熟悉,也没时间研究,就不写了,呵呵。
一、使用用户名和口令验证Web Services调用者身份。
原理很简单:客户端通过SOAP扩展,在SOAP消息中加入用户名和口令(明文或加密),发送给Web Services端;服务端接到消息后,同样通过扩展从消息上下文中得到用户名和口令,再进行身份验证和其他操作。下面是实现步骤:
客户端:
1.添加Microsoft.Web.Services2和客户端要访问的Web Services引用,没有什么好说的。当然,这两个引用是必须的,你可能还需要客户端需要添加其他引用。
2.修改从Web Services引用生成的本地proxy类,这个类的代码在引用生成Reference文件中。从.NET开发环境右边的解决方案资源管理器里打开你要操作的Web Services引用文件夹,打开Reference.map节点,就可以看到Reference.cs或Reference.vb文件。如果你没有看到这些文件,可能是没有显示所有文件,你需要在解决方案资源管理器或命令菜单“项目”里设置“显示所有文件”。找到Reference文件后,打开它,在proxy类的声明处将类的继承父类改成Microsoft.Web.Services2.WebServicesClientProtocol.因为只有这样,proxy类才能访问WSE提供的SOAP扩展。注意,如果更新了Web服务的引用,则需要重新把继承类修改。
3.通过UsernameToken类的实例添加用户名与口令。UsernameToken属于Microsoft.Web.Services2.Security.Tokens命名空间。假设Web Services的本地proxy类名称为WebServer.WebService,用户名为Username,用户口令为Userpwd,则代码可以如下所示(vb.net,下同):
'生成本地proxy类实例
Dim mywebserv as New WebServer.WebService
'生成UsernameToken类实例,将用户名,用户口令和口令发送方式写在实例中
Dim untoken As UsernameToken = New UsernameToken(Username, Userpwd, PasswordOption.SendPlainText)
'设置SOAP消息有效期,以确减少消息即使被其他用户截获后重新使用的可能性为,这里设置为30秒,但要注意不同系统时钟同步的问题。
mywebserv.RequestSoapContext.Security.Timestamp.TtlInSeconds = 30
'将UsernameToken实例加在SOAP消息上下文中
mywebserv.RequestSoapContext.Security.Tokens.Add(untoken)
'调用Web服务的方法WebMethod
mywebserv.WebMethod
这里需要说明口令发送方式的设置。口令发送方式为枚举型数据:SendNone,SendHashed,SendPlainText.分别为不发送口令,发送口令哈希值和发送口令明文,上面的例子是使用发送明文。如果选择SendNone,表示服务端不要验证口令,这个安全级别就很低,而且如果服务需要对传递的SOAP消息签名,则客户端要单独提供口令对其签名;SendHashed是指发送的是口令的SHA-1哈希值,这种情况下口令保护安全,这也是微软推荐的最好方式。但它需要服务器端通过编写代码和config文件设置自己来验证用户身份,具体验证方法在下面的服务器端设置会讲到;SendPlainText是口令以明文方式传递,如果采取这种方式,上述代码中的UsernameToken最好加密,否则口令很容易被截获。加密方法以后章节将详细论述;另外,如果服务器端选择让WSE自动根据Windows活动目录域的用户进行身份验证,那这里必须选择发送明文。
服务端:
1.首先在Web Services的配置文件Web.config里添加配置WSE的元素,这也是.NET开发的系统使用WSE最基本的一步。WSE文档上说如果服务的调用端是ASP.NET系统时才需要这一步的配置,如果是普通的Client程序则不需要加这个标识。可我测试发现即使调用端是Client程序,还是需要加这个配置。下面是完整的配置文档。
<configuration>
<system.web>
<webServices>
<soapExtensionTypes>
<add type="Microsoft.Web.Services2.WebServicesExtension, Microsoft.Web.Services2,Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" priority="1" group="0"/>
</soapExtensionTypes>
</webServices>
</system.web>
</configuration>
当然,一般情况下你只需要在Web.config的<system.web>...</system.web>之间添加<webServices>...</webServices>部分。另外,add元素的数据最好写成一行,否则可能会出错。
2.配置服务端的调用身份验证行为,这一步是可选的。
如前所述,调用方发过来的SOAP消息中已加入了用户信息,服务端要做的工作是将这些信息解析出来,再根据一定规则对比验证,并返回结果。
上述处理过程也有两种选择:一是让WSE自动验证,我们自己的代码和配置文件再也不用做什么了。这种情况下,WSE在服务端收到调用方的SOAP消息后,从里面的UsernameToken中取出用户名和口令,这也是为什么前面提到过的自动验证下用户口令必须明文发送的原因,取出用户名和口令后,WSE是基于系统所在Windows域的用户进行判别和验证的。也就是说,WSE从活动目录里的用户列表遍历,寻找是否存在和所接收到的用户名/口令匹配的有效用户用户帐号,如果未找到匹配用户,则返回调用者未被授权的错误。由此可见,这种方法下应用系统及用户需要和Windows域紧密捆绑,缺乏灵活性并且不适合与已有业务系统对接。因此在实际应用中更多的应该是用下面第二种方法。
这种方法的核心是Web Services通过继承UsernameTokenManager类,并重载AuthenticateToken方法实现的。
a.首先声明一个从UsernameTokenManager继承下来的类。
Public Class CustomUsernameTokenManager
Inherits UsernameTokenManager
这里面有一个访问权限问题,为了使经过授权的程序集才能访问这个类,你还需要给它在声明时加上一些访问限定。因为能够访问非托管代码(UnmanagedCode)的程序集信任级别都是比较高的,所以可以要求能访问此类的程序集都是可以访问UnmanagedCode的。这样上面的声明就变成如下形式:
<SecurityPermission(SecurityAction.Demand,Flags:= SecurityPermissionFlag.UnmanagedCode)> Public Class CustomUsernameTokenManager
Inherits UsernameTokenManager
当然你也可以配置成其他权限要求,只要更改Flags的SecurityPermissionFlag枚举值即可。
b.在代码中重载AuthenticateToken方法。服务端接收到含有UsernameToken实例的SOAP消息后,WSE将UsernameToken反序列化,并调用VerifyToken方法,而VerifyToken方法在执行过程中又会调用AuthenticateToken方法,这个方法会返回一个口令值,WSE会拿它与UsernameToken中的口令进行对比。如果发送的口令为明文(UsernameToken.PasswordOption=SendPlainText),则直接对比;如果发送口令为哈希值(UsernameToken.PasswordOption=SendHashed),则WSE会对这个返回值做一个SHA-1哈希运算,再将结果与UsernameToken中的口令进行对比。如果不一致,则返回用户未被授权的错误。上述过程全部是自动完成的,因此我们要做的工作就是重载AuthenticateToken方法,在里面实现返回正确的用户口令,用于对比和验证。这其实就相当于WSE1.0里面的PasswordProvider.
重载AuthenticateToken方法实现的逻辑由系统根据具体要求来定,比如根据用户名去数据库里寻找相应的用户口令,或者从LDAP中等等。这里就照找了WSE文档上的例子,返回的口令和提交的用户名相同。
Protected Overrides Function AuthenticateToken(ByVal userName As UsernameToken) As String
' Ensure that the SOAP message sender passed a UsernameToken.
If userName Is Nothing Then
Throw New ArgumentNullException
End If
' This is a very simple provider.
' In most production systems the following code
' typically consults an external database of (userName,hash)
' pairs. For this example, it is the UTF-8
' encoding of the user name.
Dim encodedUsername As Byte() = _
System.Text.Encoding.UTF8.GetBytes(userName.Username)
Return System.Text.Encoding.UTF8.GetString(encodedUsername)
End Function
c.配置web.config文件,告知Web Services使用哪个类来验证用户身份。
首先在文件中加入标明使用WSE2的元素:
<configuration>
<configSections>
<section name="microsoft.web.services2"
type="Microsoft.Web.Services2.Configuration.WebServicesConfiguration, Microsoft.Web.Services2, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
</configSections>
</configuration>
这里要注意的是一定要把<configSections>...</configSections>写到整个<configuration>...</configuration>里的最前面,否则会出现“响应内容类型为 "text/html;charset=utf-8",但应该是"text/xml"”的错误。
其次配置使用UsernameTokenManager子类
<configuration>
<microsoft.web.services2>
<security>
<securityTokenManager xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"qname="wsse:UsernameToken" type="MyNamespace.CustomUsernameTokenProvider,MyAssemblyName,Version=2.0.0.0,Culture=neutral,PublicKeyToken=81f0828a1c0bb867" />
</security>
</microsoft.web.services2>
</configuration>
在这里面type属性的值要保持在一行,"MyNamespace.CustomUsernameTokenProvider”必须是派生类有效的类层次名,MyAssemblyName为程序集名称。另外Version,Culture和PublicKeyToken等属性可以省略 。
这样,我们就完成了在Web Service调用最基本的基于用户名/口令的验证。以后各项安全功能的实现都以它为基础。
二、使用用户名和口令对SOAP消息签名
上述过程虽然实现了Web Services用户身份确认。但它不能保证Web Services接收到的SOAP消息就是所声明用户所发送的,因此在实际的应用中是还需要调用方对SOAP消息做一次签名,并将签名连同消息一起发送;服务端接收到消息后,除了验证用户身份外,还需要对其中的签名做一次认证。以确定消息在传输过程中没有被更改过,而验证的用户就是对消息签名的用户。
客户端:
只需要在原有客户端SOAP消息中添加签名即可,如下所示,黑体为新加代码:
mywebserv.RequestSoapContext.Security.Tokens.Add(untoken)
mywebserv.RequestSoapContext.Security.Elements.Add(New _
MessageSignature(untoken))
mywebserv.WebMethod
上述代码就是客户端根据UsernameToken的实例生成一个签名,然后把签名加在SOAP消息里,具体是WS-Security扩展的SOAP消息头。
服务端:
其实服务端无需改动什么。只要客户端发送的SOAP消息里面包含了签名,服务端就会自动验证签名的有效性。服务端首先验证用户名/口令,然后使用客户端传递的用户名和自己获得的口令(WSE自动从Windows活动目录中获得,或者通过重载的AuthenticateToken的方法)对签名进行验证。如果验证失败,说明消息在传递过程中被更改过或者不是当前调用用户所签,返回相应错误。
WSE的帮助文档上这一块加了一个判断SOAP里是否包含签名的函数,通过它可以获得签名的一些相关信息,并不是必须的。有兴趣的朋友可以自己去看。
三、使用证书验证身份并对SOAP消息签名
使用用户名/口令存在固有缺点,因此在某些安全要求较严格的系统中我们还需要使用证书来完成对用户身份的验证。关于PKI/CA的基础知识这里就不介绍了,大家可以去看相关资料。
首先我们需要配置服务端保证WSE可以访问证书及其私钥。(至于证书的申请,管理和使用属于基础知识,在这就不讲了)这里主要是在Web Services端设置,以保证服务端可以自动完成某些功能,还无需用户干预。在Web.config文件添加如下<x509>元素(黑体部分):
<configuration>
<microsoft.web.services2>
<security>
<x509 storeLocation="CurrentUser" />
</security>
</microsoft.web.services2>
</configuration>
storeLocation属性为WSE可访问的证书存储。它可选的值有两项:“LocalMachine”和“CurrentUser”,分别表示本机证书存储和当前用户证书存储。这里用的是CurrentUser,而storeLocation缺省值是LocalMachine.另外几个属性的配置如下: verifyTrust,是否验证所用证书的证书链有效性,缺省为true;allowTestRoot:验证证书链过程中是否认为测试根CA所签发证书有效,这个参数在verifyTrust为true时才有效,缺省为false;allowRevocationUrlRetrieval,是否在线验证证书是否被吊销,在verifyTrust为true时才有效,缺省为true;allowUrlRetrieval,是否在线验证证书链有效性,allowRevocationUrlRetrieval,是否在线验证证书吊销状态。
客户端:
第1、2步与第一节使用用户名/口令的步骤基本相同。但这里也要多加一个引用:Microsoft.Web.Services2.Security.X509
3.编写代码取得要使用的证书。我这里用了一个listview控件显示当前用户证书存储里的个人证书,以供用户选择。你当然也可以象WSE文档上那样利用证书的属性去定位需要使用的证书。Listview控件名称为lv_certlist,它有两列:证书持有者主体和颁发者名称。
Dim cert As X509Certificate
Dim lvitem As ListViewItem
'从当前用户证书存储中打开个人证书库
certstore = X509CertificateStore.CurrentUserStore(X509CertificateStore.MyStore)
certstore.Open()
lv_certlist.Items.Clear()
'遍历证书,写到listview控件里
For Each cert In certstore.Certificates
lvitem = lv_certlist.Items.Add(cert.GetName)
lvitem.SubItems.Add(cert.GetIssuerName)
lvitem.Tag = lvitem.Index
Next cert
4.选择所选择的证书生成X509SecurityToken的实例,它和前面的UsernameToken一样是SecurityToken的子类。
Dim cert As X509Certificate
Dim certToken As X509SecurityToken
Dim result As String
cert = certstore.Certificates(lv_certlist.SelectedIndices(0))
’判断所选择证书是否支持签名,并且私钥是否存在
If cert.SupportsDigitalSignature And Not (cert.Key Is Nothing) Then
'遍历证书,写到listview控件里
certToken = New X509SecurityToken(cert)
Dim mywebserv As New WebServer.WebService
mywebserv.RequestSoapContext.Security.Timestamp.TtlInSeconds = 30
'添加包含证书信息的X509SecurityToken
mywebserv.RequestSoapContext.Security.Tokens.Add(certToken)
'使用证书对SOAP消息签名,并将结果写在消息中
mywebserv.RequestSoapContext.Security.Elements.Add(New _
MessageSignature(certToken))
'调用Web服务的方法
mywebserv.WebMethod
End If
服务端:
和验证使用用户名/口令签名的消息类似,服务端也同样自动验证签名有效性。同时,服务端会根据web.config文件中<x509>元素里的属性设置来对证书的有效性进行验证。如果验证失败,则返回相应错误。签名所使用证书可以通过SoapContext.Security.Tokens得到
四、使用用户名和口令对SOAP消息加密
前面第一节讲过,如果UsernameToken实例中的口令是明文,那最好将UsernameToken加密发送。在第一节客户端代码中做如下改动,黑体代码为新加:
mywebserv.RequestSoapContext.Security.Tokens.Add(untoken)
mywebserv.RequestSoapContext.Security.Elements.Add(New _
Microsoft.Web.Services2.Security.EncryptedData (untoken))
mywebserv.WebMethod
上述代码就是客户端根据UsernameToken的对SOAP消息加密,然后把密文加在SOAP消息里,具体是WS-Security扩展的SOAP消息头
服务端自动对消息解密。服务端首先验证用户名/口令,然后使用客户端传递的用户名和自己获得的口令(WSE自动从Windows活动目录中获得,或者通过重载的AuthenticateToken的方法)对解密数据。如果失败,返回相应错误。
WSE的帮助文档上这一块同样加了一个判断SOAP是否加密的函数。
五、使用证书对SOAP消息加解密
这个略微复杂一些。根据PKI非对称加解密原理,对消息加密的客户端需要事先得到作为接收方的服务端的证书公钥,而服务端必须保证可以访问其证书对应的私钥。因此我们首先要在服务端选择设置好它所使用的证书,原则就是使服务端能够随时并自动访问其证书对应的私钥。
对于基于ASP.NET开发的Web Services,需要给它运行时的缺省用户赋予访问证书私钥的权限。运行在IIS 6.0上的Web Services缺省用户是Network Service,其他情况下用户账户由machine.config文件里<processModel>元素的userName属性决定,缺省情况下是“machine”,它等同于ASPNET账户。添加步骤如下:运行WSE 2.0自带的X.509 Certificate tool(开始---程序---Microsoft WSE 2.0---X.509 Certificate tool),选择并打开WSE要访问的证书。打开后,“Private Key File Name”文本框里会显示证书所对应私钥文件的名称,Private Key File Folder”文本框里会显示私钥文件的路径。单击“Private Key File Properties”打开文件属性对话框,可以在安全属性页里添加被授权访问的Network Service或ASPNET用户账户。
如果打开证书后发现私钥文件不存在或不可访问,说明此证书有问题或者私钥受保护,比如有口令保护或者存储在安全外设中,不能被Web Services随时自动访问,因此也就不适用。
对于Web Services,如果使用的用户是缺省的ASPNET,那最好选择本机存储(LocalMachine Store)里的证书。因为ASPNET这个用户是安装ASP.NET时自动生成的,它的口令是没有办法访问的。如果证书包含在当前用户存储(CurrentUser Store)中,服务端很可能就访问不到私钥。
服务端设置好证书后,接下来就要使客户端得到这个证书和公钥。具体的方法和你的业务系统和应用需求有关系,你可以把它放在网页上让用户事先下载安装;如果应用在局域网环境,可以让客户端去LDAP或CA里取得。
客户端做如下改动:
mywebserv.RequestSoapContext.Security.Tokens.Add(certToken)
’SOAP消息中添加加密结果
mywebserv.RequestSoapContext.Security.Elements.Add(New _
Microsoft.Web.Services2.Security.EncryptedData(certToken))
mywebserv.WebMethod
服务端接收到加密后的SOAP消息后,自动使用相应私钥解密。如果失败,返回相应错误。
可以看出,使用证书加解密消息的难点在于证书的选择和配置,尤其是消息接收端。
WSE除了支持上述用户名/口令,X509证书两种令牌外。还支持Kerberos Ticket,安全上下文令牌(Security Context Token)和用户自定义令牌。Kerberos Ticket好象是WSE自己加的内容,标准的WS-Security里没有,而且只在Windows XP SP1/SP2,Windows2003上支持。安全上下文令牌需要一个上下文令牌服务生成。而用户自定义令牌的核心是由SecurityToken派生一个用户自己的令牌类,完成各种安全功能。总之WSE就是使用这种基于令牌和扩展SOAP消息来实现Web Services安全的。