传输级vs.消息级安全
选择一个Web服务安全解决方案可能是一件具有挑战性的工作。本文将向您展示一些较为流行的解决方案,并且对这些方案的性能、限制条件等进行评价,供您在选择时参考。
Web服务设计的目的是为了将企业的功能以一种可被共同使用的、松散的联结形式表现出来。虽然有着获得一个SOA基础结构利益的潜力,Web服务也为企业的资产带来了未授权访问的风险。因此,通过将访问权限限制为只给合法的用户使用,从而防止一些对系统完整性造成破坏来保障Web服务的安全是至关重要的。
其实,让我们选择一个Web服务安全解决方案可能是一件使人畏缩的事情。目前,有很多可用的解决方案,而且有很多因素可以决定某一个特定的方案适合你的情况。虽然每一个解决方案都提供了相关的说明文档等,但我们却很难找到一个全面的可以帮助我们做出正确选择的方法指南。
本文虽然并不是一个完整的指导方针,却审视了一些流行的方案,并对其功能进行了评判。本文讨论了一些影响你的方案选择的因素,并为你提供了一些指南,帮助你做出一个科学而理智的决定。为了清楚起见,我们将相关的代码和相关消息也作为本文的一部分列示出来。
传输级vs.消息级安全
保障Web服务最常见的一种方法就是使用SSL来保障传输通道的安全。这是Web应用安全的一个自然扩展,在这种情况下,通过使用SSL,HTTPS协议来保障HTTP请求/响应的安全。SOAP/HTTPS属于与HTTPS等同的Web服务。它可以保证程序调用在保密性和安全性方面是安全的。实施SOAP/HTTP协议相对简单,因为多数应用程序服务器只是为HTTPS协议扩展了SSL的授权证书配置。
虽然这种方法可以帮助你快速地建立一个安全方案,它却有着一些你必须要考虑的局限性:
SOAP/HTTPS并没有解决认证的需要。它必须与其它的机制(如Username
Token)相结合,才能处理认证问题。
因为SSL是为整个通道加密,它就为性能带来极大的影响。如果只是消息的局部需要保障安全,可以考虑使用消息级别的安全(Message
Level Security),因为它支持局部的加密和完整性,这显然会改善性能。
SSL是一个点到点(point-to-point)的安全方案,它不适合端到端(end-to-end)的拓扑结构,因为在端到端的结构中,消息需要通过网关等仲裁设备进行传送。
通过MLS(即Message Level
Security,消息级安全),安全限制就可以被运用于消息自身而不是传输通道。Web服务安全标准在近年来被进行了具体的规定和发展,已经涉及到了MLS的应用问题。其中包括大批的标准,如XML-Encryption,
XML-Signature, UsernameToken, Kerberos,
SAML等。这些标准覆盖了不同的技术,在有些情况下这些标准可以进行组合,进一步产生一个综合性的解决方案。在一个使用UsernameToken以求鉴定和认证并且使用SSL以求机密性和完整性的例子中,将MLS
与TLS结合起来是可能的。
为了演示MLS,下面的例子对一个样本性的SOAP/HTTP消息进行了加密。这个作为示例用的是一个简单的计算器服务,其界面包括一个可以接受两个数字的乘法方法,还有一个使用Xfire
and WSS4JJ框架的Java客户端。(笔者在一台使用WebSphere
6.1的主机上测试了Web服务),不过多数应用程序服务器支持基本的SOAP消息安全,因此你可以在另外一台可选的服务器上运行这项服务)。你可以从这儿下载这个示例代码。其中的ReadMe.html文件描述了下载的zip文件的内容。(下一页)
一个简单的示例
这个计算器服务通过执行EJB来备份。当然,你可以有另外的选择,如选择JavaBeans等。下面展示的是EJB的界面:
package com.dev.ws.security.ejb;
public interface Calculator extends javax.ejb.EJBObject {
public float multiply(float a, float b) throws java.rmi.RemoteException;
}
The EJB bean simply implements the multiply method as follows:
package com.dev.ws.security.ejb;
public class CalculatorBean implements javax.ejb.SessionBean {
...
public float multiply(float a, float b){
return a*b;
}
} |
此服务的WSDL显示如下。注意,它使用了一个标准的SOAP/HTTP绑定,并且使这个服务端点在URL中是可用的。
<?xml version="1.0" encoding="UTF-8"?>
<wsdl:definitions ...>
<wsdl:types>
...
<wsdl:portType name="Calculator">
<wsdl:operation name="multiply">
<wsdl:input message="intf:multiplyRequest"name="multiplyRequest"/>
<wsdl:output message="intf:multiplyResponse" name="multiplyResponse"/>
</wsdl:operation>
</wsdl:portType>
<wsdl:binding name="CalculatorSoapBinding" type="intf:Calculator">
<wsaw:UsingAddressing wsdl:required="false"
xmlns:wsaw="http://www.w3.org/2006/02/addressing/wsdl"/>
<wsdlsoap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
<wsdl:operation name="multiply">
<wsdlsoap:operation soapAction="multiply"/>
...
</wsdl:operation>
</wsdl:binding>
<wsdl:service name="CalculatorService">
<wsdl:port binding="intf:CalculatorSoapBinding" name="Calculator">
<wsdlsoap:address
location="http://localhost:9080/WSUTSigEncRouterWeb/services/Calculator"/>
</wsdl:port>
</wsdl:service>
</wsdl:definitions> |
在此示例中,使用Xfire
stub生成器从WSDL创建CalculatorServiceClient。下面的代码使用这个stub来调用web服务的乘法方法,并将参数5和7传递给它:
package com.dev.ws.client.calculator.driver;
...
public class WSClientUTSigEnc {
// Non-SSL URL
public static String UT_ENDPOINT
= "http://localhost:9080/WSUTSigEncRouterWeb/services/Calculator";
public static void main(String[] args) throws MalformedURLException {
CalculatorServiceClient sc = new CalculatorServiceClient();
Calculator calc = sc.getCalculator(UT_ENDPOINT);
float a = 5f;
float b = 7f;
System.out.println(a + " * " + b + " = " + calc.multiply(a,b));
}
After starting the server and running the client, you can use a TCP/IP monitor at port
9080 to observe the messages that the client sends to the web service. You should observe
a SOAP message that looks like this:
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<soap:Body>
<multiply xmlns="http://ejb.security.ws.dev.com">
<a>5.0</a>
<b>7.0</b>
</multiply>
</soap:Body>
</soap:Envelope>
The client outputs the expected result after getting the SOAP response:
5.0 * 7.0 = 35.0 |
上面的SOAP消息只是简单地将方法请求包含进来,并没有安全的报头。为了在SOAP消息的级别上支持加密,应用程序服务器和客户端必须加以配置来支持XML加密。客户端从一个X509的证书使用一个公钥来对SOAP消息加密,而服务器相对应地使用私钥对此消息解密。一对公/私钥必须由给客户端的公钥和给服务器的私钥来生成。(可参考如下的XFire文章来查看如何创建这对密钥的相关细节信息。应用程序服务器的配置与服务器息息相关,因此请参考你的服务器的相关文档资料,以得到XML加密的用法说明。你可以在这里找到本文的WebSphere配置的相关指导。)
(下一页)
加密过程
在XFire框架中,你使用处理程序来处理客户端的SOAP消息。每一个处理程序接受一套属性,这些属性可以规定WSS4J必须实施的消息安全性。为了执行加密,你必须为以下的几个方面规定属性:
客户端公钥的存储位置
加密的运行法则
要进行加密的消息的部分
下面列示计算器服务的客户端加密代码:
protected static void configureEncryption(Properties config)
{
// Encrypt action
config.setProperty(WSHandlerConstants.ACTION, WSHandlerConstants.ENCRYPT);
//The property file describes the public key used for encryption
config.setProperty(WSHandlerConstants.ENC_PROP_FILE,
"com/dev/ws/client/driver/outsecurity_enc.properties");
config.setProperty(WSHandlerConstants.ENCRYPTION_USER, "serveralias");
// The encryption algorithm
config.setProperty(WSHandlerConstants.ENC_SYM_ALGO,WSConstants.TRIPLE_DES);
// Encryption Key Identifier Types
config.setProperty(WSHandlerConstants.ENC_KEY_ID, "SKIKeyIdentifier");
// Encrypt the SOAP body
String bodyPart = "{Content}{}Body";
config.setProperty(WSHandlerConstants.ENCRYPTION_PARTS, bodyPart);
} |
注意:客户端的密钥存储配置包含在属性文件outsecurity_enc.properties中。这个属性文件就如下面的显示一样描述了密钥存储的位置和证书:
org.apache.ws.security.crypto.provider=org.apache.ws.security.components.crypto.Merlin
org.apache.ws.security.crypto.merlin.keystore.type=jks
org.apache.ws.security.crypto.merlin.keystore.password=keystorePass
org.apache.ws.security.crypto.merlin.alias.password=client344Password
org.apache.ws.security.crypto.merlin.keystore.alias=serveralias
org.apache.ws.security.crypto.merlin.file=com/dev/ws/client/driver/clientStore.jks |
一旦客户端和服务器配置完毕,运行客户端就会在TCP/IP监视器上产生如下的SOAP消息:
<soap:Envelope ...>
<soap:Header>
<wsse:Security ...>
<xenc:EncryptedKey ...>
<xenc:EncryptionMethod
Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5" />
<ds:KeyInfo ...>
...
</ds:KeyInfo>
<xenc:CipherData>
<xenc:CipherValue>
M+Tp2Q4ZtBJtpT1q7SkOALNnpv57Cgh/4EHV0gHONtUZsYQLIVYYWNdYqIkjb81pgxBFU94WKQK
au2BEZbF8rL4KdA9tdfb3McRzCOJDcGl4eDs2FC1Pe1Bj0b2VJ+m4D83EhGSsUEeItp+SZcF0Kw
jh5dEcV61Q4cscMZaruSg=
</xenc:CipherValue>
</xenc:CipherData>
</xenc:EncryptedKey>
<xenc:EncryptedData ...>
<xenc:CipherData>
<xenc:CipherValue...>
P8D3xHloRUSCvMA7gNaezLTtENS2R6oXJ8jByaBKvBl5t4joml2qIo9V2LXsnM3nuYJun2UADKfg...
</xenc:CipherValue>
</xenc:CipherData>
</xenc:EncryptedData>
</wsse:Security>
</soap:Header>
<soap:Body>
<xenc:EncryptedData ...>
<xenc:CipherData ...>
<xenc:CipherValue ...>
dIxwIHuC9TCLbSmfsgohBr2A81lY+GfPA7lofgXPcMvcblO+hOVeiKMyxXvuZF8M2fEtmHTa3kVY
fNDYFAKauoDwq4lWBKMuk4f0s8mTkhyBJrMmbD2mrw==
</xenc:CipherValue>
</xenc:CipherData>
</xenc:EncryptedData>
</soap:Body>
</soap:Envelope> |
这个SOAP消息现在已经被加密,并且明文已经被其密码数值所替换。这个SOAP报头包含着加密方法的信息。
本示例演示了一个大体的配置,它需要使用消息级安全(Message Level
Security)来执行加密。虽然消息级安全与TLS相比,在性能和对端到端的安全性的支持方面拥有十分确定的优越性,你必须考虑它本身所带来的额外的复杂性。对于许多应用程序来说,对这种精细的和端到端的消息安全的支持并非关键所在,而TLS就算是一个在保密和加密方面不错的方案了。(下一页)
UsernameToken 身份验证
现在我们来讨论身份验证问题并且讨论一个最简单的方案:UsernameToken身份验证。
UsernameToken 身份验证
UsernameToken
身份验证与HTTP身份验证很相似,此时用户保密信息在消息的头部被传送。UsernameToken被添加到SOAP的头部,可能还会夹杂着一个散列的口令。这是一种加密的简单形式,可以避免无限制地传递口令。应该使用传输级别或者消息级别的加密来保证用户口令的保密性。
通过允许配置一个用于口令验证的LDAP,应用程序服务器支持UsernameToken身份验证。在应用程序服务器收到一个SOAP消息时,在将消息转发给应用程序之前,它会通过LDAP验证用户证书的正确性。
由于其简易性和在应用程序服务器中的广泛支持,UsernameToken身份验证是非常吸引人的。然而,在考虑将它部署到你的应用框架时,你必须清楚其限制条件:
会话状态:
UsernameToken需要用户名和口令在每一个Web服务的调用上都被传递。这暗示着如果客户端经常与该服务会话,客户端必须要对口令进行缓存,而这会导致一种安全风险。
性能:应用程序服务器必须重新验证每一个Web服务调用。而且,因为口令是消息的一部分,那么即使消息并没有包含敏感信息也必须加密。
为了演示UsernameToken身份验证,下面的例子重新使用了计算器服务的例子。你必须配置应用程序服务器以支持UsernameToken的安全,并且指定一个LDAP来验证用户的证书。配置步骤是与特定服务器相关的,因此请参考你的服务器的相关文档。笔者用WebSphere
6.1和活动目录测试了这个例子。
你必须增加一个WSS4J
SOAP处理程序,以支持客户端支持在报头中发送UsernameToken。下面的客户端代码在调用Web服务之前,先调用configureClientHandlers方法:
public static void main(String[] args) throws MalformedURLException {
CalculatorServiceClient sc = new CalculatorServiceClient();
Calculator calc = sc.getCalculator(UT_ENDPOINT);
configureClientHandlers(calc);
float a = 5f;
float b = 7f;
System.out.println(a + " * " + b + " = " + calc.multiply(a,b));
} |
configureClientHandlers方法创建一个新的拥有UsernameToken
属性的WSS4JoutHandler处理程序。下面的configureUsernameToken方法指定了属性:
private static void configureClientHandlers(Object svc) {
Client client=Client.getInstance(svc);
client.addOutHandler(new DOMOutHandler());
Properties properties = new Properties();
// Configure the UsernameToken properties
configureUsernameToken(properties);
client.addOutHandler(new WSS4JOutHandler(properties));
}
protected static void configureUsernameToken(Properties config)
{
// UsernameToken Action
config.setProperty(WSHandlerConstants.ACTION, WSHandlerConstants.USERNAME_TOKEN);
// Clear Text Password
config.setProperty(WSHandlerConstants.PASSWORD_TYPE, WSConstants.PW_TEXT);
// User name to send
config.setProperty(WSHandlerConstants.USER, "johndoe");
// Callback used to retrieve password for given user.
config.setProperty(WSHandlerConstants.PW_CALLBACK_CLASS,
PasswordHandler.class.getName());
} |
(下一页)
用户证书代码
口令从PasswordHandler中重新检索。它将口令存储在一个由用户名索引的地图中。下面是示例样本的用户证书代码:
public class PasswordHandler implements CallbackHandler {
private Map passwords = new HashMap();
public PasswordHandler() {
passwords.put("johndoe", "abc123");
}
...
The following SOAP message is observed by the TCP/IP monitor after running the client:
<soap:Envelope ...>
<soap:Header>
<wsse:Security ...>
<wsse:UsernameToken ...>
<wsse:Username ...>
johndoe
</wsse:Username>
<wsse:Password ...>
abc123
</wsse:Password>
</wsse:UsernameToken>
</wsse:Security>
</soap:Header>
<soap:Body>
<multiply xmlns="http://ejb.security.ws.dev.com">
<a>5.0</a>
<b>7.0</b>
</multiply>
</soap:Body>
</soap:Envelope> |
现在这个SOAP消息的头部部分包含了用户名和口令。服务器对用户证书进行验证,或者许可或者拒绝了对目标Web服务的访问。为了防止对口令的窃听,你必须使用消息级的加密或者以前描述的SSL方法。
一个.net2.0客户端也将一个UsernameToken添加到SOAP/HTTP的头部。为了支持这项功能,你必须在你的Visual
Studio环境中安装WSE
3.0库及其扩展。(你可从这里下载。)你必须像在Java客户端的例子中所做的那样,从WSDL中生成一个客户端的stub。生成的stub看起来会是如下这样子:
...
public partial class CalculatorServiceWse : Microsoft.Web.Services3.WebServicesClientProtocol {
private System.Threading.SendOrPostCallback multiplyOperationCompleted;
private bool useDefaultCredentialsSetExplicitly;
... |
一旦stub被创建,客户端代码就会展现这个stub,并且指定UsernameToken被发送到服务器。下面的c#示例代码演示了如何做到:
static void Main(string[] args)
{
CalculatorServiceWse calc = new CalculatorServiceWse();
calc.Url = "http://localhost:9080/WSUTSigEncRouterWeb/services/Calculator";
//
UsernameToken token = new UsernameToken("johndoe", "abc123",
PasswordOption.SendPlainText);
calc.SetClientCredential(token);
calc.SetPolicy("usernameTokenSecurity");
float result = calc.multiply(7, 5);
Console.WriteLine("Result = " + result);
Console.ReadLine();
} |
运行客户端代码就会生成所期望的输出结果:Result = 35.0(下一页)
基于签名的身份验证
基于签名的身份验证
UsernameToken身份验证适合于客户端身份基于用户的情况,但是你的Web服务客户端可能是一个并不直接代表一个用户的服务器或者设备。可以考虑这样一种情况:一个处理船舶运载的Web服务通过检查定单的交付日期,将定单发送到适当的运载服务供应商。假定客户端是一个消息驱动的服务器定单应用程序,这个应用程序从一个队列中接收消息,并且通过调用运载和账单处理服务来处理定单。在这种情况下,运载服务的身份验证就必须限制为只能给定单应用程序服务器而不是特定的用户。
验证服务器和设备客户端的一种方法是使用XML签名。客户端使用一个私钥来签署SOAP消息,而Web服务使用相应的公钥来验证这个签名。正如在前面的关于加密的示例中所讨论的那样,必须生成一对公/私钥密码。一个密钥存储通常情况下存储着包含服务器公钥的X509证书。图2显示了基于签名的身份验证顺序:
配置XML签名的Web服务仍然是与特定服务器相关的,因此请参考你的服务器的相关文档。
为客户端增加签名需要与配置加密方式一样配置一个WSS4J处理程序。你可以用以前介绍的configureClientHandlers方法为这个处理程序增加代码:
private static void configureClientHandlers(Object svc) {
Client client=Client.getInstance(svc);
client.addOutHandler(new DOMOutHandler());
// Add WSS4J signature Handler
Properties sigProps = new Properties();
configureSignature(sigProps);
client.addOutHandler(new WSS4JOutHandler(sigProps));
} |
configureClientHandlers方法调用configureSignature的目的是指定处理程序的签名属性。其属性包含要签名的局部消息、签名属性文件的位置以及用于在SOAP报头中指定签名的方法:
protected static void configureSignature(Properties config)
{
// Signature Action
config.setProperty(WSHandlerConstants.ACTION, WSHandlerConstants.SIGNATURE );
// Method of specifying the signature in the header
config.setProperty(WSHandlerConstants.SIG_KEY_ID, "DirectReference");
// Signature Property File Location
config.setProperty
(WSHandlerConstants.SIG_PROP_FILE, "com/dev/ws/client/driver/outsecurity_sig.properties");
// Sign the Body part of the message
String bodyPart = "{Content}{}Body";
config.setProperty(WSHandlerConstants.SIGNATURE_PARTS,bodyPart);
} |
签名属性文件outsecurity_sig.properties指定了密钥存储的位置、口令、和签名者的别名:
org.apache.ws.security.crypto.provider=org.apache.ws.security.components.crypto.Merlin
org.apache.ws.security.crypto.merlin.keystore.type=jks
org.apache.ws.security.crypto.merlin.keystore.password=keystorePass
org.apache.ws.security.crypto.merlin.alias.password=client344Password
org.apache.ws.security.crypto.merlin.keystore.alias=client-344-839
org.apache.ws.security.crypto.merlin.file=com/dev/ws/client/driver/clientStore.jks |
通过可用的签名运行计算器的例子会产生如下的SOAP消息:
<soap:Envelope ...>
<soap:Header>
<wsse:Security ...>
<ds:Signature ...>
...
<ds:SignatureValue ...>
Jx/wbsqjTsNZha+JDTCeCtNzUlaXAzWUOjyRCTQE6OQhiwQOpCt0gOd4mxsb5mI9Hrtr+0lSIZOJ
dOHBKTqcCQNmQneM8CI/oeo0RwLSaMXh2fIA+/mZt+EBcS9+WB9Vdv4AnCWYr4/feAxJkioZwzXt
9NquZMnC1nhRTMOoFpM=
</ds:SignatureValue>
...
</ds:Signature>
</wsse:Security>
</soap:Header>
<soap:Body ...>
...
</soap:Body>
<soap:Envelope> |
注意SOAP的头部现在包括了一个SignatureValue成分,该成分包含着签名。服务器使用这个签名来验证SOAP消息的真实性。
基于签名的身份验证是一种用于保障服务安全性的有效方法,这些服务的客户是其它服务器和设备。签名还满足了应用程序对安全性的需要,这些应用程序需要每一项服务处理身份验证的合法证据。管理客户端和服务器证书的需要增加了总体的可维护性,而且必须在规划阶段就加以考虑。你应该清楚,这种方案并没有涉及到J2EE验证模式,因此它仅限于处理身份验证的需要。(下一页)
影响你选择安全方案的主要因素
影响你选择安全方案的主要因素
本文讨论了一些最流行的Web服务安全解决方案,并提供了一些例子用以具体展示其使用方法。本文根据影响你应用程序安全问题的因素评估了每一种方案。下面我们给出在你需要做出决策时应该考虑的主要因素:
保密性和完整性:你需要决定到底你的消息的哪些部分需要保密和完整。消息级的安全(MLS)支持精细的安全性,而SSL以增加性能上的负担为代价可以保障整个消息的安全。
会话状态:如果服务客户不需要保持状态,那么一种简单的身份验证方案,如UsernameToken也就足够了。如果需要保持状态的话,就应该考虑其它的方案,如Kerberos。
拓扑:服务通信可以是点到点的,也可以是涉及到网关等仲裁者的端到端的拓扑。SSL仅支持点到点的结构,而MLS两种都支持。
基础结构:应用程序服务器提供了对WS-*安全性不同程度的支持。因此,你的应用程序服务器支持会影响你对安全方案的选择。
身份验证:应用程序需要各种各样的身份验证,其中包括基于用户的验证、基于签名的验证和联合验证。你需要决定哪一种身份验证模式最适合你的安全性需要。
客户端类型:互联网(Internet)客户端与Intranet(企业内部网)客户端相比,一般都有不同的安全限制。在考虑一个安全方案时,你应该考虑它对你的客户端部署的影响。
性能:安全性往往对应用程序的性能都有负面的影响。SSL比起基于MLS(消息级别的安全)的方案要容易实施,但它会导致更高的性能的损失。
复杂性:在选择一个方案时,复杂性是一个容易被忽略的、可能会对项目有害的因素。MLS可比SSL提供更好的性能,不过,它要以增加开发的复杂性为代价。
网友评论