调用需求Http Basic身份验证的SAP Webservice


背景

笔者当前从事制造业相关行业软件工作,因工作需要,在MES系统中需要向SAP拉取订单、物料、工序等数据。

笔者接触的SAP提供了跨系统通信的Po中间件(实际上是WebService SOAP1.0)。

通常情况下,我们只要使用wsdl生成工具,生成本地的调用客户端即可。常用的客户端工具是apace-cxf。

IDEA旗舰版本中,邮件项目,有个WebService相关的菜单,输入wsdl地址即可。社区版一般安装一个maven插件,如下

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.cxfgroupId>
            <artifactId>cxf-codegen-pluginartifactId>
            <version>3.4.5version>
            <executions>
                <execution>
                    <id>generate-sources-w2jid>
                    <phase>generate-sourcesphase>
                    <configuration>
                        <sourceRoot>src/main/javasourceRoot>
                        <defaultOptions>
                            <extraargs>
                                <extraarg>-implextraarg>
                                <extraarg>-verboseextraarg>
                                <extraarg>-validateextraarg>
                                
                            extraargs>
                        defaultOptions>
                        <wsdlOptions>
                            <wsdlOption>
                                <wsdl>http://www1.host.com/dir/wsdl?p=ic/2f1826f53a9b398a83000fdd7a05814ewsdl>
                                <extendedSoapHeaders>trueextendedSoapHeaders>
                                <autoNameResolution>trueautoNameResolution>
                            wsdlOption>
                            
                            <wsdlOption>
                                <wsdl>http://www1.host.com/dir/wsdl?p=ic/fb71dee0a5c538d99d305c6f28eebd53wsdl>
                                <extendedSoapHeaders>trueextendedSoapHeaders>
                                <autoNameResolution>trueautoNameResolution>
                            wsdlOption>
                            
                            <wsdlOption>
                                <wsdl>http://www1.host.com/dir/wsdl?p=ic/46d3848617fa3dd7bc806f248d51340fwsdl>
                                <extendedSoapHeaders>trueextendedSoapHeaders>
                                <autoNameResolution>trueautoNameResolution>
                            wsdlOption>
                            
                            <wsdlOption>
                                <wsdl>http://www1.host.com/dir/wsdl?p=ic/48c28c49f4fc337d9fdfc3d5feee4e66wsdl>
                                <extendedSoapHeaders>trueextendedSoapHeaders>
                                <autoNameResolution>trueautoNameResolution>
                            wsdlOption>
                            
                            <wsdlOption>
                                <wsdl>http://www1.host.com/dir/wsdl?p=ic/6dcd80497ea937c8b484635b554710f4wsdl>
                                <extendedSoapHeaders>trueextendedSoapHeaders>
                                <autoNameResolution>trueautoNameResolution>
                            wsdlOption>
                            
                            <wsdlOption>
                                <wsdl>http://www1.host.com/dir/wsdl?p=ic/4c602e2c4d673d3ba71a205d708b9c02wsdl>
                                <extendedSoapHeaders>trueextendedSoapHeaders>
                                <autoNameResolution>trueautoNameResolution>
                            wsdlOption>
                            
                            <wsdlOption>
                                <wsdl>http://www1.host.com/dir/wsdl?p=ic/40dd063793613e2caec3d96510577b37wsdl>
                                <extendedSoapHeaders>trueextendedSoapHeaders>
                                <autoNameResolution>trueautoNameResolution>
                            wsdlOption>
                        wsdlOptions>
                    configuration>
                    <goals>
                        <goal>wsdl2javagoal>
                    goals>
                execution>
            executions>
        plugin>
    plugins>
plugins>

然后在右侧点击wsdl2java即可生成本地客户端调用代码

 这是一是常规情况,SAP的特殊支持就是在通信协议层面做了Http Basic认证,即在请求头要添加

Authorization: Basic base64字符串

以下是物料按时间拉取的一个标准请求报文及相应报文,使用soapui发包,通过wireshark抓取

