项目中需要用到CXF做WS处理,花点时间对其有个简单认识,主要是在安全认证以及日志记录和异常处理这块有要求控制。
安全认证采用的是WSS4J,日志记录和异常处理采用拦截器控制,
资源下载:客户端和服务端都点这
服务端
整体架构.
至于webservice的配置可以参考其他文档,
服务接口
package com.cxfdemo.ws.service; import java.util.List; import javax.jws.WebParam; import javax.jws.WebResult; import javax.jws.WebService; import com.cxfdemo.ws.service.model.Resume; import com.cxfdemo.ws.service.model.User; @WebService public interface HelloWorld { @WebResult(name = "String") public String sayHi(@WebParam(name="text")String text); @WebResult(name = "user") public User getUser(@WebParam(name="id")String id); public List<User> getAllUsers(); public void saveUser(@WebParam(name="id")String id, @WebParam(name="name")String name, @WebParam(name="sex")int sex); /** * 客户端的ObjectFactory的createUser方法的参数必须和User的构造函数的参数一致 * <p> * 如客户端中需要用到new User("id","name",1)构造User对象时 * * 需要在ObjectFactory中加入 * public User createUser(String id,String name,int sex) { * return new User(id,name,sex); * } * </p> * @param user */ public void saveUsers(@WebParam(name="user")User user); @WebResult(name = "String") public String saveResumes(@WebParam(name="resume")Resume resume); }
服务实现
package com.cxfdemo.ws.service; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.List; import javax.activation.DataHandler; import javax.jws.WebService; import javax.xml.bind.annotation.XmlMimeType; import com.cxfdemo.ws.service.model.Resume; import com.cxfdemo.ws.service.model.User; @WebService(endpointInterface="com.cxfdemo.ws.service.HelloWorld", serviceName="HelloWorldServiceService",portName="HelloWorldServicePort", name="HelloWorldService",targetNamespace="http://service.ws.cxfdemo.com/") public class HelloWorldService implements HelloWorld{ public String sayHi(String text) { return "Hello " + text; } public User getUser(String id) { return new User("大王", id, 1); } public List<User> getAllUsers() { List<User> list = new ArrayList<User>(); for (int i=0;i<4;i++) { list.add(new User("小明"+i,""+i,i)); } return list; } public void saveUser(String id, String name, int sex) { System.out.println(new User(id, name, sex)); } public void saveUsers(User user) { System.out.println(user); } public String saveResumes(@XmlMimeType("application/octet-stream")Resume resume) { if (resume == null) { throw new NullPointerException("参数非法."); } DataHandler handler = resume.getDataHandler(); if (handler == null) { throw new NullPointerException("参数非法."); } OutputStream os = null; InputStream is = null; try { is = handler.getInputStream(); os = new FileOutputStream(new File("D:\\" + resume.getCandidateName() + "." + resume.getResumeFileType())); byte[] b = new byte[100000]; int bytesRead = 0; while ((bytesRead = is.read(b)) != -1) { os.write(b, 0, bytesRead); } os.flush(); } catch (IOException e) { e.printStackTrace(); } finally { try { if (os != null) { os.close(); } } catch (IOException e) { e.printStackTrace(); } try { if (is != null) { is.close(); } } catch (IOException e) { e.printStackTrace(); } } return "ok"; } }
其中关于文件传输采用二进制方式.
有2点是必须配置的。
1.传输对象中含有DataHandler 属性
@XmlMimeType("application/octet-stream")
private DataHandler dataHandler;
2.服务发布时协议规定
<!-- 文件传送必须协议 -->
<jaxws:properties>
<entry key="mtom-enabled" value="true"/>
</jaxws:properties>
spring配置
<import resource="classpath:META-INF/cxf/cxf.xml" /> <import resource="classpath:META-INF/cxf/cxf-extension-soap.xml" /> <import resource="classpath:META-INF/cxf/cxf-servlet.xml" /> <!-- 设置密码bean --> <bean id="serverPasswordCallback" class="com.cxfdemo.ws.service.ServerPasswordCallback"></bean> <!-- WSS4J密码校验 --> <bean id="wss4jInInterceptor" class="org.apache.cxf.ws.security.wss4j.WSS4JInInterceptor"> <constructor-arg> <map> <!-- 用户认证(明文密码) --> <entry key="action" value="UsernameToken" /> <entry key="passwordType" value="PasswordText" /><!-- 明文 密文采用PasswordDigest--> <entry key="passwordCallbackRef" value-ref="serverPasswordCallback" /> </map> </constructor-arg> </bean> <!-- 发布服务 --> <jaxws:endpoint id="helloWorld" address="/helloWorld" implementor="com.cxfdemo.ws.service.HelloWorldService"> <!-- 文件传送必须协议 --> <jaxws:properties> <entry key="mtom-enabled" value="true"/> </jaxws:properties> <!-- 输入拦截器 --> <jaxws:inInterceptors> <ref bean="wss4jInInterceptor" /> <!-- 日志打印 --> <bean class="org.apache.cxf.interceptor.LoggingInInterceptor" /> </jaxws:inInterceptors> <!-- 正常输出拦截器 --> <jaxws:outInterceptors> <bean class="com.cxfdemo.ws.service.interceptor.ErrorHandlerInterceptor"></bean> </jaxws:outInterceptors> <!-- 错误输出拦截器 --> <jaxws:outFaultInterceptors> <bean class="com.cxfdemo.ws.service.interceptor.ErrorHandlerInterceptor"></bean> </jaxws:outFaultInterceptors> </jaxws:endpoint> <jaxws:endpoint id="updateFile" address="/updateFile" implementor="com.cxfdemo.ws.service.UpdateFileService"> <jaxws:properties> <entry key="mtom-enabled" value="true" /> </jaxws:properties> </jaxws:endpoint> <!-- 全局配置 --> <!-- <cxf:bus> <cxf:features> <cxf:logging /> </cxf:features> </cxf:bus> -->
采用的是jaxws:endpoint发布服务,至于其他方式,这里就不描述了,关于WSS4J的文章,网上也有很多。
服务端CallbackHandler配置
public class ServerPasswordCallback implements CallbackHandler { private static final Map<String, String> userMap = new HashMap<String, String>(); static { userMap.put("client", "clientpass"); userMap.put("server", "serverpass"); } public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { WSPasswordCallback callback = (WSPasswordCallback) callbacks[0]; //实际上 callback.getPassword()是为null String clientUsername = callback.getIdentifier(); //其实这一步是应该从数据库中取密码在设置到callback中 String serverPassword = userMap.get(clientUsername); if (serverPassword != null) { callback.setPassword(serverPassword); } try { System.out.println(new Date()); Thread.sleep(1000L); } catch (InterruptedException e) { //nothing. } } }
网上很多文章中都是在这个handle里去校验密码是否匹配的,其实不然,你会发现 callback.getPassword()是为null的。
其实密码验证的工作是交给了WSS4JInInterceptor来处理的。
下面是一次错误密码的异常信息。
从异常堆栈中看出密码校验在UsernameTokenValidator中处理。源码如下。
/** * Verify a UsernameToken containing a password digest. It does this by querying a * CallbackHandler instance to obtain a password for the given username, and then comparing * it against the received password. * @param usernameToken The UsernameToken instance to verify * @throws WSSecurityException on a failed authentication. */ protected void verifyDigestPassword(UsernameToken usernameToken, RequestData data) throws WSSecurityException { if (data.getCallbackHandler() == null) { throw new WSSecurityException(WSSecurityException.FAILURE, "noCallback"); } String user = usernameToken.getName(); String password = usernameToken.getPassword(); String nonce = usernameToken.getNonce(); String createdTime = usernameToken.getCreated(); String pwType = usernameToken.getPasswordType(); boolean passwordsAreEncoded = usernameToken.getPasswordsAreEncoded(); WSPasswordCallback pwCb = new WSPasswordCallback(user, null, pwType, WSPasswordCallback.USERNAME_TOKEN, data); try { data.getCallbackHandler().handle(new Callback[]{pwCb}); } catch (IOException e) { if (log.isDebugEnabled()) { log.debug(e); } throw new WSSecurityException( WSSecurityException.FAILED_AUTHENTICATION, null, null, e ); } catch (UnsupportedCallbackException e) { if (log.isDebugEnabled()) { log.debug(e); } throw new WSSecurityException( WSSecurityException.FAILED_AUTHENTICATION, null, null, e ); } String origPassword = pwCb.getPassword(); if (origPassword == null) { if (log.isDebugEnabled()) { log.debug("Callback supplied no password for: " + user); } throw new WSSecurityException(WSSecurityException.FAILED_AUTHENTICATION); } if (usernameToken.isHashed()) { String passDigest; if (passwordsAreEncoded) { passDigest = UsernameToken.doPasswordDigest(nonce, createdTime, Base64.decode(origPassword)); } else { passDigest = UsernameToken.doPasswordDigest(nonce, createdTime, origPassword); } if (!passDigest.equals(password)) { throw new WSSecurityException(WSSecurityException.FAILED_AUTHENTICATION); } } else { if (!origPassword.equals(password)) { throw new WSSecurityException(WSSecurityException.FAILED_AUTHENTICATION); } } }
日志记录拦截器
public class ErrorHandlerInterceptor extends AbstractSoapInterceptor { public ErrorHandlerInterceptor() { super(Phase.MARSHAL); } public void handleMessage(SoapMessage message) throws Fault { // 错误原因 Fault fault = (Fault) message.getContent(Exception.class); // 错误信息 String errorMessage = null; Throwable cause = null; if (fault != null) { errorMessage = fault.getMessage(); cause = fault.getCause(); } Exchange exchange = message.getExchange(); // wsdl描述 String servicePath = null; // url与uri String url = null; String uri = null; // 客户端ip String clientIp = null; // 用户名 String username = null; // 密码 String psw = null; // 服务中的方法 String methodName = null; // 参数名 Object[] paramNames = null; // 参数值 Object[] paramValues = null; if (exchange != null) { Object object = exchange.get("javax.xml.ws.wsdl.description"); if (object != null) { servicePath = object.toString(); } Message inMessage = exchange.getInMessage(); if (inMessage != null) { HttpServletRequest req = (HttpServletRequest) inMessage .get("HTTP.REQUEST"); clientIp = getIpAddr(req); url = (String) inMessage.get("org.apache.cxf.request.url"); uri = (String) inMessage.get("org.apache.cxf.request.uri"); } W3CDOMStreamWriter w3CDOMStreamWriter = (W3CDOMStreamWriter) inMessage .get(W3CDOMStreamWriter.class.getName()); if (w3CDOMStreamWriter != null) { Document document = w3CDOMStreamWriter.getDocument(); if (document != null) { NodeList usernames = document .getElementsByTagName("wsse:Username"); NodeList psws = document .getElementsByTagName("wsse:Password"); if (usernames != null && usernames.getLength() == 1) { username = usernames.item(0).getTextContent(); } if (psws != null && psws.getLength() == 1) { psw = psws.item(0).getTextContent(); } NodeList body = document.getElementsByTagName("soap:Body"); Node method = body.item(0).getFirstChild(); if (method != null) { methodName = method.getNodeName(); String[] methods = methodName.split(":"); if (methods != null && methods.length == 2) { methodName = methods[1]; } NodeList args = method.getChildNodes(); paramNames = getParam(args, true); paramValues = getParam(args, false); } } } } System.out.println(cause); System.out.println(errorMessage); System.out.println(servicePath); System.out.println(url); System.out.println(uri); System.out.println(clientIp); System.out.println(username); System.out.println(psw); System.out.println(methodName); System.out.println("---names--"); System.out.print(getParam(paramNames) + "\t"); System.out.println("\n---values--"); System.out.print(getParam(paramValues) + "\t"); }
其中拦截获取诸多信息,可作为日志记录入库,含错误原因、错误信息、wsdl描述、url与uri、客户端ip、用户名、密码、服务中的方法、 参数名和参数值
可根据是否含有错误原因或错误信息分辨出当前此次服务请求成功与否。
一次密码错误的日志信息
请求信息为:
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><SOAP-ENV:Header xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"><wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" soap:mustUnderstand="1"><wsse:UsernameToken wsu:Id="UsernameToken-CF676781B956C77EB214127352680991"><wsse:Username>client</wsse:Username><wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">clientpass1</wsse:Password></wsse:UsernameToken></wsse:Security></SOAP-ENV:Header><soap:Body><ns2:saveUsers
xmlns:ns2="http://service.ws.cxfdemo.com/"><user><collections>11dfdfd</collections><collections>22dfdfd</collections><collections>33dfdfd</collections><id>id</id><map><entry><key>44</key><value>333</value></entry><entry><key>55</key><value>333</value></entry><entry><key>22</key><value>333</value></entry><entry><key>33</key><value>333</value></entry><entry><key>11</key><value>333</value></entry></map><name>name</name><sex>2</sex></user></ns2:saveUsers></soap:Body></soap:Envelope>
soapUI配置
Eclipse的插件地址:http://www.soapui.org/eclipse/update
客服端
采用wsdl2java 生成的客户端。
密码也是用WSS4JOutInterceptor拦截设置的。
public class ClientPasswordCallback implements CallbackHandler { public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { WSPasswordCallback callback = (WSPasswordCallback) callbacks[0]; System.out.println(new Date()); try { Thread.sleep(1000L); } catch (InterruptedException e) { e.printStackTrace(); } callback.setPassword("clientpass"); } }
可以采用
spring配置
<import resource="classpath:META-INF/cxf/cxf.xml" /> <import resource="classpath:META-INF/cxf/cxf-extension-soap.xml" /> <import resource="classpath:META-INF/cxf/cxf-servlet.xml" /> <bean id="clientPasswordCallback" class="com.cxfdemo.ws.client.ClientPasswordCallback"></bean> <bean id="wss4jOutInterceptor" class="org.apache.cxf.ws.security.wss4j.WSS4JOutInterceptor"> <constructor-arg> <map> <!-- 用户认证(明文密码) --> <entry key="action" value="UsernameToken"/> <entry key="user" value="client"/> <entry key="passwordType" value="PasswordText"/> <entry key="passwordCallbackRef" value-ref="clientPasswordCallback"/> </map> </constructor-arg> </bean> <jaxws:client id="client" address="http://localhost:8888/CXFDemo/webservice/helloWorld" serviceClass="com.cxfdemo.ws.service.HelloWorld"> <jaxws:inInterceptors> <bean class="org.apache.cxf.interceptor.LoggingInInterceptor" /> </jaxws:inInterceptors> <jaxws:outInterceptors> <bean class="org.apache.cxf.interceptor.LoggingOutInterceptor" /> <ref bean="wss4jOutInterceptor"/> </jaxws:outInterceptors> </jaxws:client> <jaxws:client id="updateFile" address="http://localhost:8888/CXFDemo/webservice/updateFile" serviceClass="com.cxfdemo.ws.service.HelloWorld"> </jaxws:client> <!-- 对所有的服务配置超时机制 只对服务名为{http://service.ws.cxfdemo.com/}HelloWorldService的服务生效. --> <http-conf:conduit name="*.http-conduit"> <!-- ConnectionTimeout获取连接超时 ReceiveTimeout获取结果超时--> <http-conf:client ConnectionTimeout="15000" ReceiveTimeout="30000"/> </http-conf:conduit>
ApplicationContext context = new ClassPathXmlApplicationContext("client_Spring.xml"); HelloWorld helloService = context.getBean("client",HelloWorld.class); String response = helloService.sayHi("Peter"); System.out.println(response);