关于j2ee中webservice的搭建以及不同系统中的访问,我已经在一篇博文《webservice之cxf实现》中进行了介绍,下面我们来谈谈webservice的架构设计要考虑的一些因素。
最首要的因素就是安全性,比如:如果验证调用者的合法身份?如果保证数据传输的安全性?等等。
先来看调用者身份的合法性验证问题。一般情况下ws底层使用http做为传输协议,http本身是无状态的,所以,我们要确保调用者的唯一身份,就要求调用者在调用时,携带身份标识参数。身份标识可以采用用户名加密码的方式实现,webservice标准中ws-security部分已经有了相应的标准。
第二个问题是数据传统过程中的安全性问题,服务器端和客户端如何知道信息的来源可靠、真实。其实这个问题在互联网应用中广泛存在的问题,并不是webservice考虑的范畴,但是,ws底层采用http协议传输,固然存在安全情况的问题(如果仅仅在公司内部局域网中的服务器群中调用,安全性要求就没有这么高了,呵呵)。
http验证信息的准确性,可采用的方式很多,比如:md5签名,时间戳、消息自身加密、https等等,下面我们先来看使用cxf如何对用户验证,并确保信息的安全性。
在服务器端,通过AuthCheckOnServer类对用户名密码进行验证,在配置com.my.webservice.WebServiceFacadeImpl的时候通过拦截器加入AuthCheckOnServer,注意,在action中加了Timestamp,而passwordType采用PasswordDigest,实现密码部分的加密(你可以通过抓包工具观察密码加密效果),服务器上拦截类代码如下:
AuthCheckOnServer.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| public class AuthCheckOnServer implements CallbackHandler {
@Override
public void handle(Callback[] callbackArray) throws IOException,
UnsupportedCallbackException {
if (callbackArray.length > 0) {
WSPasswordCallback pc = (WSPasswordCallback)callbackArray[0];
String userId = pc.getIdentifier();
System.out.println("server得到用户名:" + userId );
String password = "此处根据用户标识userId,通过service(或dao)查询该用户的密码,我略去了...";
//设置好用查出的密码,此处是明文,cxf自动生成密文并进行校验
pc.setPassword(password);
}
}
} |
服务器ws服务类配置如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| <jaxws:endpoint id="myWebService" address="/myWebService"
implementor="com.my.webservice.WebServiceFacadeImpl">
<jaxws:inInterceptors>
<bean class="org.apache.cxf.binding.soap.saaj.SAAJInInterceptor" />
<bean class="org.apache.cxf.ws.security.wss4j.WSS4JInInterceptor">
<constructor-arg>
<map>
<entry key="action" value="Timestamp UsernameToken" />
<entry key="passwordType" value="PasswordDigest" />
<entry key="passwordCallbackClass" value="com.my.webservice.AuthCheckOnServer" />
</map>
</constructor-arg>
</bean>
</jaxws:inInterceptors>
</jaxws:endpoint>
|
在cxf的客户端调用代码中,也加上拦截器,在调用请求中加入用户名及密码相关信息,客户端拦截类代码如下:
AuthPrepareProcesser4Client.java
1
2
3
4
5
6
7
8
9
10
11
12
| public class AuthPrepareProcesser4Client implements CallbackHandler {
@Override
public void handle(Callback[] callbackArray) throws IOException,
UnsupportedCallbackException {
if (callbackArray.length > 0) {
WSPasswordCallback pc = (WSPasswordCallback)callbackArray[0];
pc.setPassword("123");
pc.setIdentifier("yanwawa");
System.out.println("Client setting userName and password OK.");
}
}
}
|
客户端spring的客户端配置代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| <bean id="wsClient" class="com.my.webservice.IWebServiceFacade"
factory-bean="wsClientProxy" factory-method="create" />
<bean id="wsClientProxy" class="org.apache.cxf.jaxws.JaxWsProxyFactoryBean">
<property name="serviceClass" value="com.my.webservice.IWebServiceFacade" />
<property name="address"
value="http://localhost:8088/web_service_server/services/myWebService" />
<property name="inInterceptors">
<list>
<ref bean="logIn" />
</list>
</property>
<property name="outInterceptors">
<list>
<ref bean="logOut" />
<ref bean="saajOut" />
<ref bean="wss4jOut" />
</list>
</property>
</bean>
<bean id="wss4jOut" class="org.apache.cxf.ws.security.wss4j.WSS4JOutInterceptor">
<constructor-arg>
<map>
<entry key="action" value="Timestamp UsernameToken" />
<entry key="user" value="yanwawa" />
<entry key="passwordType" value="PasswordDigest" />
<entry key="passwordCallbackClass" value="com.my.webservice.AuthPrepareProcesser4Client" />
</map>
</constructor-arg>
</bean>
<bean id="logIn" class="org.apache.cxf.interceptor.LoggingInInterceptor" />
<bean id="logOut" class="org.apache.cxf.interceptor.LoggingOutInterceptor" />
<bean id="saajOut" class="org.apache.cxf.binding.soap.saaj.SAAJOutInterceptor" />
|
当然,要以axis的soap客户端调用,代码要复杂一些,会用到WSSecEncrypt等几个类,也就是用这些类来帮助我们生成要发送的消息体,具体实现请参考axis的sample。
在传输过程中,为了确保数据的安全,我们可以在服务器上配置https支持,保证传输中的数据相对安全。
下面我给出另外一种不依赖ws-security的调用方式,当然,我是借鉴网上支付的数字签名的设计方式(网上支付实现请参阅《项目集成第三方支付设计方案》),即完全由程序自己来实现用户身份的验证,这种方案相对来说比较通用。
类图设计如下所示:
webservice sign架构图
在这种设计方案中,ws调用被封装到外观中,由于这种方式需要客户端上传用户id以及验证签名,所以外观类中的所有方法签名都带有这两个参数,似乎有了入侵,但话又说回来,这种方式并不影响service对象的方法设计,而且这种设计更有利于其它非java系统的ws调用。
非核心代码我都略去了,仅贴出客户端调用及服务器端验证的sayHello、add方法代码片断,客户端代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| BeanFactory bf = new ClassPathXmlApplicationContext("classpath:ws-client.xml");
IWebServiceFacade wsClient = (IWebServiceFacade)bf.getBean("wsClient");
String clientId = "1";
String privateKey = "client1_md5_privateKey";
Map reqMap = new HashMap();
reqMap.put("name", "yanwawa");
String sign = SignatureHelper.sign(reqMap, privateKey);
String sayHelloResult = wsClient.sayHello("yanwawa", clientId, sign);
System.out.println(sayHelloResult);
reqMap = new HashMap();
reqMap.put("number1", "9");
reqMap.put("number2", 2);
sign = SignatureHelper.sign(reqMap, privateKey);
int addResult = wsClient.add("9", 2, clientId, sign);
System.out.println(addResult);
|
服务器端代码:
WebServiceFacadeImpl.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| @WebService(endpointInterface="com.my.webservice.IWebServiceFacade")
public class WebServiceFacadeImpl implements IWebServiceFacade {
//业务接口,由spring注入
private IBizService bizService;
@Override
public String sayHello(String name, String clientId, String signStr) {
//auth check
String privateKey = bizService.getClientPrivateKey(clientId);
Map reqMap = new HashMap();
reqMap.put("name", name);
if (SignatureHelper.verifySignCode(reqMap, privateKey, signStr)) {
return bizService.sayHello(name);
} else {
//log and throws Exception
throw new IllegalArgumentException("calling ws method 'sayHello', sign code error!");
}
}
public int add(String number1, int number2, String clientId, String signStr) {
//auth check
String privateKey = bizService.getClientPrivateKey(clientId);
Map reqMap = new HashMap();
reqMap.put("number1", number1);
reqMap.put("number2", number2);
if (SignatureHelper.verifySignCode(reqMap, privateKey, signStr)) {
return bizService.add(number1, number2);
} else {
//log and throws Exception
throw new IllegalArgumentException("calling ws method 'add', sign code error!");
}
}
//……………………….其它代码略
|
总的来说,用第一种用户名加密码的方式确保用户的每一次调用的合法性在cxf中是不错的选择,因为它是在拦截器加入用户名及密码信息,服务器端的验证也是在拦截器中做的,因此,这种方式的代码看起来更简洁,可读性更好。
第二种方式类似于的线支付的签名验证方式,比较通用,但是方法签名都要携带验证信息,看起来比较累赘,但由于http的无状态性,身份标识每次都提交到服务器是一种比较简单的方法。第二种方式还有一个问题需要注意,就是客户端也要通过签名验证返回消息的合法性,
当然,任何东西都有两面性,好比一把“双刃剑”,我们只有根据项目的实际需求,平衡各种因素,扬长避短,选择最适合、最实用的一种方式,解决问题才是最终的目的。