POST /XISOAPAdapter/MessageServlet?senderParty=&senderService=BS_MES_DEV&receiverParty=&receiverService=
&interface=SI_MaterialMasterData_In&interfaceNamespace=http://host.com/MES/MaterialMasterData/Sender HTTP/1.1 Accept-Encoding: gzip,deflate Content-Type: text/xml;charset=UTF-8 SOAPAction: "http://sap.com/xi/WebService/soap1.1" Content-Length: 450 Host: www1.host.com Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.5 (Java/15) Authorization: Basic VmlzaXRvcjpxd2VyMTIzNA== <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:urn="urn:sap-com:document:sap:rfc:functions"> <soapenv:Header/> <soapenv:Body> <urn:ZMES_SAP_003> <BEDAT>20211210BEDAT> <EDDAT>20211212EDDAT> urn:ZMES_SAP_003> soapenv:Body> soapenv:Envelope>HTTP/1.1 200 OK server: SAP NetWeaver Application Server 7.49 / AS Java 7.50 date: Thu, 23 Dec 2021 07:41:51 GMT set-cookie: MYSAPSSO2=AjExMDAgAA5wb3J0YWw6dmlzaXRvcogAB2RlZmF1bHQBAAdWSVNJVE9SAgADMDAwAwADUE9EBAAMMjAyMTEyMjMwNz
QxBQAEAAAACAoAB1ZJU0lUT1L%2FAQYwggECBgkqhkiG9w0BBwKggfQwgfECAQExCzAJBgUrDgMCGgUAMAsGCSqGSIb3DQEHATGB0TCBzgIBAT
AiMB0xDDAKBgNVBAMTA1BPRDENMAsGA1UECxMESjJFRQIBADAJBgUrDgMCGgUAoF0wGAYJKoZIhvcNAQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG
9w0BCQUxDxcNMjExMjIzMDc0MTUxWjAjBgkqhkiG9w0BCQQxFgQUdWxoAzhEYiSIc27jSe8FsKCEbjswCQYHKoZIzjgEAwQwMC4CFQDLMD
BWN7xZHY!5EWvCn3aesvvO6wIVAJWRZf5SLYj6KJlD!DaUx%2F!D3X!H;path=/;domain=.host.com;HttpOnly content-type: text/xml; charset=utf-8 content-id:
<soap-cb1e8be463c311ec92be0000002094d2@sap.com> content-disposition: attachment;filename="soap-cb1e8be463c311ec92be0000002094d2@sap.com.xml" content-description: SOAP content-encoding: gzip content-length: 557 set-cookie: saplb_*=(J2EE2135220)2135250; Version=1; Path=/ set-cookie: JSESSIONID=lvNdixDYPSn8vdeDR9UxlrxfOT3mfQHSlCAA_SAPZmasLjjtl7ew9y4UdoHLQTn2; Version=1; Path=/ set-cookie: JSESSIONMARKID=9EvTHA-TE05LwidImrSEHzSGbAroysc13JPdKUIAA; Version=1; Path=/ <SOAP:Envelope xmlns:SOAP='http://schemas.xmlsoap.org/soap/envelope/'><SOAP:Header/><SOAP:Body>
<
ns0:ZMES_SAP_003.Response xmlns:ns0='urn:sap-com:document:sap:rfc:functions'><MESSAGE>查询成功MESSAGE>
<
STA>SSTA><IT_MARA><item><MATNR>A06010475MATNR><MAKTX>卡扣 φ6.3-φ7 白MAKTX><MTART>Z001MTART>
<
MEINS>STMEINS><MATKL>3001MATKL><GROES/><BISMT/><PRDHA/><MFRNR>0000200021MFRNR><MFRPN/>
<
ZEXTRA1/><ZEXTRA2>CZEXTRA2><ZEXTRA3>18ZEXTRA3><ZEXTRA4>总装领料ZEXTRA4><ZEXTRA5>0ZEXTRA5>
<
ZEXTRA6/><ZEXTRA7/><ZEXTRA8>180ZEXTRA8><ZEXTRA9/><ZEXTRA10/><ZEXTRA13/>item>IT_MARA><IT_MARC>
<
item><MATNR>A06010475MATNR><WERKS>1201WERKS><EKGRP/><BSTMI>0BSTMI><BESKZ/><SOBSL/><RGEKM/>
<
DZEIT>0DZEIT><PLIFZ>0PLIFZ><EISBE>0EISBE><MAABC/><FEVOR/><BSTRF>0BSTRF>item><item>
<
MATNR>A06010475MATNR><WERKS>1101WERKS><EKGRP>101EKGRP><BSTMI>0BSTMI><BESKZ>FBESKZ>
<
SOBSL/><RGEKM/><DZEIT>0DZEIT><PLIFZ>5PLIFZ><EISBE>0EISBE><MAABC/><FEVOR/><BSTRF>12000.000BSTRF>
item><item><MATNR>A06010475MATNR><WERKS>1001WERKS><EKGRP>101EKGRP><BSTMI>1000.000BSTMI>
<
BESKZ>FBESKZ><SOBSL/><RGEKM/><DZEIT>0DZEIT><PLIFZ>30PLIFZ><EISBE>0EISBE><MAABC/><FEVOR/>
<
BSTRF>1000.000BSTRF>item>IT_MARC>ns0:ZMES_SAP_003.Response>SOAP:Body>SOAP:Envelope>

