CXF:构建安全的webservice服务
主要讲解两种方式:
1、基于WS-Security的安全认证
加入依赖:
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-ws-security</artifactId>
<version>2.7.0</version>
</dependency>
可能会遇到的异常:encache时日志不能正确
java.lang.IllegalStateException: org.slf4j.LoggerFactory could not be successfully initialized. See also http://www.slf4j.org/codes.html#unsuccessfulInit
org.slf4j.LoggerFactory.getILoggerFactory(LoggerFactory.java:282)
org.slf4j.LoggerFactory.getLogger(LoggerFactory.java:248)
org.slf4j.LoggerFactory.getLogger(LoggerFactory.java:261)
net.sf.ehcache.CacheManager.<clinit>(CacheManager.java:125)
org.apache.cxf.ws.security.cache.EHCacheManagerHolder.getCacheManager(EHCacheManagerHolder.java:76)
在cxf2.7.0中用的1.5.8报错修改版本:
<dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.6.1</version> </dependency>
CXF对WS-Security的实现还是采用Interceptor的方式,我们在需要调用的地方构造WSS4JInInterceptor的实例。
1、server端配置:
<jaxws:server id="cxfSecurityService" serviceClass="org.ws.server.cxf.chap2.CXFSecurityService" address="/cxfSecurityService"> <jaxws:serviceBean> <bean class="org.ws.server.cxf.chap2.impl.CXFSecurityServiceImpl" /> </jaxws:serviceBean> <jaxws:inInterceptors> <bean class="org.apache.cxf.interceptor.LoggingInInterceptor" /> <bean class="org.apache.cxf.ws.security.wss4j.WSS4JInInterceptor"> <constructor-arg> <map> <entry key="action" value="UsernameToken" /> <entry key="passwordType" value="PasswordText" /> <entry key="user" value="admin" /> <entry key="passwordCallbackRef"> <ref bean="serverPasswordCallback" /> </entry> </map> </constructor-arg> </bean> </jaxws:inInterceptors> <jaxws:outInterceptors> <bean class="org.apache.cxf.interceptor.LoggingOutInterceptor" /> </jaxws:outInterceptors> </jaxws:server>
<bean id="serverPasswordCallback" class="org.ws.server.cxf.chap2.interceptor.ServerAuthCallBack"> <property name="username" value="admin" /> <property name="password" value="admin123" /> </bean>
作为服务端,这里对传入参数加入了WSS4JInInterceptor,这里构造函数传入Map关注下这四个参数的含义:
- action:UsernameToken 指使用用户名令牌
- passwordType: PasswordText采用UsernameToken的加密策略,默认为 WSConstants.PW_DIGEST,即PasswordDigest。这里直接文本
- passwordCallbackRef:指定获取对象password的方式,需要实现CallbackHandler
下面来看看passwordCallbackRef服务端的校验:
public class ServerAuthCallBack implements CallbackHandler {
private String username;
private String password;
@Override
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
WSPasswordCallback pc = (WSPasswordCallback) callbacks[0];
String identifier = pc.getIdentifier();
if (!StringUtils.isEmpty(getUsername()) && !StringUtils.isEmpty(getPassword())) {
if (getUsername().equals(identifier)) {
pc.setPassword(getPassword());
}
} else {
throw new SecurityException("验证失败");
}
}
//getter/setter
}
对于CXF2.3.X(包括2.3.X)以下的版本在校验时需要这样:
public class ServerPasswordCallback implements CallbackHandler {
public void handle(Callback[] callbacks) throws IOException,
UnsupportedCallbackException {
WSPasswordCallback pc = (WSPasswordCallback) callbacks[0];
if (pc.getIdentifer().equals("joe") {
if (!pc.getPassword().equals("password")) {
throw new IOException("wrong password");
}
}
}
}
更多信息见这里
2、客户端校验
与服务端类似,需要在配置文件的outinterceptors加入
<jaxws:client id="cxfSecurityClient" address="http://localhost:8080/webservice/service/cxfSecurityService" serviceClass="org.sample.ws.client.cxf.chap3.CXFSecurityService"> <jaxws:inInterceptors> <bean class="org.apache.cxf.interceptor.LoggingInInterceptor" /> </jaxws:inInterceptors> <jaxws:outInterceptors> <bean class="org.apache.cxf.interceptor.LoggingOutInterceptor" /> <bean class="org.apache.cxf.ws.security.wss4j.WSS4JOutInterceptor"> <constructor-arg> <map> <entry key="action" value="UsernameToken" /> <entry key="passwordType" value="PasswordText" /> <entry key="user" value="admin" /> <entry key="passwordCallbackRef"> <ref bean="clientPasswordCallback" /> </entry> </map> </constructor-arg> </bean> </jaxws:outInterceptors> </jaxws:client>
而客户端只需要将注入的username和password设置到相应的属性即可:
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
WSPasswordCallback wsPasswordCallBack = (WSPasswordCallback) callbacks[0];
wsPasswordCallBack.setIdentifier(getUsername());
wsPasswordCallBack.setPassword(getPassword());
}
2、Intercepter
另一种思路,之前在介绍JAX-WS的时候提供了一种思路,在SOAP的head中添加QName节点,加入用户信息这里同样也可以采用这样的方式来实现:
public class SecuritySOAPHeaderIntercepter extends AbstractSoapInterceptor {
private String qName;
private String key;
private String token;
private String tokenValue;
public SecuritySOAPHeaderIntercepter() {
super(Phase.WRITE);
}
public void handleMessage(SoapMessage message) throws Fault {
List<Header> headers = message.getHeaders();
headers.add(getHeader());
}
private Header getHeader() {
QName qName = new QName(getqName(), getKey());
Document document = DOMUtils.createDocument();
Element element = document.createElementNS(getqName(), getKey());
Element token = document.createElement(getToken());
token.setTextContent(getTokenValue());
element.appendChild(token);
SoapHeader header = new SoapHeader(qName, element);
return (header);
}
//getter/setter
}
对上面的代码我们需要关注两点:
1、指定该拦截器的执行阶段,需要在构造函数中指定,这里是写入阶段Phase.WRITE
2、实现自定义的拦截器主要需要实现handleMessage,这里是在SOAP的header中加入qname为节点的元素,具体生产的格式如下:
<soap:Header><user_admin xmlns="http://org.webservice.cxf.sample"><user_token>1234567</user_token></user_admin></soap:Header>
当然,在实际的业务中可自行构造该节点格式。所需要注意的就是在server端按照该节点来解析获取相应的值来判断即可。如针对上述生成的header文件,我们可以通过解析user_admin,namespace,user_token,value等值来作为校验的依据
对客户端配置,只需要将该拦截器作为输出拦截器链中
<!-- 基于SOAPinterceptor --> <jaxws:client id="cxfHeaderSecurityClient" address="http://localhost:8080/webservice/service/cxfHeaderSecurityService" serviceClass="org.sample.ws.client.cxf.chap4.CXFSecurityService"> <jaxws:inInterceptors> <bean class="org.apache.cxf.interceptor.LoggingInInterceptor" /> </jaxws:inInterceptors> <jaxws:outInterceptors> <bean class="org.apache.cxf.interceptor.LoggingOutInterceptor" /> <bean class="org.sample.ws.client.cxf.chap4.interceptor.SecuritySOAPHeaderIntercepter"> <property name="qName" value="http://org.webservice.cxf.sample" /> <property name="key" value="user_admin" /> <property name="token" value="user_token" /> <property name="tokenValue" value="1234567" /> </bean> </jaxws:outInterceptors> </jaxws:client>
接下来看看服务端配置,根据上述的SOAP协议header部分,我们需要做的就是解析该header获取相应的值作为检验的依据:
public class SecuritySOAPHeaderIntercepter extends AbstractSoapInterceptor {
private static final Logger LOG = Logger.getLogger(SecuritySOAPHeaderIntercepter.class.getName());
private String qName;
private String key;
private String token;
private String tokenValue;
public SecuritySOAPHeaderIntercepter() {
super(Phase.PRE_LOGICAL);//这里指定在拦截器链中的执行阶段为PRE_PROTOCOL
}
@Override
public void handleMessage(SoapMessage message) throws Fault {
List<Header> headers = message.getHeaders();
boolean authorized = false;
if (null != headers && !headers.isEmpty()) {
for (Header header : headers) {
QName qName = header.getName();
if (getKey().equals(qName.getLocalPart()) && getqName().equals(qName.getNamespaceURI())) {
Element element = (Element) header.getObject();
NodeList nodeList = element.getChildNodes();
for (int i = 0; i < nodeList.getLength(); i++) {
Node node = nodeList.item(i);
if (getToken().equals(node.getNodeName())
&& getTokenValue().equals(node.getFirstChild().getNodeValue())) {
authorized = true;
break;
}
}
}
}
}
if (!authorized) {
throw new Fault("authorized error", LOG);
}
}
//getter/setter
}
在handleMessage方法中通过解析SOAP中header来达到授权的目的。
指定了在拦截器链中的执行阶段,这里是Phase.PRE_PROTOCOL。更多的执行阶段见下面:
或者这里http://cxf.apache.org/docs/interceptors.html
<!-- 采用soapHeader的方式 --> <jaxws:server id="cxfHeaderSecurityService" serviceClass="org.ws.server.cxf.chap2.CXFSecurityService" address="/cxfHeaderSecurityService"> <jaxws:serviceBean> <bean class="org.ws.server.cxf.chap2.impl.CXFSecurityServiceImpl" /> </jaxws:serviceBean> <jaxws:inInterceptors> <bean class="org.apache.cxf.interceptor.LoggingInInterceptor" /> <bean class="org.ws.server.cxf.chap2.interceptor.AnotherInterceptor"/> <bean class="org.ws.server.cxf.chap2.interceptor.SecuritySOAPHeaderIntercepter"> <property name="qName" value="http://org.webservice.cxf.sample" /> <property name="key" value="user_admin" /> <property name="token" value="user_token" /> <property name="tokenValue" value="123456" /> </bean> </jaxws:inInterceptors> <jaxws:outInterceptors> <bean class="org.apache.cxf.interceptor.LoggingOutInterceptor" /> </jaxws:outInterceptors> </jaxws:server>