Webservice架构设计

来源:互联网 发布:淘宝做单一个月挣多少 编辑:程序博客网 时间:2024/06/06 19:08

关于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

1234567891011121314
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服务类配置如下:

123456789101112131415
<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
123456789101112
public class AuthPrepareProcesser4Client implements CallbackHandler {@Overridepublic 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的客户端配置代码如下:

1234567891011121314151617181920212223242526272829303132
<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方法代码片断,客户端代码:

123456789101112131415
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
12345678910111213141516171819202122232425262728293031
@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的无状态性,身份标识每次都提交到服务器是一种比较简单的方法。第二种方式还有一个问题需要注意,就是客户端也要通过签名验证返回消息的合法性,

  当然,任何东西都有两面性,好比一把“双刃剑”,我们只有根据项目的实际需求,平衡各种因素,扬长避短,选择最适合、最实用的一种方式,解决问题才是最终的目的。


0 0