常规情况下apache cxf生成的客户端是不能带自定义请求头的,payload可以是自定义。当然网上有注册一个bean,

代理cxf本地请求客户端,带入身份验证,笔者认为笔记麻烦,没去测试。

解决方案

这篇文章九不科普太多了,直接上解决方案

SOAPClient.java
import cn.hutool.core.annotation.AnnotationUtil;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.fasterxml.jackson.module.jaxb.JaxbAnnotationIntrospector;
import com.xxx.mes.common.exception.SAPCommunicationException;
import com.xxx.mes.webservice.XPathExpression;
import lombok.extern.slf4j.Slf4j;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Node;
import org.dom4j.io.SAXReader;
import org.dom4j.xpath.DefaultXPath;
import org.springframework.http.HttpStatus;

import javax.xml.bind.annotation.XmlRootElement;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Base64;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;

import static com.xxx.mes.common.util.XmlUtil.JaxbBinder.fromXml;

/***
 * SOAP客户端
* 可以发送报文,解析报文映射成实体类 *
*/ @Slf4j public final class SOAPClient { private static final SAXReader saxReader = new SAXReader(); private static final Map,String> annotatedClassCache = new LinkedHashMap,String>(); /*** * 从基于soap的webservice报文中提取响应结果 * @param xml 返回的原始报文 * @param expression xpath评估表达式 * @param targetClass 待返回的结果 * @param * @return 如果解析成功将返回一个T类的响应实体 * @throws DocumentException */ public static T extractResponse(String xml, XPathExpression expression, Class targetClass) throws Exception { Document doc = saxReader.read(new ByteArrayInputStream(xml.getBytes())); DefaultXPath xpath = new DefaultXPath(expression.getExpression()); xpath.setNamespaceURIs(Collections.singletonMap(expression.getNamespaceURI(), expression.getTargetNamespace())); Node node = xpath.selectSingleNode(doc); final T response = fromXml(targetClass, node.asXML()); return response; } public static String getServiceUrl(String serviceName) { return String.format("http://www1.host.com/XISOAPAdapter/MessageServlet?senderParty=&senderService=BS_MES_DEV" + "&receiverParty=&receiverService=&interface=SI_%s_In&interfaceNamespace=http://host.com/MES/%s/Sender" , new Object[]{serviceName, serviceName}); } /*** * 通过soap请求载荷构造soap请求报文 * @param payload * @param * @return xml-based soap request payload * @throws JsonProcessingException */ public static String buildSOAPRequest(T payload) throws JsonProcessingException { final Class<?> targetClass = payload.getClass(); String elementName = annotatedClassCache.get(targetClass); if(elementName == null){ elementName = AnnotationUtil.getAnnotation(targetClass, XmlRootElement.class).name(); annotatedClassCache.put(targetClass,elementName); } XmlMapper xmlMapper = (XmlMapper) new XmlMapper() .setAnnotationIntrospector(new JaxbAnnotationIntrospector(TypeFactory.defaultInstance())); //字段为null,自动忽略,不再序列化 xmlMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); StringBuilder builder = new StringBuilder(); builder.append("\n" + " \n" + " "); builder.append(""); String xml = xmlMapper.writeValueAsString(payload); builder.append(xml); builder.append(""). append(" "). append(""); log.info("xml报文:{}", builder); return builder.toString().replace("<" + elementName + ">", "").replace("", ""); } /*** * 向SAP服务器提供的Webservice发起调用请求 * @param httpUrl 请求地址 * @param soapBody 使用{@link #buildSOAPRequest(Object)}构造的请求报文 * @return 返回xml格式的响应报文 * @throws IOException */ public static String send(String httpUrl, String soapBody) throws IOException { String resultData = ""; URL geturl = null; try { // 构造一个URL对象 geturl = new URL(httpUrl); } catch (MalformedURLException e) { throw new SAPCommunicationException("url地址格式无效:%s", new Object[]{httpUrl}); } try { // 使用HttpURLConnection打开连接 HttpURLConnection urlConn = (HttpURLConnection) geturl.openConnection(); // 设置请求的超时时间 urlConn.setReadTimeout(50000); urlConn.setConnectTimeout(50000); // Windows验证 用户密码 String datap = "Visitor:qwer1234"; String authorization = Base64.getEncoder().encodeToString(datap.getBytes()); urlConn.setRequestProperty("Authorization", "Basic " + authorization); // 设置请求的头 urlConn.setRequestProperty("Connection", "keep-alive"); // 配置本次连接的Content-type,配置为application/x-www-form-urlencoded的 urlConn.setRequestProperty("Content-Type", "text/xml;charset=UTF-8"); urlConn.setRequestProperty("SOAPAction", "http://sap.com/xi/WebService/soap1.1"); // 设置请求的头 urlConn.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 6.3; WOW64; rv:27.0) Gecko/20100101 Firefox/27.0"); // 因为这个是post请求,设立需要设置为true urlConn.setDoOutput(true); urlConn.setDoInput(true); // 设置以POST方式 urlConn.setRequestMethod("POST"); // Post 请求不能使用缓存 urlConn.setUseCaches(false); // 获取输出流 // BufferedReader os = new BufferedReader(new // OutputStream(urlConn.getOutputStream())) OutputStream os = urlConn.getOutputStream(); // byte[] content = data.getBytes("utf-8"); os.write(soapBody.getBytes()); os.flush(); os.close(); int statusCode = urlConn.getResponseCode(); if (statusCode == HttpStatus.OK.value()) { // 获取响应的输入流对象 InputStream is = urlConn.getInputStream(); // 创建字节输出流对象 ByteArrayOutputStream baos = new ByteArrayOutputStream(); // 定义读取的长度 int len; int irecord = 1; // 定义缓冲区 byte buffer[] = new byte[1024]; // 按照缓冲区的大小,循环读取 while ((len = is.read(buffer)) != -1) { // 根据读取的长度写入到os对象中 baos.write(buffer, 0, len); irecord += 1; } // 释放资源 is.close(); baos.close(); // 返回字符串 resultData = new String(baos.toByteArray()); irecord -= 1; return resultData; } else { urlConn.disconnect(); throw new SAPCommunicationException("接口报文返回异常:http状态码,期望值200,实际:%d", new Object[]{statusCode}); } } catch (Exception e) { log.error("SAP通信异常",e); throw e; } }

XPathExpression.java

import lombok.Data;

/**
 * 中间参数类,用于解析xml的xpath表达式
* 数据样本: *
 *     
 *     
 *     
 *         
 *             查询成功
 *             S
 *             
 *                 
 *                     A06010475
 *                     卡扣 φ6.3-φ7 白
 *                     Z001
 *                     ST
 *                     3001
 *                     
 *                     
 *                     
 *                     0000200021
 *                     
 *                     
 *                     C
 *                     18
 *                     总装领料
 *                     0
 *                     
 *                     
 *                     180
 *                     
 *                     
 *                     
 *                 
 *             
 *             
 *                 
 *                     A06010475
 *                     1201
 *                     
 *                     0
 *                     
 *                     
 *                     
 *                     0
 *                     0
 *                     0
 *                     
 *                     
 *                     0
 *                 
 *                 
 *                     A06010475
 *                     1101
 *                     101
 *                     0
 *                     F
 *                     
 *                     
 *                     0
 *                     5
 *                     0
 *                     
 *                     
 *                     12000.000
 *                 
 *                 
 *                     A06010475
 *                     1001
 *                     101
 *                     1000.000
 *                     F
 *                     
 *                     
 *                     0
 *                     30
 *                     0
 *                     
 *                     
 *                     1000.000
 *                 
 *             
 *         
 *     
 * 
 * 
* *
@author: passedbylove * @date: Created by 2021/12/28 11:04 * @version: 1.0.0 */ @Data public class XPathExpression { /*** * xpath提取数据节点的表达式 * eg.
* //ns0:ZMES_SAP_003.Response
*/ private String expression; /*** * 命名空间的前缀 * eg.
* ns0
*/ private String namespaceURI; /*** * 命名空间 * eg.
* "urn:sap-com:document:sap:rfc:functions"
*/ private String targetNamespace; public XPathExpression() { } public XPathExpression(String expression, String namespaceURI, String targetNamespace) { this.expression = expression; this.namespaceURI = namespaceURI; this.targetNamespace = targetNamespace; } }
SAPCommunicationException.java
import lombok.NoArgsConstructor;

/**
 * 用于注明是SAP系统通信过程中产生的异常,区别于其他异常,比如调用SAP接口过程中可能产生
 * UnknownHostException或者IOException等;此处归一化为SAPCommunicationException
 * 方便排错
 * @author: daiqiankun
 * @date: Created by  2021/12/28 11:11
 * @version: 1.0.0
 */
@NoArgsConstructor
public class SAPCommunicationException extends RuntimeException {

    public SAPCommunicationException(String message) {
        super(message);
    }

    public SAPCommunicationException(String format,Object[] args) {
        super(String.format(format,args));
    }

    public SAPCommunicationException(String message, Throwable ex) {
        super(message, ex);
    }

    public SAPCommunicationException(String message, Object[] args,Throwable ex) {
        super(String.format(message,args), ex);
    }
}
JaxbBinder.java
 /**
     * @datetime 2021/12/27 15:29
     */
    public static class JaxbBinder
    {
        /***
         * 缓存一下JAXBContext,减少反射开销
         */
        static Map, JAXBContext> contextCache = new LinkedHashMap<>();

        //多线程安全的Context.
        private JAXBContext jaxbContext;

        public JaxbBinder()
        {}

        /**
         * @param types 所有需要序列化的Root对象的类型.
         */
        public JaxbBinder(Class<?>... types) {
            try {
                jaxbContext = JAXBContext.newInstance(types);
            } catch (JAXBException e) {
                throw new RuntimeException(e);
            }
        }

        /**
         * Java Object->Xml.
         */
        public String toXml(Object root, String encoding) {
            try {
                StringWriter writer = new StringWriter();
                createMarshaller(encoding).marshal(root, writer);
                return writer.toString();
            } catch (JAXBException e) {
                throw new RuntimeException(e);
            }
        }

        /**
         * Java Object->Xml, 特别支持对Root Element是Collection的情形.
         */
        @SuppressWarnings("unchecked")
        public String toXml(Collection root, String rootName, String encoding) {
            try {
                CollectionWrapper wrapper = new CollectionWrapper();
                wrapper.collection = root;

                JAXBElement wrapperElement = new JAXBElement(new QName(rootName),
                        CollectionWrapper.class, wrapper);

                StringWriter writer = new StringWriter();
                createMarshaller(encoding).marshal(wrapperElement, writer);

                return writer.toString();
            } catch (JAXBException e) {
                throw new RuntimeException(e);
            }
        }

        /**
         * Xml->Java Object.
         */
        @SuppressWarnings("unchecked")
        public  T fromXml(String xml) {
            try {
                StringReader reader = new StringReader(xml);
                return (T) createUnmarshaller().unmarshal(reader);
            } catch (JAXBException e) {
                throw new RuntimeException(e);
            }
        }

        /**
         * 将XML转为指定的POJO
         *
         * @param clazz
         * @param xmlStr
         * @return
         * @throws Exception
         */
        public static  T fromXml(Class<?> clazz, String xmlStr) throws Exception {
            T xmlObject = null;
            Reader reader = null;

            JAXBContext context = null;
            context = contextCache.get(clazz);

            if (Objects.isNull(context)) {
                context = JAXBContext.newInstance(clazz);
                contextCache.put(clazz, context);
            }
            // XML 转为对象的接口
            Unmarshaller unmarshaller = context.createUnmarshaller();
            reader = new StringReader(xmlStr);
            //以文件流的方式传入这个string
            xmlObject = (T)unmarshaller.unmarshal(reader);
            if (null != reader) {
                reader.close();
            }
            return xmlObject;
        }

        /**
         * 创建Marshaller, 设定encoding(可为Null).
         */
        public Marshaller createMarshaller(String encoding) {
            try {
                Marshaller marshaller = jaxbContext.createMarshaller();

                marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);

                if (!StringUtils.isEmpty(encoding)) {
                    marshaller.setProperty(Marshaller.JAXB_ENCODING, encoding);
                }
                return marshaller;
            } catch (JAXBException e) {
                throw new RuntimeException(e);
            }
        }

        /**
         * 创建UnMarshaller.
         */
        public Unmarshaller createUnmarshaller() {
            try {
                return jaxbContext.createUnmarshaller();
            } catch (JAXBException e) {
                throw new RuntimeException(e);
            }
        }

        /**
         * 封装Root Element 是 Collection的情况.
         */
        public static class CollectionWrapper {
            @SuppressWarnings("unchecked")
            @XmlAnyElement
            protected Collection collection;
        }


        @SuppressWarnings("unchecked")
        public  T fromXML(String fileName) {
            return (T)fromXML(new File(fileName));
        }


        @SuppressWarnings("unchecked")
        public  T fromXML(File file) {
            try {
                Unmarshaller unmarshaller = createUnmarshaller();
                return (T) unmarshaller.unmarshal(file);
            } catch (JAXBException e) {
                throw new RuntimeException(e);
            }
        }


        @SuppressWarnings("unchecked")
        public  T fromXML(InputStream stream) {
            try {
                Unmarshaller unmarshaller = createUnmarshaller();
                return (T) unmarshaller.unmarshal(stream);
            } catch (JAXBException e) {
                throw new RuntimeException(e);
            }
        }
    }

调用方法

ZMESSAP003 zmessap003 = new ZMESSAP003();
zmessap003.setBEDAT("19901101");
zmessap003.setEDDAT("19901130");
final XmlRootElement annotation = AnnotationUtil.getAnnotation(ZMESSAP003.class, XmlRootElement.class);
final String data = buildSOAPRequest(zmessap003, annotation.name());
final String xml = SOAPClient.send("http://www1.host.com/XISOAPAdapter/MessageServlet?senderParty=&senderService=BS_MES_DEV&receiverParty=&receiverService=&interface=SI_MaterialMasterData_In&interfaceNamespace=http://xxx.com/MES/MaterialMasterData/Sender&sap-user=Visitor&sap-password=qwer1234", data);
System.out.println(data);
XPathExpression expression = new XPathExpression();
expression.setExpression("//ns0:ZMES_SAP_003.Response");
expression.setNamespaceURI("ns0");
expression.setTargetNamespace("urn:sap-com:document:sap:rfc:functions");
final ZMESSAP003Response response = SOAPClient.extractResponse(xml, expression, ZMESSAP003Response.class);

其中ZMESSAP003 是cxf生成的本地客户端代码,本文不是最终代码,仅供各位参考。

需要用到的pom依赖

hutool

<dependency>
    <groupId>com.fasterxml.jackson.dataformatgroupId>
    <artifactId>jackson-dataformat-xmlartifactId>
dependency>