spring boot + cxf 实现有密码验证功能的 webservice 接口笔记

2,554 阅读4分钟

前言

之前因为一些原因,需要搭建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、服务端一共三个类:

  1. CxfConfig.java
  2. SecurityHandler.java
  3. 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、还有一个配置文件:

  1. 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中未发现这种报错。

这篇博文借鉴了很多其他博主的文章,可以说是个缝合怪,同时也加入了我自己的一些东西。闻道有先后,术业有专攻,感谢各位前辈的知识输出。(鄙视那些不知所谓的混子。。。)