前言
之前因为一些原因,需要搭建webservice接口进行测试。发现网上的博客写得好的很多,写的乱七八糟的也不少,而且搜索排名还能很靠前。 这里对自己搭建的webservice接口进行一下总结和记录,方便以后再用到时做个参考。
摘要
这篇博文主要展示了spring boot框架下,使用cxf实现有用户名密码验证的webservice接口的demo。不是生产环境下的代码。另外,文中会提及一些开发中遇到的坑。
备注
jar包版本见jar包依赖,这里不做赘述。具体实现代码可以在下文中看到。
DEMO
下面上代码:
一、jar包依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.9.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.liuy</groupId>
<artifactId>ws</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>ws</name>
<description>Demo project for webservice</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-spring-boot-starter-jaxws</artifactId>
<version>3.2.8</version>
</dependency>
<dependency>
<groupId>org.springframework.ws</groupId>
<artifactId>spring-ws-security</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.18.Final</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.56</version>
</dependency>
<dependency>
<groupId>org.apache.ws.security</groupId>
<artifactId>wss4j</artifactId>
<version>1.6.19</version>
</dependency>
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-ws-security</artifactId>
<version>3.2.8</version>
</dependency>
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-frontend-jaxws</artifactId>
<version>3.2.8</version>
</dependency>
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-transports-http</artifactId>
<version>3.2.8</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
二、webservice接口和实现
1、接口
package com.liuy.ws.service;
import com.alibaba.fastjson.JSONObject;
import javax.jws.WebMethod;
import javax.jws.WebParam;
import javax.jws.WebService;
@WebService(name = "helloService", targetNamespace = "http://service.ws.liuy.com/")
public interface HelloService {
@WebMethod
String sayHello(@WebParam(name = "msg") String msg);
@WebMethod
String getUser(@WebParam(name = "msg") String msg);
@WebMethod
JSONObject getJson(@WebParam(name = "msg") String msg);
}
package com.liuy.ws.service;
import javax.jws.WebMethod;
import javax.jws.WebService;
@WebService( targetNamespace = "http://service.webservice.yinhai.com/" )
public interface HelloService2 {
/**
* 获取医师号源详情
*
* @param inputStr
* @return
*/
@WebMethod( operationName = "getUser" )
String getUser( String inputStr );
}
2、实现类
package com.liuy.ws.service.impl;
import com.alibaba.fastjson.JSONObject;
import com.liuy.ws.service.HelloService;
import org.springframework.stereotype.Service;
@Service
public class HelloServiceImpl implements HelloService {
@Override
public String sayHello(String msg) {
return "hello: " + msg;
}
@Override
public String getUser(String msg) {
StringBuffer sb = new StringBuffer(msg);
for (int i = 0; i < 100000; i++) {
sb.append(msg);
}
return sb.toString();
}
@Override
public JSONObject getJson(String msg) {
JSONObject json = new JSONObject();
json.put("msg", msg);
return json;
}
}
package com.liuy.ws.service.impl;
import com.alibaba.fastjson.JSONObject;
import com.liuy.ws.service.PatientWebservice;
import org.opensaml.xml.signature.J;
import org.springframework.stereotype.Service;
import javax.jws.HandlerChain;
@HandlerChain(file = "/handle-chain.xml")
@Service
public class PatientWebserviceImpl implements PatientWebservice {
@Override
public String getUser(String inputStr) {
JSONObject obj = JSONObject.parseObject(inputStr);
return obj.toJSONString();
}
}
三、服务端代码
1、服务端一共三个类:
- CxfConfig.java
- SecurityHandler.java
- WsClinetAuthHandler.java
package com.liuy.ws.server;
import com.liuy.ws.client.PasswordHandler;
import com.liuy.ws.service.HelloService;
import com.liuy.ws.service.PatientWebservice;
import org.apache.cxf.Bus;
import javax.xml.ws.Endpoint;
import org.apache.cxf.binding.soap.saaj.SAAJOutInterceptor;
import org.apache.cxf.jaxws.EndpointImpl;
import org.apache.cxf.ws.security.wss4j.WSS4JOutInterceptor;
import org.apache.ws.security.WSConstants;
import org.apache.ws.security.handler.WSHandlerConstants;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.ws.soap.security.wss4j2.Wss4jSecurityInterceptor;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class CxfConfig {
@Autowired
private Bus bus;
@Autowired
private HelloService helloService;
@Autowired
private HelloService2 helloService2;
@Autowired
AuthInterceptor authInterceptor;
@Bean
public Endpoint endpoint() {
EndpointImpl endpoint = new EndpointImpl(bus, helloService);
endpoint.publish("/helloService");
// endpoint.getInInterceptors().add(authInterceptor);
// 访问连接: http://localhost:8080/services/helloService?wsdl
return endpoint;
}
@Bean
public Endpoint endpoint2() {
EndpointImpl endpoint = new EndpointImpl(bus, helloService2);
endpoint.publish("/helloService2");
Map<String, Object> outProps = new HashMap<String, Object>();
outProps.put(WSHandlerConstants.ACTION, WSHandlerConstants.USERNAME_TOKEN);
outProps.put(WSHandlerConstants.USER, "admin");
outProps.put(WSHandlerConstants.PASSWORD_TYPE, WSConstants.PW_TEXT);
outProps.put(WSHandlerConstants.PW_CALLBACK_CLASS, WsClinetAuthHandler.class.getName());
ArrayList list = new ArrayList();
// 添加cxf安全验证拦截器,必须
list.add(new SAAJOutInterceptor());
list.add(new WSS4JOutInterceptor(outProps));
endpoint.getOutInterceptors().addAll(list);
// 访问连接: http://localhost:8080/services/helloService2?wsdl
return endpoint;
}
}
package com.liuy.ws.server;
import javax.xml.namespace.QName;
import javax.xml.soap.*;
import javax.xml.ws.handler.MessageContext;
import javax.xml.ws.handler.soap.SOAPHandler;
import javax.xml.ws.handler.soap.SOAPMessageContext;
import javax.xml.ws.soap.SOAPFaultException;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
public class SecurityHandler implements SOAPHandler<SOAPMessageContext> {
@Override
public boolean handleMessage(SOAPMessageContext messageContext) {
// TODO Auto-generated method stub
System.out.println("To handle SOAP message...");
boolean responseFlag = (Boolean) messageContext
.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY);
// for response message only, true for outbound messages, false for
// inbound
System.out.println("responseFlag=" + responseFlag);
if (!responseFlag) {
SOAPMessage soapMessage = messageContext.getMessage();
try {
SOAPEnvelope soapEnvelope = soapMessage.getSOAPPart()
.getEnvelope();
SOAPHeader soapHeader = soapEnvelope.getHeader();
if (null == soapHeader) {
System.out.println("No SOAP message header");
generateSOAPFault(soapMessage, "No SOAP message header");
return false;
}
Iterator iterator = soapHeader
.extractHeaderElements(SOAPConstants.URI_SOAP_ACTOR_NEXT);
if (null == iterator || null == iterator.next()) {
System.out.println("No header block for role next.");
generateSOAPFault(soapMessage,
"No header block for role next.");
return false;
}
Node next = (Node) iterator.next();
String value = (next == null) ? null : next.getValue();
if (null == value) {
System.out
.println("No authentication info in header block.");
generateSOAPFault(soapMessage,
"No authentication info in header block.");
return false;
}
// 只要有头部,就可以访问
return true;
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
}
return true;
}
private void generateSOAPFault(SOAPMessage soapMessage, String reason) {
// TODO Auto-generated method stub
try {
SOAPBody soapBody = soapMessage.getSOAPBody();
SOAPFault soapFault = soapBody.getFault();
soapFault.setFaultString(reason);
throw new SOAPFaultException(soapFault);
} catch (SOAPException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
@Override
public boolean handleFault(SOAPMessageContext context) {
// TODO Auto-generated method stub
return false;
}
@Override
public void close(MessageContext context) {
// TODO Auto-generated method stub
}
@Override
public Set<QName> getHeaders() {
// TODO Auto-generated method stub
HashSet<QName> headers = new HashSet<QName>();
QName securityHeader = new QName(
"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd",
"Security");
headers.add(securityHeader);
QName addressingHeader = new QName(
"http://www.w3.org/2005/08/addressing", "To");
headers.add(addressingHeader);
return headers;
}
}
package com.liuy.ws.server;
import org.apache.wss4j.common.ext.WSPasswordCallback;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.UnsupportedCallbackException;
import java.io.IOException;
/**
* 测试用
*/
public class WsClinetAuthHandler implements CallbackHandler {
@Override
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
for (int i = 0; i < callbacks.length; i++) {
WSPasswordCallback pc = (WSPasswordCallback) callbacks[i];
System.out.println("identifier: " + pc.getIdentifier());
pc.setPassword("111111");//密码
pc.setIdentifier("222222");
}
}
}
2、还有一个配置文件:
- handle-chain.xml
这里不得不提一下这个配置文件的路径问题。之前有参考过一些博文,但是对这个xml文件的路径配置方式,并么有明确的描述。我个人做了测试,在spring boot2框架中,可以把它放在resources目录下,同时,在实现类的注解中使用/作为根路径开头,@HandlerChain(file = "/handle-chain.xml"),这样就不会报错了。如果直接使用file = "handle-chain.xml"的话,需要在打包时,把文件放到实现类所在的目录下,否则会找不到文件。
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<javaee:handler-chains
xmlns:javaee="http://java.sun.com/xml/ns/javaee"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<javaee:handler-chain>
<javaee:handler>
<javaee:handler-class>com.liuy.ws.server.SecurityHandler</javaee:handler-class>
</javaee:handler>
</javaee:handler-chain>
</javaee:handler-chains>
四、客户端代码
package com.liuy.ws.client;
import com.alibaba.fastjson.JSONObject;
import org.apache.cxf.endpoint.Client;
import org.apache.cxf.jaxws.endpoint.dynamic.JaxWsDynamicClientFactory;
import org.apache.cxf.ws.security.wss4j.WSS4JOutInterceptor;
import org.apache.ws.security.WSConstants;
import org.apache.ws.security.handler.WSHandlerConstants;
import javax.xml.namespace.QName;
import javax.xml.soap.SOAPElement;
import java.util.HashMap;
import java.util.Map;
public class ClientDemo {
public static void main(String[] args) {
JaxWsDynamicClientFactory dcf = JaxWsDynamicClientFactory.newInstance();
// Client client = dcf.createClient("http://localhost:8082/services/helloService?wsdl");
Client client = dcf.createClient("http://localhost:8082/services/helloService2?wsdl");
// 不做用户名密码验证时,以下代码可以省略 ↓
Map<String, Object> props = new HashMap<String, Object>();
props.put(WSHandlerConstants.ACTION, WSHandlerConstants.USERNAME_TOKEN);
//密码类型 明文:PasswordText密文:PasswordDigest
props.put(WSHandlerConstants.PASSWORD_TYPE, WSConstants.PW_TEXT);
//用户名
props.put(WSHandlerConstants.USER, "admin");
//将PasswordHandler 的类名传递给服务器,通过PasswordHandler来配置用户名和密码
props.put(WSHandlerConstants.PW_CALLBACK_CLASS, PasswordHandler.class.getName());
WSS4JOutInterceptor wssOut = new WSS4JOutInterceptor(props);
client.getOutInterceptors().add(wssOut);
// 不做用户名密码验证时,以上代码可以省略 ↑
Object[] objects;
JSONObject input = new JSONObject();
input.put( "name", "张三" );
input.put( "年龄", "18" );
input.put( "性别", "LGBT" );
String inputStr = input.toString();
try {
// 这里用来指明服务端接口的targetNamespace,如果客户端的接口类包路径和服务端配置的targetNamespace一致,可以不使用QName,直接传入方法名字符串。
QName name = new QName("http://service.ws.liuy.com/","getUser");
objects = client.invoke(name, inputStr);
// objects = client.invoke("getUser", inputStr);
String result = String.valueOf(objects[0]);
System.out.println(result.length());
System.out.println(result);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
package com.liuy.ws.client;
import org.apache.wss4j.common.ext.WSPasswordCallback;
import javax.security.auth.callback.CallbackHandler;
public class PasswordHandler implements CallbackHandler {
@Override
public void handle(javax.security.auth.callback.Callback[] callbacks) {
for (int i = 0; i < callbacks.length; i++) {
WSPasswordCallback pc = (WSPasswordCallback)callbacks[i];
pc.setPassword("111111");
pc.setIdentifier("222222");
}
}
}
总结
与其说太多语言,不如直接跑一下代码来的更直观。如果有人发现本博文存在错误或缺漏,请指出来,轻喷。
后记
在工作中有同事遇到过字符串较长报错的问题,报错信息如下:
严重: Servlet.service() for servlet [XXMobileAction] in context with path [/xxapp] threw exception [Request processing failed; nested exception is javax.xml.ws.soap.SOAPFaultException: Error reading XMLStreamReader.] with root cause
com.ctc.wstx.exc.WstxEOFException: Unexpected EOF in prolog
at [row,col {unknown-source}]: [1,0]
但是同事用的框架是spring+struts2的老框架,我在本博文提到的demo中未发现这种报错。
这篇博文借鉴了很多其他博主的文章,可以说是个缝合怪,同时也加入了我自己的一些东西。闻道有先后,术业有专攻,感谢各位前辈的知识输出。(鄙视那些不知所谓的混子。。。)