最近需要对外提供 web service 服务,初步调研后决定采用 CXF 进行开发。CXF 是 apache 提供的开源框架,对 JAX-WS 标准的实现较为完整,并提供了与 spring 框架的集成,功能较为丰富。由于涉及到在 SOAP 头中传输身份验证信息,本文主要讲解如何通过 CXF 提供的 interceptor 来处理 SOAP 头中的数据。实际上, JAX-WS 规范的 Handler 也可以进行类似消息的处理,由于是标准中的定义,因此具有较强的通用性。但是在大部分情况下开发者其实并不太关注程序的可移植性,而且 CXF 的 interceptor 提供了更为丰富的功能,因此使用上较为广泛。相关 jar 包可以在这里下载,里面包含了 CXF 及其所依赖的各 jar 包。
服务端
下面是一个简单的 SEI(Service Endpoint Inteface),已经使用 JAX-WS 定义的标注将其暴露为一个 web service 接口:
@WebService(targetNamespace = "http://services.devsumo.com/cxfMessenger/v001")
@XmlSeeAlso(AuthHeader.class)
public interface CXFMessenger {
@WebMethod
String sendMessage(
@WebParam(name="recipient") String recipient,
@WebParam(name="messageContent") String messageContent);
}
这里每一个请求都需要传入用户的用户名、密码进行校验,我们新建一个 AuthHeader 类:
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "AuthHeader")
public class AuthHeader {
protected String username;
protected String password;
public String getusername() {
return username;
}
public void setusername(String value) {
this.username = value;
}
public String getpassword() {
return password;
}
public void setpassword(String value) {
this.password = value;
}
}
CXF 自己已经提供了许多功能强大的 Interceptor 以便开发者使用,不过这里我们使用 AbstracSoapInterceptor 类作为我们的基础类。AbstractSoapInterceptor 是其他 Interceptor 类的基类,为我们提供了基本的代码模板:
public class AuthInterceptor extends AbstractSoapInterceptor {
public AuthInterceptor() {
super(Phase.PRE_INVOKE);
}
@Override
public void handleMessage(SoapMessage message) throws Fault {
// TODO: Implement our header handling
}
}
这里我们有一个简单的 handlerMessage 函数需要实现。但在此之前,我们首先来关注填一下 Phase。在 CXF 中,各个 Interceptor 组成了 Interceptor Chains,CXF 将它们划分为几个阶段,用于精细化的控制各 interceptor 的执行时间。一个 interceptor 可以在请求刚刚被接收时执行,也就是说早于任何其他的解析或者编解码,这种情况比较适合端对端的性能监控。也可以让它在服务方法即将被调用时执行。后一种情况就是我们要使用的,在这里将 Phase 定义为 Phase.PRE_INVOKE。
handleMessage 的参数 SoapMessage 类有一个十分便捷的方法: getHeader ,其参数是一个 QName 对象,如果存在匹配此 QName 对象的 Header 的话则将其返回:
public class AuthInterceptor extends AbstractSoapInterceptor {
private static final QName HEADER_TYPE =
new QName("http://services.devsumo.com/cxfMessenger/v005",
"AuthHeader");
public AuthInterceptor() {
super(Phase.PRE_INVOKE);
}
@Override
public void handleMessage(SoapMessage message) throws Fault {
Header header = message.getHeader(HEADER_TYPE);
if(header != null) {
// TODO: process our header
}
}
}
通过 Header 得到我们需要的数据不需要我们自己进行编解码,通过相关的 DataBinding,CXF 可以自动帮我们完成。得到所需数据后,就可以进行校验了:
@Override
public void handleMessage(SoapMessage message) throws Fault {
Header header = message.getHeader(HEADER_TYPE);
if (header == null)
throws new Exception("无身份信息");
Service service =
ServiceModelUtil.getService(message.getExchange());
DataReader<Node> dataReader =
service.getDataBinding().createReader(Node.class);
AuthHeader AuthHeader =
(AuthHeader)dataReader.read(HEADER_TYPE,
(Node)header.getObject(), AuthHeader.class);
if (!AuthHeader.getUsername.equals("hello")) {
throws new Exception("用户无权限");
}
if (!AuthHeader.getPassword.equals("world")) {
throws new Exception("密码错误");
}
}
最后我们需要在 CXF 中对 interceptor 进行注册。Bus 为 CXF 的骨架类,提供了 CXF 运行时的资源共享,下面的配置使得此 Interceptor 对所有的服务接口生效:
<cxf:bus>
<cxf:inInterceptors>
<bean id="authInterceptor"
class="com.devsumo.cxfmessenger.v005.AuthInterceptor"/>
</cxf:inInterceptors>
</cxf:bus>
客户端
客户端可以使用如下代码将在请求信息中添加 Soap Header:
ApplicationContext cxt = new ClassPathXmlApplicationContext("classpath:applicationContext-client.xml");
CustomerService client = (CustomerService)cxt.getBean("customerServiceClient");
AuthHeader authHeader = new AuthHeader();
authHeader.setUsername("hello");
authHeader.setPassword("world");
Header header = new Header(new QName("http://services.devsumo.com/cxfMessenger/v001", "AuthHeader"),
authHeader, new JAXBDataBinding(AuthHeader.class));
List<Header> headers = new ArrayList<Header>();
headers.add(header);
((BindingProvider)client).getRequestContext().put(Header.HEADER_LIST, headers);
其中 Spring 配置文件内容如下。注意 serviceClass 属性的值为 SEI 接口的全限定名,address 属性的值为上述 WS 服务对外开放的 url。
<!-- 在客户端中,此接口类所在package可按需要放置 -->
<bean id="messenger" class="api.client.CXFMessenger"
factory-bean="messengerClientFactory" factory-method="create"/>
<bean id="messengerClientFactory" class="org.apache.cxf.jaxws.JaxWsProxyFactoryBean">
<property name="serviceClass" value="api.client.CXFMessenger"/>
<property name="address" value="http://services.devsumo.com/cxfMessenger/v001/messenger"/>
</bean>