[feature] 补漏提交的code

This commit is contained in:
Weasley J
2023-08-15 17:51:03 +08:00
parent 1f954a992b
commit 0eaa8f6ea0
25 changed files with 2555 additions and 0 deletions
@@ -0,0 +1,11 @@
FROM azul/zulu-openjdk:17-jre
MAINTAINER weasley
WORKDIR /app
COPY target/chinaport-data-signature-*.jar /app/chinaport-data-signature.jar
EXPOSE 8080
ENV TZ=PRC
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ >/etc/timezone
ENV JAVA_OPTS=""
ENV SPRING_ARGS=""
ENTRYPOINT ["sh", "-c", "java ${JVM_OPTIONS} -jar chinaport-data-signature.jar ${SPRING_ARGS}"]
@@ -0,0 +1,8 @@
#!/bin/bash
TAG="1.1.1"
docker stop chinaport-data-signature && docker rm -f chinaport-data-signature
docker rmi -f weasleyj/chinaport-data-signature:${TAG}-arm64
docker build -t weasleyj/chinaport-data-signature:${TAG}-arm64 . -f ./Dockerfile
docker login -u weasleyj
docker push weasleyj/chinaport-data-signature:${TAG}-arm64
exit 0
@@ -0,0 +1,8 @@
#!/bin/bash
TAG="latest"
docker stop chinaport-data-signature && docker rm -f chinaport-data-signature
docker rmi -f weasleyj/chinaport-data-signature:${TAG}
docker build -t weasleyj/chinaport-data-signature:${TAG} . -f ./Dockerfile
docker login -u weasleyj
docker push weasleyj/chinaport-data-signature:${TAG}
exit 0
@@ -0,0 +1,20 @@
package cn.alphahub.eport.signature;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
/**
* 中国电子口岸(海口海关)报文加签推送应用
*
* @author Weasley J
* @date 2022-02-13
* @since 1.0.0
*/
@EnableWebSocket
@SpringBootApplication
public class ChinaEportSignatureApplication {
public static void main(String[] args) {
SpringApplication.run(ChinaEportSignatureApplication.class, args);
}
}
@@ -0,0 +1,41 @@
package cn.alphahub.eport.signature.base.exception;
import lombok.Getter;
import java.io.Serial;
/**
* The eport web client exception
*
* @author weasley
* @version 1.1.0
*/
@Getter
public class EportWebClientException extends RuntimeException {
@Serial
private static final long serialVersionUID = 1L;
/**
* 异常消息
*/
private String msg;
/**
* 错误码, 默认: 500
*/
private int code = 500;
public EportWebClientException(String msg) {
super(msg);
this.msg = msg;
}
public EportWebClientException(String msg, Throwable e) {
super(msg, e);
this.msg = msg;
}
public EportWebClientException(String msg, int code, Throwable e) {
super(msg, e);
this.msg = msg;
this.code = code;
}
}
@@ -0,0 +1,49 @@
package cn.alphahub.eport.signature.base.exception;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.io.Serial;
/**
* 自定义异常
*
* @author liuwenjing
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class SignException extends RuntimeException {
@Serial
private static final long serialVersionUID = 1L;
/**
* 异常消息
*/
private String msg;
/**
* 错误码, 默认500
*/
private int code = 500;
public SignException(String msg) {
super(msg);
this.msg = msg;
}
public SignException(String msg, Throwable e) {
super(msg, e);
this.msg = msg;
}
public SignException(String msg, int code, Throwable e) {
super(msg, e);
this.msg = msg;
this.code = code;
}
}
@@ -0,0 +1,44 @@
package cn.alphahub.eport.signature.config;
import cn.alphahub.eport.signature.core.ChinaEportReportClient;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.validation.annotation.Validated;
/**
* 电子口岸XML报文传输的企业元数据配置
*
* @author weasley
* @version 1.1.0
*/
@Data
@Validated
@Configuration
@ConfigurationProperties(prefix = ChinaEportProperties.PREFIX)
public class ChinaEportProperties {
public static final String PREFIX = "eport.signature.report.ceb-message";
/**
* 传输企业代码,报文传输的企业代码(需要与接入客户端的企业身份一致)
*/
@NotBlank(message = "配置文件中的报文传输的企业代码不能为空")
private String copCode;
/**
* 传输企业名称,报文传输的企业名称
*/
@NotBlank(message = "报文传输的企业名称不能为空")
private String copName;
/**
* 报文传输编号,向中国电子口岸数据中心申请数据交换平台的用户编号
*/
@NotBlank(message = "向中国电子口岸数据中心申请数据交换平台的用户编号不能为空")
private String dxpId;
/**
* 海关服务器地址,缺省则采用Client中的密文作文默认Server URL
*
* @apiNote CEMMessage XML数据上报服务器地址,格式: http://ip:port
* @see ChinaEportReportClient#EPORT_CEBMESSAGE_SERVER_ENCODE
*/
private String server;
}
@@ -0,0 +1,33 @@
package cn.alphahub.eport.signature.config;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
/**
* 海关 179 数据上报配置元数据
*
* @author weasley
* @version 1.1.0
* @apiNote 如果接口没有传过来, 就取
*/
@Data
@Validated
@ConfigurationProperties(prefix = Customs179Properties.PREFIX)
public class Customs179Properties {
public static final String PREFIX = "eport.signature.report.customs179";
/**
* 电商平台代码
*
* @apiNote 电商平台海关注册登记编号或统一社会信用代码。境内企业需据实填写海关注册登记编号或统一社会信用代码,境外平台企业如无法提供电商平台代码可填“无”。
*/
@NotBlank(message = "电商平台代码不能为空;如果你想对接CEBMessage进出口单, 先随便填一个,后面改成正确的")
private String ebpCode;
/**
* 数据上报服务器URL地址,链接格式: https://域名联系海关获取/ceb2grab/grab/realTimeDataUpload
*
* @apiNote 没有配置的话采用项目内置的URL地址
*/
private String server;
}
@@ -0,0 +1,45 @@
package cn.alphahub.eport.signature.config;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
import java.util.Objects;
/**
* XML Signature Algorithm
*
* @author weasley
* @version 1.0.0
* @apiNote 签名算法
*/
@Getter
@AllArgsConstructor
public enum SignatureAlgorithm {
RSA_SHA1("rsa-sha1", "SHA1WITHRSA", "http://www.w3.org/2000/09/xmldsig#rsa-sha1"),
SM2_SM3("sm2-sm3", "SM3WITHSM2", "http://www.w3.org/2000/09/xmldsig#sm2-sm3"),
;
/**
* 算法类型
*/
private final String algorithmType;
/**
* 算法名称
*/
private final String algorithmName;
/**
* XML代码段中 ds:SignatureMethod 的算法值
* <p>
* XML Signature Method:
* <pre>
* {@code <ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#${algorithmType}"/>}
* </pre>
*/
private final String signatureMethod;
public static SignatureAlgorithm getSignatureAlgorithmSigAlgName(String sigAlgName) {
return Arrays.stream(values())
.filter(original -> Objects.equals(original.getAlgorithmName(), sigAlgName))
.findFirst().orElse(null);
}
}
@@ -0,0 +1,158 @@
package cn.alphahub.eport.signature.config;
import cn.alphahub.eport.signature.support.CommandClient;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.SystemUtils;
import org.apache.logging.log4j.util.Strings;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import static cn.alphahub.eport.signature.config.ICommand.EPORT_ACCESS_CONTROL;
import static cn.alphahub.eport.signature.config.ICommand.EPORT_WS_SERVER;
import static cn.alphahub.eport.signature.config.UkeyProperties.PREFIX;
/**
* An interface to build command
*/
@FunctionalInterface
interface ICommand extends Serializable {
/**
* WebSocketServer.exe
*/
String EPORT_WS_SERVER = "CnEport.Pub.WebSocketServer.exe";
/**
* SetAccessControl.exe
*/
String EPORT_ACCESS_CONTROL = "SetAccessControl.exe";
/**
* To build command
*
* @param exeClient e.g: C:\中国电子口岸客户端控件\SetAccessControl.exe
* @return command script
*/
String build(String exeClient);
}
/**
* Ukey自我重启相关
* <p>
* Windows上的可执行文件: C:\中国电子口岸客户端控件\SetAccessControl.exe
*
* @author weasley
* @version 1.0.0
* @apiNote 可执行文件 SetAccessControl.exe 的安装位置以实际为准
*/
@Data
@Slf4j
@ConfigurationProperties(prefix = PREFIX + ".health.endpoint")
public class UkeyAccessClientProperties {
/**
* Windows上重启的ukey可执行文件全限定文件名称
*
* @apiNote 建议指定 clientName,可以避免全盘查找 SetAccessControl.exe, 提高性能
* @apiNote 全限定文件名称,包含文件后缀, 建议不要安装到中文目录里面,如: C:\中国电子口岸客户端控件\SetAccessControl.exe
*/
private String clientName;
/**
* 查找中国电子口岸客户端控件重启客户端的Windows可执行文件的具体位置
*
* @return SetAccessControl.exe 文件的绝对路径
* @apiNote 以下命令行只适用于Windows平台,其他平台报错直接忽略
*/
public String findClient() {
if (!SystemUtils.IS_OS_WINDOWS) return null;
CommandClient sharedInstance = CommandClient.getSharedInstance();
List<String> accessControlStdOutList;
List<String> eportWsServerStdOutList;
sharedInstance.execute("where " + EPORT_ACCESS_CONTROL);
accessControlStdOutList = new ArrayList<>(sharedInstance.getStdOutList());
if (CollectionUtils.isEmpty(accessControlStdOutList)) return null;
sharedInstance.execute("where " + EPORT_WS_SERVER);
eportWsServerStdOutList = new ArrayList<>(sharedInstance.getStdOutList());
if (CollectionUtils.isEmpty(eportWsServerStdOutList)) return null;
String accessControlStdOut = FilenameUtils.getFullPathNoEndSeparator(accessControlStdOutList.get(0));
String eportWsServerStdOut = FilenameUtils.getFullPathNoEndSeparator(eportWsServerStdOutList.get(0));
if (accessControlStdOut.equals(eportWsServerStdOut)) {
log.info("找到u-key Windows客户端执行文件: {} {}", EPORT_WS_SERVER, EPORT_ACCESS_CONTROL);
return accessControlStdOutList.get(0);
}
return null;
}
/**
* 执行指令
* <pre>
* -h 或 -help: 帮助
* -restart: 重启控件
* -start: 启动控件
* -stop: 停止控件
* -repair: 修复证书
* </pre>
*/
@Getter
@AllArgsConstructor
public enum Command implements ICommand {
/**
* 重启控件
*/
RESTART("-restart", "重启控件"),
/**
* 启动控件
*/
START("-start", "启动控件"),
/**
* 停止控件
*/
STOP("-stop", "停止控件"),
/**
* 修复证书
*/
REPAIR("-repair", "修复证书"),
/**
* 缺省符,无业务意义
*/
DEFAULT("", "缺省符");
/**
* 指令
*/
private final String cmd;
/**
* 描述
*/
private final String desc;
public String getCmd() {
if (RESTART.cmd.equals(cmd)) {
return cmd + " -i";
}
return cmd;
}
@Override
public String build(String exeClient) {
if (null == exeClient) return Strings.EMPTY;
if (StringUtils.isBlank(this.getCmd())) {
return "cmd /c \"" + exeClient + "\"";
}
return "cmd /c \"" + exeClient + " " + this.getCmd() + "\"";
}
}
}
@@ -0,0 +1,300 @@
package cn.alphahub.eport.signature.config;
import cn.alphahub.dtt.plus.util.JacksonUtil;
import cn.alphahub.dtt.plus.util.SpringUtil;
import cn.alphahub.eport.signature.core.CertificateHandler;
import cn.alphahub.eport.signature.core.SignHandler;
import cn.alphahub.eport.signature.core.WebSocketClientHandler;
import cn.alphahub.eport.signature.entity.SignRequest;
import cn.alphahub.eport.signature.entity.SpcValidTime;
import cn.alphahub.eport.signature.entity.UkeyRequest;
import cn.alphahub.eport.signature.entity.UkeyResponse;
import cn.alphahub.eport.signature.entity.WebSocketWrapper;
import cn.alphahub.eport.signature.support.CertificateParser;
import cn.hutool.core.lang.TypeReference;
import lombok.Data;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.client.WebSocketConnectionManager;
import org.springframework.web.socket.client.standard.StandardWebSocketClient;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import java.io.IOException;
import java.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.LockSupport;
import static cn.alphahub.eport.signature.core.CertificateHandler.SING_DATA_METHOD;
import static cn.hutool.json.JSONUtil.toBean;
import static cn.hutool.json.JSONUtil.toJsonStr;
/**
* 初始化配置
*
* @author lwj
* @version 1.0
* @date 2022-01-11 15:29
*/
@Data
@Slf4j
@Configuration
@EnableConfigurationProperties({UkeyProperties.class, EmailProperties.class, SignatureAlgorithmProperties.class, Customs179Properties.class})
public class UkeyInitialConfig implements ApplicationRunner {
/**
* u-key默认密码8个8不要修改
*/
private static final String DEFAULT_PASSWORD = "88888888";
/**
* 取海关签名证书PEM, X509Certificate证书(未经HASH算法散列过)
*
* @return 取海关签名证书PEM, X509Certificate证书是加签证书,真正的X509Certificate证书
*/
public static String getX509CertificateParameter() {
UkeyRequest ukeyRequest = new UkeyRequest();
ukeyRequest.set_method("cus-sec_SpcGetSignCertAsPEM");
ukeyRequest.set_id(1);
ukeyRequest.setArgs(new HashMap<>());
return JacksonUtil.toJson(ukeyRequest);
}
/**
* 使用卡计算摘要,返回PEM格式信息, 这个不调也罢,已经硬核计算出来了
*
* @param sourceXml 不含ds:Signature节点的<ceb:CEBXxxMessage></ceb:CEBXxxMessage>源xml原文数据
* @param uniqueId uniqueId 唯一id, 用来区分是哪一次发送的消息,int32,最大32位大于0
* @return DigestValue的值
*/
public static String getDigestValueParameter(String sourceXml, Integer uniqueId) {
Map<String, Object> args = new LinkedHashMap<>(2);
args.put("szInfo", sourceXml);
args.put("passwd", ObjectUtils.defaultIfNull(SpringUtil.getBean(UkeyProperties.class).getPassword(), DEFAULT_PASSWORD));
UkeyRequest ukeyRequest = new UkeyRequest();
ukeyRequest.set_method("sec_SpcSHA1DigestAsPEM");
ukeyRequest.set_id(uniqueId);
ukeyRequest.setArgs(args);
return JacksonUtil.toJson(ukeyRequest);
}
/**
* 签名, 不对原文计算摘要, 请您自行计算好摘要传入
*
* @param request 原文入参
* @return SignatureValue的值, 返回的数组,包含您的证书编号,可作为KeyName的值
*/
public static String getSignDataAsPEM(SignRequest request) {
String initData = SignHandler.getInitData(request);
log.warn("发送给ukey的真正请求入参: {}\n原始报文: {}", initData, request.getData());
@SuppressWarnings({"all"}) UkeyRequest ukeyRequest = new UkeyRequest("cus-sec_SpcSignDataAsPEM", new HashMap<>() {{
put("inData", initData);
put("passwd", ObjectUtils.defaultIfNull(SpringUtil.getBean(UkeyProperties.class).getPassword(), DEFAULT_PASSWORD));
}});
ukeyRequest.set_id(request.getId());
return JacksonUtil.toJson(ukeyRequest);
}
/**
* 使用卡计算摘要,返回PEM格式信息
*
* @return ukey响应数据,示例: B959CD298E3BA6DAE360FB8CDB87B68B66D8BD221474046232910DD5FDA54354
*/
public static String getSHA1DigestAsPEMParams(SignRequest request) {
Map<String, Object> args = new LinkedHashMap<>();
String initData = SignHandler.getInitData(request);
log.warn("使用卡计算摘要,返回PEM格式信息: \n{}原始报文:\n{}", initData, request.getData());
args.put("szInfo", initData);
args.put("passwd", ObjectUtils.defaultIfNull(SpringUtil.getBean(UkeyProperties.class).getPassword(), DEFAULT_PASSWORD));
UkeyRequest ukeyRequest = new UkeyRequest();
ukeyRequest.set_method("cus-sec_SpcSHA1DigestAsPEM");
ukeyRequest.set_id(request.getId());
ukeyRequest.setArgs(args);
return JacksonUtil.toJson(ukeyRequest);
}
/**
* To park thread
*/
private static void parkThread(AtomicReference<Thread> reference, WebSocketConnectionManager socketConnectionManager) {
socketConnectionManager.start();
try {
LockSupport.parkNanos(reference.get(), 1000 * 1000 * 1000 * 3L);
} catch (Exception e) {
log.error("线程自动unpark异常 {}", e.getLocalizedMessage(), e);
} finally {
socketConnectionManager.stop();
}
}
/**
* Callback used to run the bean.
*
* @param args incoming application arguments
*/
@Override
public void run(ApplicationArguments args) throws IOException {
org.apache.xml.security.Init.init();
if (log.isDebugEnabled()) {
log.debug("XMl output format with C14n Initial Succeed.");
}
}
/**
* IOC中返回一个WebSocket包装类的实例对象
*
* @return WebSocket包装类
*/
@Bean
@Scope(scopeName = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public WebSocketWrapper webSocketWrapper() {
return new WebSocketWrapper();
}
/**
* 基于标准 Java WebSocket API 的 WebSocketClient
*
* @return An instance of StandardWebSocketClient
*/
@Bean
@ConditionalOnMissingBean({StandardWebSocketClient.class})
public StandardWebSocketClient standardWebSocketClient() {
return new StandardWebSocketClient();
}
/**
* 注入一个X509Certificate证书判断的Bean
*
* @return X509Certificate证书判断
*/
@Bean
@SneakyThrows
public CertificateHandler certificateHandler(UkeyProperties ukeyProperties, StandardWebSocketClient standardWebSocketClient) {
CertificateHandler certificateHandler = new CertificateHandler();
Map<String, String> certMap = new ConcurrentHashMap<>();
AtomicReference<Thread> reference = new AtomicReference<>();
reference.set(Thread.currentThread());
// 获取u-key内的证书相关
WebSocketConnectionManager certManager = new WebSocketConnectionManager(standardWebSocketClient, new TextWebSocketHandler() {
final UkeyRequest ukeyRequest = new UkeyRequest("cus-sec_SpcGetSignCertAsPEM", new HashMap<>());
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
session.sendMessage(new TextMessage(JacksonUtil.toJson(ukeyRequest)));
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) {
UkeyResponse response = toBean(message.getPayload(), new TypeReference<>() {
}, true);
try {
if (Objects.equals(response.get_id(), 1)) {
UkeyResponse.Args responseArgs = response.get_args();
if (responseArgs.getResult().equals(true) && CollectionUtils.isNotEmpty(responseArgs.getData())) {
certMap.put(SING_DATA_METHOD, responseArgs.getData().get(0));
log.warn("已从电子口岸u-key中获取到x509Certificate证书: {}", JacksonUtil.toJson(certMap));
}
}
} catch (Exception e) {
log.error("唤醒线程异常 {}", e.getLocalizedMessage(), e);
} finally {
LockSupport.unpark(reference.get());
}
}
}, ukeyProperties.getWsUrl());
parkThread(reference, certManager);
// 获取u-key签发日期, 查看海关证书有效期
WebSocketConnectionManager ukeyValidTimeManager = new WebSocketConnectionManager(standardWebSocketClient, new TextWebSocketHandler() {
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
session.sendMessage(new TextMessage(toJsonStr(new UkeyRequest("cus-sec_SpcGetValidTimeFromCert", 1, new LinkedHashMap<>()))));
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) {
UkeyResponse response = toBean(message.getPayload(), new TypeReference<>() {
}, true);
try {
if (Objects.equals(response.get_id(), 1)) {
UkeyResponse.Args responseArgs = response.get_args();
if (responseArgs.getResult().equals(true) && CollectionUtils.isNotEmpty(responseArgs.getData())) {
log.warn("从电子口岸u-key中获取到u-key有效期区间: {}", responseArgs.getData());
SpcValidTime validTime = toBean(responseArgs.getData().get(0), new TypeReference<>() {
}, true);
certificateHandler.setUkeyValidTimeBegin(validTime.getSzStartTime());
certificateHandler.setUkeyValidTimeEnd(validTime.getSzEndTime());
}
}
} catch (Exception e) {
log.error("唤醒线程异常 {}", e.getLocalizedMessage(), e);
} finally {
LockSupport.unpark(reference.get());
}
}
}, ukeyProperties.getWsUrl());
parkThread(reference, ukeyValidTimeManager);
//自动推断ukey使用的签名算法
WebSocketConnectionManager certSignatureAlgorithmManager = new WebSocketConnectionManager(standardWebSocketClient, new TextWebSocketHandler() {
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
log.info("开始推断ukey使用的签名算法:");
session.sendMessage(new TextMessage(toJsonStr(new UkeyRequest("cus-sec_SpcGetSignCertAsPEM", new LinkedHashMap<>()))));
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) {
UkeyResponse response = toBean(message.getPayload(), new TypeReference<>() {
}, true);
try {
if (Objects.equals(response.get_id(), 1)) {
UkeyResponse.Args responseArgs = response.get_args();
if (responseArgs.getResult().equals(true) && CollectionUtils.isNotEmpty(responseArgs.getData())) {
String x509Certificate = CertificateHandler.buildX509CertificateWithHeader(responseArgs.getData().get(0));
X509Certificate certificateByCertText = CertificateParser.parseCertificateByCertText(x509Certificate);
if (null != SignatureAlgorithm.getSignatureAlgorithmSigAlgName(certificateByCertText.getSigAlgName())) {
certificateHandler.setAlgorithm(SignatureAlgorithm.getSignatureAlgorithmSigAlgName(certificateByCertText.getSigAlgName()));
}
}
}
} catch (Exception e) {
log.error("唤醒线程异常 {}", e.getLocalizedMessage(), e);
} finally {
LockSupport.unpark(reference.get());
}
}
}, ukeyProperties.getWsUrl());
parkThread(reference, certSignatureAlgorithmManager);
certificateHandler.setX509Map(certMap);
return certificateHandler;
}
/**
* @return An instance of WebSocketClientHandler
*/
@Bean
@ConditionalOnMissingBean({WebSocketClientHandler.class})
public WebSocketClientHandler webSocketClientHandler(UkeyProperties ukeyProperties, WebSocketWrapper webSocketWrapper, CertificateHandler certificateHandler) {
return new WebSocketClientHandler(ukeyProperties, webSocketWrapper, certificateHandler);
}
}
@@ -0,0 +1,29 @@
package cn.alphahub.eport.signature.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
import static cn.alphahub.eport.signature.config.UkeyProperties.PREFIX;
/**
* 电子口岸u-key的配置参数
*
* @author weasley
* @version 1.0
* @date 2022/2/13
*/
@Data
@Validated
@ConfigurationProperties(prefix = PREFIX)
public class UkeyProperties {
public static final String PREFIX = "eport.signature.ukey";
/**
* socket链接url
*/
private String wsUrl = "ws://127.0.0.1:61232";
/**
* u-key密码
*/
private String password = "88888888";
}
@@ -0,0 +1,197 @@
package cn.alphahub.eport.signature.config;
import cn.alphahub.eport.signature.base.exception.EportWebClientException;
import cn.alphahub.eport.signature.core.web.EportCebMessageHttpClient;
import cn.alphahub.eport.signature.core.web.EportCustoms179HttpClient;
import cn.alphahub.eport.signature.core.web.EportReportResultHttpClient;
import cn.alphahub.eport.signature.core.web.WebfluxDemoHttpClient;
import cn.alphahub.eport.signature.support.OriginalPropertyNamingStrategy;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.tomcat.util.codec.binary.Base64;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.codec.json.Jackson2JsonEncoder;
import org.springframework.web.reactive.function.client.ClientRequest;
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
import org.springframework.web.reactive.function.client.ExchangeStrategies;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.support.WebClientAdapter;
import org.springframework.web.service.invoker.HttpServiceProxyFactory;
import reactor.core.publisher.Mono;
import java.util.Objects;
import java.util.function.Consumer;
import static cn.alphahub.eport.signature.core.ChinaEportReportClient.EPORT_CEBMESSAGE_SERVER_ENCODE;
import static cn.alphahub.eport.signature.core.ChinaEportReportClient.REPORT_PROD_ENV_179_URL_ENCODE;
/**
* The WebClient Configuration
*
* @author weasley
* @version 1.0.0
*/
@Slf4j
@Configuration
@EnableConfigurationProperties({Customs179Properties.class, ChinaEportProperties.class})
public class WebClientConfiguration {
public static final ThreadLocal<Consumer<HttpHeaders>> HTTP_HEADERS = new ThreadLocal<>();
/**
* @return A proxy bean of EportCebMessageHttpClient
*/
@Bean
public EportCebMessageHttpClient eportCebMessageHttpClient(ObjectMapper objectMapper, ChinaEportProperties chinaEportProperties) {
WebClient webClient = WebClient.builder()
.exchangeStrategies(customExchangeStrategies(objectMapper))
.filter(((request, next) -> next.exchange(Objects.requireNonNull(injectHeader(request).block()))))
.filters(exchangeFilters -> {
exchangeFilters.add(logRequest());
exchangeFilters.add(logResponse());
})
.defaultStatusHandler(HttpStatusCode::isError, resp -> Mono.just(new EportWebClientException("Web Client 调用发生异常!")))
.baseUrl(StringUtils.defaultIfBlank(chinaEportProperties.getServer(), new String(Base64.decodeBase64(EPORT_CEBMESSAGE_SERVER_ENCODE))))
.build();
HttpServiceProxyFactory httpServiceProxyFactory = HttpServiceProxyFactory.builder(WebClientAdapter.forClient(webClient))
.build();
return httpServiceProxyFactory.createClient(EportCebMessageHttpClient.class);
}
/**
* @return A proxy bean of EportCustoms179HttpClient
*/
@Bean
public EportCustoms179HttpClient eportCustoms179HttpClient(ObjectMapper objectMapper, Customs179Properties customs179Properties) {
WebClient webClient = WebClient.builder()
.exchangeStrategies(ExchangeStrategies.builder().codecs(configurer -> {
configurer.defaultCodecs().jackson2JsonEncoder(new Jackson2JsonEncoder(enhanceSourceMapper(objectMapper)));
}).build())
.filters(exchangeFilters -> {
exchangeFilters.add(((request, next) -> next.exchange(Objects.requireNonNull(injectHeader(request).block()))));
exchangeFilters.add(logRequest());
exchangeFilters.add(logResponse());
})
.defaultStatusHandler(HttpStatusCode::isError, resp -> Mono.just(new EportWebClientException("Web Client 调用发生异常!")))
.baseUrl(StringUtils.defaultIfBlank(customs179Properties.getServer(), new String(Base64.decodeBase64(REPORT_PROD_ENV_179_URL_ENCODE))))
.build();
HttpServiceProxyFactory httpServiceProxyFactory = HttpServiceProxyFactory.builder(WebClientAdapter.forClient(webClient)).build();
return httpServiceProxyFactory.createClient(EportCustoms179HttpClient.class);
}
/**
* @return A proxy bean of EportReportResultHttpClient
*/
@Bean
public EportReportResultHttpClient eportReportResultHttpClient(ChinaEportProperties chinaEportProperties) {
String baseUrl = StringUtils.defaultIfBlank(chinaEportProperties.getServer(), new String(Base64.decodeBase64(EPORT_CEBMESSAGE_SERVER_ENCODE)));
WebClient webClient = WebClient.builder()
.filter(logRequest())
.filter(logResponse())
.defaultStatusHandler(HttpStatusCode::isError, resp -> Mono.just(new EportWebClientException("Web Client 调用发生异常!")))
.baseUrl(baseUrl)
.build();
return HttpServiceProxyFactory.builder(WebClientAdapter.forClient(webClient)).build()
.createClient(EportReportResultHttpClient.class);
}
/**
* @return A proxy bean of WebfluxDemoHttpClient
*/
@Bean
public WebfluxDemoHttpClient webfluxDemoHttpClient(ObjectMapper objectMapper) {
WebClient webClient = WebClient.builder()
.exchangeStrategies(customExchangeStrategies(objectMapper))
.filters(exchangeFilters -> {
exchangeFilters.add(((request, next) -> next.exchange(Objects.requireNonNull(injectHeader(request).block()))));
exchangeFilters.add(logRequest());
exchangeFilters.add(logResponse());
})
.defaultStatusHandler(HttpStatusCode::isError, resp -> Mono.just(new EportWebClientException("Web Client 调用发生异常!")))
.baseUrl("http://127.0.0.1:8080")
.build();
WebClientAdapter clientAdapter = WebClientAdapter.forClient(webClient);
HttpServiceProxyFactory httpServiceProxyFactory = HttpServiceProxyFactory.builder(clientAdapter).build();
return httpServiceProxyFactory.createClient(WebfluxDemoHttpClient.class);
}
/**
* To inject headers
*/
public Mono<ClientRequest> injectHeader(final ClientRequest clientRequest) {
if (null != HTTP_HEADERS.get()) {
Mono<ClientRequest> clientRequestMono = Mono.just(ClientRequest.from(clientRequest)
.headers(HTTP_HEADERS.get())
.build());
clearHttpHeaders();
return clientRequestMono;
}
return Mono.just(ClientRequest.from(clientRequest)
.build());
}
/**
* Clear Http Headers
*/
public void clearHttpHeaders() {
if (null != HTTP_HEADERS.get()) {
HTTP_HEADERS.remove();
}
}
/**
* Custom exchange strategies
*/
public ExchangeStrategies customExchangeStrategies(ObjectMapper objectMapper) {
return ExchangeStrategies.builder()
.codecs(configurer -> {
configurer.defaultCodecs().jackson2JsonEncoder(new Jackson2JsonEncoder(enhanceSourceMapper(objectMapper)));
}).build();
}
/**
* To enhance mapper
*/
private ObjectMapper enhanceSourceMapper(ObjectMapper objectMapper) {
ObjectMapper enhanceMapper = objectMapper.copy();
enhanceMapper.setPropertyNamingStrategy(new OriginalPropertyNamingStrategy());
return enhanceMapper;
}
/**
* log web request
*/
private ExchangeFilterFunction logRequest() {
return ExchangeFilterFunction.ofRequestProcessor(clientRequest -> {
if (log.isInfoEnabled()) {
log.info("Request URL: " + clientRequest.url());
log.info("Request Method: " + clientRequest.method());
log.info("Request Headers: " + clientRequest.headers());
if (clientRequest.method() == HttpMethod.POST || clientRequest.method() == HttpMethod.PUT) {
log.info("Request Body: " + clientRequest.body());
}
}
return Mono.just(clientRequest);
});
}
/**
* log web response
*/
private ExchangeFilterFunction logResponse() {
return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> {
if (log.isInfoEnabled()) {
log.info("Response status: {}", clientResponse.statusCode());
log.info("Response headers: {}", clientResponse.headers().asHttpHeaders());
}
return Mono.just(clientResponse);
});
}
}
@@ -0,0 +1,80 @@
package cn.alphahub.eport.signature.controller.rpc;
import cn.alphahub.eport.signature.base.domain.Result;
import cn.alphahub.eport.signature.config.ChinaEportProperties;
import cn.alphahub.eport.signature.core.web.EportReportResultHttpClient;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* 查询申报回执结果
*
* @author weasley
* @version 1.1.0
* @apiNote 目前仅实现:CEB311Message、CEB621Message 申报结果查询
*/
@Slf4j
@RestController
@RequestMapping("/rpc/eport/result")
public class EportReportResultController {
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
@Autowired
private ChinaEportProperties chinaEportProperties;
@Autowired
private EportReportResultHttpClient eportReportResultHttpClient;
/**
* 查询311进口单申报结果
*
* @param qryId 查询时间,日期时间格式:yyyyMMddHHmmss
* @response {
* "message": "操作成功",
* "success": true,
* "timestamp": "2023-08-09 15:18:25",
* "code": 200,
* "data": "[{org_id=1, instm=1691397836000, re_order_guid=NWIVQA-WEASLEY-20230807164347-GCVZ4S, returnstatus=2, ebpcode=46016602EV, returntime2=20230807164356, ebccode=46016602EV, trans_dxpid=DXPENT0000530815, returninfo=新增申报成功[NWIVQA-WEASLEY-20230807164347-GCVZ4S], returntime=1691397830000, orderno=JO_D8079556739228400022}]"
* }
* @apiNote 查询此时间点之后的数据,必须在当前时间点之前,默认查询当前时间前15秒的申报数据,当前时间:20230806144417,则 qryId=20230806144401,不传的情况适用于刚刚申报成功查询回执结果,海关的处理时间间隔约为15秒
* @since 1.1.1
*/
@GetMapping("/ceb312msg")
public Result<String> getCeb312msgResult(@RequestParam(required = false) @DateTimeFormat(pattern = "yyyyMMddHHmmss") LocalDateTime qryId) {
qryId = ObjectUtils.defaultIfNull(qryId, LocalDateTime.now().minusSeconds(15));
log.info("开始查询311申报回执结果, qryId {}", FORMATTER.format(qryId));
Mono<String> msgResult = eportReportResultHttpClient.getCeb312msgResult(chinaEportProperties.getDxpId(), FORMATTER.format(qryId));
return Result.ok(msgResult.block());
}
/**
* 查询621进口单申报结果
*
* @param qryId 查询时间,日期时间格式:yyyyMMddHHmmss
* @response {
* "message": "操作成功",
* "success": true,
* "timestamp": "2023-08-09 15:26:53",
* "code": 200,
* "data": "[{org_id=1, instm=1691397837000, agentcode=46016602EV, re_elist_guid_cus=9422f96b-ccbc-4a69-a899-6d9dd583d501, returnstatus=2, ebpcode=46016602EV, preno=B20230807570793612, returntime2=20230807164357, ebccode=46016602EV, createtime=20230807164351, trans_dxpid=DXPENT0000530815, returninfo=清单新增申报成功[3CQHDG-WEASLEY-20230807164348-7KYRQN][电商企业编码:46016602EV订单编号:JO_D8079556739228400022], returntime=1691397830000, re_elist_guid=3CQHDG-WEASLEY-20230807164348-7KYRQN, customscode=6409}, {org_id=1, instm=1691397847000, agentcode=46016602EV, re_elist_guid_cus=9422f96b-ccbc-4a69-a899-6d9dd583d501, returnstatus=120, ebpcode=46016602EV, preno=B20230807570793612, returntime2=20230807164407, ebccode=46016602EV, createtime=20230807164355, trans_dxpid=DXPENT0000530815, returninfo=[Code:1800;Desc:逻辑校验通过], returntime=1691397831000, re_elist_guid=4e7a2dab-064b-410b-9ce6-e322c590ae3d, customscode=6409, invtno=64092023I000722460}, {org_id=1, instm=1691397847000, agentcode=46016602EV, re_elist_guid_cus=9422f96b-ccbc-4a69-a899-6d9dd583d501, returnstatus=800, ebpcode=46016602EV, preno=B20230807570793612, returntime2=20230807164407, ebccode=46016602EV, createtime=20230807164355, trans_dxpid=DXPENT0000530815, returninfo=[Code:2600;Desc:放行], returntime=1691397832000, re_elist_guid=4e7a2dab-064b-410b-9ce6-e322c590ae3d, customscode=6409, invtno=64092023I000722460}]"
* }
* @apiNote 查询此时间点之后的数据,必须在当前时间点之前,默认查询当前时间前15秒的申报数据,当前时间:20230806144417,则 qryId=20230806144401,不传的情况适用于刚刚申报成功查询回执结果,海关的处理时间间隔约为15秒
* @since 1.1.1
*/
@GetMapping("/ceb622msg")
public Result<String> getCe622msgResult(@RequestParam(required = false) @DateTimeFormat(pattern = "yyyyMMddHHmmss") LocalDateTime qryId) {
qryId = ObjectUtils.defaultIfNull(qryId, LocalDateTime.now().minusSeconds(15));
log.info("开始查询621进口单申报回执结果, qryId {}", FORMATTER.format(qryId));
Mono<String> msgResult = eportReportResultHttpClient.getCe622msgResult(chinaEportProperties.getDxpId(), FORMATTER.format(qryId));
return Result.ok(msgResult.block());
}
}
@@ -0,0 +1,130 @@
package cn.alphahub.eport.signature.core;
import cn.alphahub.eport.signature.config.SignatureAlgorithm;
import cn.hutool.core.date.LocalDateTimeUtil;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.Map;
import static cn.alphahub.eport.signature.config.SignatureAlgorithm.RSA_SHA1;
/**
* X509Certificate证书
*
* @author weasley
* @version 1.0
* @date 2022/2/13
*/
@Data
@Slf4j
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class CertificateHandler implements Serializable {
/**
* 从u-key读取含发送推送 X509Certificate 证书对应的加签方法
*/
public static final String SING_DATA_METHOD = "cus-sec_SpcSignDataAsPEM";
/**
* 暂定2022-07-01为第一个时间分界
*/
public static final LocalDateTime DATE_TIME_202207 = LocalDateTimeUtil.parse("2022-07-01", "yyyy-MM-dd");
/**
* u-key的.cer证书是否存在
*/
private Boolean certExists = false;
/**
* XML 签名算法, RSA_SHA1 兜底
*/
private SignatureAlgorithm algorithm = RSA_SHA1;
/**
* 判断用那一个证书的集合
* <ul>
* <li>key -> 从u-key读取<ds:SignatureValue>的方法名</li>
* <li>value -> 从u-key读取<ds:SignatureValue>的方法名对应的X509Certificate证书</li>
* </ul>
*/
@Getter
private Map<String, String> x509Map;
/**
* 海关u-key证书有效期开始
*/
private LocalDateTime ukeyValidTimeBegin;
/**
* 海关u-key证书有效期结束
*/
private LocalDateTime ukeyValidTimeEnd;
/**
* 组装X509Certificate证书
* <p>
* 组装64位长度一行的pem分割的pem
*
* @param certPomFromUkey 从ukey里面读取的pem
* @return 64位长度一行的pem分割的pem
* @apiNote 不带Header
*/
public static String buildX509CertificateWithoutHeader(String certPomFromUkey) {
certPomFromUkey = certPomFromUkey.trim();
StringBuilder sb = new StringBuilder();
int count = 0;
for (char c : certPomFromUkey.toCharArray()) {
sb.append(c);
count++;
if (count == 64) {
sb.append("\n");
count = 0;
}
}
return sb.toString().trim();
}
/**
* 组装X509Certificate证书
* <p>
* 组装64位长度一行的pem分割的pem
*
* @param certPomFromUkey 从ukey里面读取的pem
* @return 64位长度一行的pem分割的pem
* @apiNote 带Header
*/
public static String buildX509CertificateWithHeader(String certPomFromUkey) {
return "-----BEGIN CERTIFICATE-----\n"
.concat(buildX509CertificateWithoutHeader(certPomFromUkey)).concat("\n")
.concat("-----END CERTIFICATE-----");
}
public LocalDateTime getUkeyValidTimeBegin() {
if (ukeyValidTimeBegin == null) {
return LocalDateTime.now();
}
return ukeyValidTimeBegin;
}
public void setX509Map(Map<String, String> x509Map) {
this.x509Map = x509Map;
}
/**
* 获取509Certificate证书
*
* @param method 发送给电子口岸u-key的入参的[_method]字段的值
* @return 509Certificate证书
*/
public String getX509Certificate(String method) {
return switch (method) {
case SING_DATA_METHOD -> this.getX509Map().get(SING_DATA_METHOD);
default -> throw new IllegalStateException("Unexpected value: " + method);
};
}
}
@@ -0,0 +1,476 @@
package cn.alphahub.eport.signature.core;
import cn.alphahub.eport.signature.base.exception.SignException;
import cn.alphahub.eport.signature.config.ChinaEportProperties;
import cn.alphahub.eport.signature.config.Customs179Properties;
import cn.alphahub.eport.signature.config.UkeyProperties;
import cn.alphahub.eport.signature.entity.Capture179DataRequest;
import cn.alphahub.eport.signature.entity.Capture179DataResponse;
import cn.alphahub.eport.signature.entity.SignRequest;
import cn.alphahub.eport.signature.entity.SignResult;
import cn.alphahub.eport.signature.entity.ThirdAbstractResponse;
import cn.alphahub.eport.signature.entity.UkeyRequest;
import cn.alphahub.eport.signature.entity.UkeyResponse.Args;
import cn.alphahub.eport.signature.entity.UploadCEBMessageRequest;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.lang.TypeReference;
import cn.hutool.http.ContentType;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONUtil;
import io.github.weasleyj.china.eport.sign.AbstractCebMessage;
import io.github.weasleyj.china.eport.sign.IMessageType;
import io.github.weasleyj.china.eport.sign.model.cebmessage.BaseTransfer;
import io.github.weasleyj.china.eport.sign.model.cebmessage.CEB311Message;
import io.github.weasleyj.china.eport.sign.model.cebmessage.CEB621Message;
import io.github.weasleyj.china.eport.sign.model.cebmessage.export.CEB303Message;
import io.github.weasleyj.china.eport.sign.model.customs179.Customs179Request;
import io.github.weasleyj.china.eport.sign.model.request.Message;
import io.github.weasleyj.china.eport.sign.model.request.MessageBody;
import io.github.weasleyj.china.eport.sign.model.request.MessageHead;
import io.github.weasleyj.china.eport.sign.model.request.MessageRequest;
import io.github.weasleyj.china.eport.sign.model.signature.CanonicalizationMethod;
import io.github.weasleyj.china.eport.sign.model.signature.DigestMethod;
import io.github.weasleyj.china.eport.sign.model.signature.KeyInfo;
import io.github.weasleyj.china.eport.sign.model.signature.Reference;
import io.github.weasleyj.china.eport.sign.model.signature.Signature;
import io.github.weasleyj.china.eport.sign.model.signature.SignatureMethod;
import io.github.weasleyj.china.eport.sign.model.signature.SignedInfo;
import io.github.weasleyj.china.eport.sign.model.signature.Transforms;
import io.github.weasleyj.china.eport.sign.model.signature.X509Data;
import io.github.weasleyj.china.eport.sign.util.GUIDUtil;
import io.github.weasleyj.china.eport.sign.util.JAXBUtil;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import static cn.alphahub.dtt.plus.util.JacksonUtil.readValue;
import static cn.alphahub.dtt.plus.util.JacksonUtil.toJson;
import static cn.alphahub.eport.signature.core.CertificateHandler.SING_DATA_METHOD;
/**
* 电子口岸上报客户端
*
* @author weasley
* @since 1.1.0
*/
@Slf4j
@Component
@AllArgsConstructor
public class ChinaEportReportClient {
/**
* 海关服务器地址,格式: http://ip:port
*
* @apiNote base64加密下,不适合直接公布到公网
*/
public static final String EPORT_CEBMESSAGE_SERVER_ENCODE = "aHR0cDovLzM2LjEwMS4yMDguMjMwOjgwOTA=";
/**
* 海关 179 数据抓取测试环境 URL (base64加密,不适合直接公布到外网)
*/
public static final String REPORT_TEST_ENV_179_URL_ENCODE = "aHR0cHM6Ly9zd2FwcHRlc3Quc2luZ2xld2luZG93LmNuL2NlYjJncmFiL2dyYWIvcmVhbFRpbWVEYXRhVXBsb2Fk";
/**
* 海关 179 数据抓取生产环境 URL (base64加密,不适合直接公布到外网)
*/
public static final String REPORT_PROD_ENV_179_URL_ENCODE = "aHR0cHM6Ly9jdXN0b21zLmNoaW5hcG9ydC5nb3YuY24vY2ViMmdyYWIvZ3JhYi9yZWFsVGltZURhdGFVcGxvYWQ=";
private final SignHandler signHandler;
private final UkeyProperties ukeyProperties;
private final Customs179Properties customs179Properties;
private final ChinaEportProperties chinaEportProperties;
/**
* 构建 ds:Signature 节点
*
* @param signResult 加签结果
* @return ds:Signature 节点
*/
public static String buildSignatureNode(SignResult signResult) {
KeyInfo keyInfo = KeyInfo.builder()
.keyName(signResult.getCertNo())
.x509Data(new X509Data(signResult.getX509Certificate()))
.build();
Reference reference = Reference.builder()
.digestMethod(new DigestMethod())
.uri("")
.transforms(new Transforms())
.digestValue(signResult.getDigestValue())
.build();
SignedInfo signedInfo = SignedInfo.builder()
.CanonicalizationMethod(new CanonicalizationMethod())
.SignatureMethod(new SignatureMethod().setAlgorithm(SignatureHandler.getSignatureMethodAlgorithm()))
.reference(reference)
.build();
Signature signature = Signature.builder()
.keyInfo(keyInfo)
.signatureValue(signResult.getSignatureValue())
.signedInfo(signedInfo)
.build();
return JAXBUtil.toXml(signature);
}
/**
* 构建 ds:SignedInfo 节点
*
* @param signResult 加签结果
* @return ds:SignedInfo 节点
*/
public static String buildSignedInfoNode(SignResult signResult) {
Reference reference = Reference.builder()
.digestMethod(new DigestMethod())
.uri("")
.transforms(new Transforms())
.digestValue(signResult.getDigestValue())
.build();
SignedInfo signedInfo = SignedInfo.builder()
.CanonicalizationMethod(new CanonicalizationMethod())
.SignatureMethod(new SignatureMethod().setAlgorithm(SignatureHandler.getSignatureMethodAlgorithm()))
.reference(reference)
.build();
return JAXBUtil.toXml(signedInfo);
}
/**
* CebXxxMessage数据上报海关
*
* @param cebMessage 原始xml对象
* @param messageType CEB311Message|CEB621Message|...
* @return 推送结果, OK: 表示成功, 报关结果需要自己查询回执
*/
public ThirdAbstractResponse<MessageRequest, String, String> report(AbstractCebMessage cebMessage, IMessageType messageType) {
log.info("数据上报海关xml入参: {}, {}", JSONUtil.toJsonStr(cebMessage), messageType.getType());
MessageRequest messageRequest = buildMessageRequest(cebMessage, messageType);
String requestServer = Base64.decodeStr(EPORT_CEBMESSAGE_SERVER_ENCODE);
requestServer = StringUtils.defaultIfBlank(chinaEportProperties.getServer(), requestServer);
String requestBody = JSONUtil.toJsonStr(messageRequest);
log.info("数据上报海关请求入参 {}\nRequest Server: {}", requestBody, requestServer);
HttpResponse httpResponse = HttpUtil.createPost(requestServer + "/cebcmsg")
.contentType(ContentType.JSON.getValue())
.body(requestBody)
.execute();
String responseBody = httpResponse.body();
log.info("数据上报Http响应结果: {}", responseBody);
if (!"OK".equals(responseBody)) {
log.error("上报海关请求异常, 请求入参: {}\n海关Http响应: {}", requestBody, responseBody);
}
ThirdAbstractResponse<MessageRequest, String, String> thirdResponse = ThirdAbstractResponse.getInstance();
thirdResponse.setPayload(messageRequest);
thirdResponse.setOriginal(responseBody);
thirdResponse.setExpected("OK");
return thirdResponse;
}
/**
* 海关 179 数据抓取
* <p>
* <ul>
* <li>
* 期望的成功数据返回示例
* <pre>
* {"code":"10000","message":"","serviceTime":1567050097628}
* </pre>
* </li>
* </ul>
*/
public ThirdAbstractResponse<Map<String, Object>, String, Capture179DataResponse> capture179Data(Capture179DataRequest request) {
log.info("海关 179 数据抓取 {}", JSONUtil.toJsonStr(request));
Customs179Request customs179Request = new Customs179Request();
customs179Request.setSessionID(request.getSessionID());
customs179Request.setPayExchangeInfoHead(request.getPayExchangeInfoHead());
customs179Request.setPayExchangeInfoLists(request.getPayExchangeInfoLists());
customs179Request.setServiceTime(request.getServiceTime());
String guid = GUIDUtil.getGuid();
customs179Request.setSessionID(guid);
customs179Request.getPayExchangeInfoHead().setGuid(guid);
customs179Request.getPayExchangeInfoHead().setEbpCode(customs179Properties.getEbpCode());
String testEnvServerUrl = Base64.decodeStr(REPORT_TEST_ENV_179_URL_ENCODE);
String prodEnvServerUrl = Base64.decodeStr(REPORT_PROD_ENV_179_URL_ENCODE);
String requestUrl = StringUtils.defaultIfBlank(customs179Properties.getServer(), prodEnvServerUrl);
String capture179DataUkeyRequest = "\"sessionID\":\"" + customs179Request.getSessionID() + "\"||" +
"\"payExchangeInfoHead\":\"" + toJson(customs179Request.getPayExchangeInfoHead()) + "\"||" +
"\"payExchangeInfoLists\":\"" + toJson(customs179Request.getPayExchangeInfoLists()) + "\"||" +
"\"serviceTime\":" + "\"" + customs179Request.getServiceTime() + "\"";
@SuppressWarnings("all") UkeyRequest ukeyRequest = new UkeyRequest(SING_DATA_METHOD, new HashMap<>() {{
put("inData", capture179DataUkeyRequest);
put("passwd", ukeyProperties.getPassword());
}});
String signParams = toJson(ukeyRequest);
SignRequest signRequest = new SignRequest(capture179DataUkeyRequest);
SignResult signResult = signHandler.sign(signRequest, signParams);
if (Boolean.FALSE.equals(signResult.getSuccess())) {
throw new SignException("海关 179 数据抓取加签失败, ukey加签入参: " + signParams + ", 原始入参: " + toJson(customs179Request));
}
customs179Request.setCertNo(signResult.getCertNo());
customs179Request.setSignValue(signResult.getSignatureValue());
Map<String, Object> formData = new LinkedHashMap<>();
formData.put("payExInfoStr", toJson(customs179Request));
log.info("海关179数据抓取URL: {}\n开始上报,请求数据:{}", requestUrl, toJson(formData));
HttpResponse httpResponse = HttpUtil.createPost(requestUrl)
.form(formData)
.header("Content-Type", "application/x-www-form-urlencoded", true)
.timeout(5 * 1000)
.execute();
log.info("海关 179 数据抓取返回结果 {}", httpResponse.body());
Capture179DataResponse expected = readValue("""
{"code":"10000","message":"上传成功","total":0,"serviceTime":null}
""", Capture179DataResponse.class);
expected.setServiceTime(Long.parseLong(customs179Request.getServiceTime()));
ThirdAbstractResponse<Map<String, Object>, String, Capture179DataResponse> thirdResponse = ThirdAbstractResponse.getInstance();
thirdResponse.setPayload(formData);
thirdResponse.setOriginal(httpResponse.body());
thirdResponse.setExpected(expected);
return thirdResponse;
}
/**
* 推断CebMessage的具体的XML入参解析成Java对象
*
* @implNote 已实现: CEB311Message,CEB621Message,CEB303Message
*/
@SuppressWarnings("all")
public AbstractCebMessage buildCebMessage(UploadCEBMessageRequest request) {
String guid = GUIDUtil.getGuid();
String cebMessage = String.valueOf(request.getCebMessage());
switch (request.getDataType()) {
case XML -> {
return switch (request.getMessageType()) {
// 进口单XML报文解析
case CEB311Message -> {
CEB311Message ceb311Message = Objects.requireNonNull(JAXBUtil.toBean(cebMessage, CEB311Message.class));
ceb311Message.setGuid(guid);
ceb311Message.getOrder().getOrderHead().setGuid(guid);
buildBaseTransfer(ceb311Message.getBaseTransfer());
yield ceb311Message;
}
case CEB312Message -> null;
case CEB411Message -> null;
case CEB412Message -> null;
case CEB511Message -> null;
case CEB512Message -> null;
case CEB513Message -> null;
case CEB514Message -> null;
case CEB621Message -> {
CEB621Message ceb621Message = Objects.requireNonNull(JAXBUtil.toBean(cebMessage, CEB621Message.class));
ceb621Message.setGuid(guid);
ceb621Message.getInventory().getInventoryHead().setGuid(guid);
buildBaseTransfer(ceb621Message.getBaseTransfer());
yield ceb621Message;
}
case CEB622Message -> null;
case CEB623Message -> null;
case CEB624Message -> null;
case CEB625Message -> null;
case CEB626Message -> null;
case CEB711Message -> null;
case CEB712Message -> null;
case CEB213Message -> null;
case CEB214Message -> null;
case CEB215Message -> null;
case CEB216Message -> null;
case CEB303Message -> {
CEB303Message ceb303Message = Objects.requireNonNull(JAXBUtil.toBean(cebMessage, CEB303Message.class));
ceb303Message.setGuid(guid);
ceb303Message.getOrder().getOrderHead().setGuid(guid);
buildBaseTransfer(ceb303Message.getBaseTransfer());
yield ceb303Message;
}
// 出口单XML报文
case CEB304Message -> null;
case CEB403Message -> null;
case CEB404Message -> null;
case CEB505Message -> null;
case CEB506Message -> null;
case CEB507Message -> null;
case CEB508Message -> null;
case CEB509Message -> null;
case CEB510Message -> null;
case CEB603Message -> null;
case CEB604Message -> null;
case CEB605Message -> null;
case CEB606Message -> null;
case CEB607Message -> null;
case CEB608Message -> null;
case CEB701Message -> null;
case CEB702Message -> null;
case CEB792Message -> null;
};
}
case JSON -> {
return switch (request.getMessageType()) {
case CEB311Message -> {
CEB311Message ceb311Message = JSONUtil.toBean(cebMessage, new TypeReference<>() {
}, false);
ceb311Message.setGuid(guid);
ceb311Message.getOrder().getOrderHead().setGuid(guid);
buildBaseTransfer(ceb311Message.getBaseTransfer());
yield ceb311Message;
}
// 进口单JSON报文解析
case CEB312Message -> null;
case CEB411Message -> null;
case CEB412Message -> null;
case CEB511Message -> null;
case CEB512Message -> null;
case CEB513Message -> null;
case CEB514Message -> null;
case CEB621Message -> {
CEB621Message ceb621Message = JSONUtil.toBean(cebMessage, new TypeReference<>() {
}, false);
ceb621Message.setGuid(guid);
ceb621Message.getInventory().getInventoryHead().setGuid(guid);
buildBaseTransfer(ceb621Message.getBaseTransfer());
yield ceb621Message;
}
case CEB622Message -> null;
case CEB623Message -> null;
case CEB624Message -> null;
case CEB625Message -> null;
case CEB626Message -> null;
case CEB711Message -> null;
case CEB712Message -> null;
case CEB213Message -> null;
case CEB214Message -> null;
case CEB215Message -> null;
case CEB216Message -> null;
// 出口单JSON报文解析
case CEB303Message -> {
CEB303Message ceb303Message = JSONUtil.toBean(cebMessage, new TypeReference<>() {
}, false);
ceb303Message.setGuid(guid);
ceb303Message.getOrder().getOrderHead().setGuid(guid);
buildBaseTransfer(ceb303Message.getBaseTransfer());
yield ceb303Message;
}
case CEB304Message -> null;
case CEB403Message -> null;
case CEB404Message -> null;
case CEB505Message -> null;
case CEB506Message -> null;
case CEB507Message -> null;
case CEB508Message -> null;
case CEB509Message -> null;
case CEB510Message -> null;
case CEB603Message -> null;
case CEB604Message -> null;
case CEB605Message -> null;
case CEB606Message -> null;
case CEB607Message -> null;
case CEB608Message -> null;
case CEB701Message -> null;
case CEB702Message -> null;
case CEB792Message -> null;
};
}
default -> throw new IllegalStateException("Unexpected value: " + toJson(request));
}
}
/**
* Build BaseTransfer XML Node
*/
@SuppressWarnings("all")
public BaseTransfer buildBaseTransfer(BaseTransfer baseTransfer) {
baseTransfer.setCopCode(StringUtils.defaultIfBlank(baseTransfer.getCopCode(), chinaEportProperties.getCopCode()));
baseTransfer.setCopName(StringUtils.defaultIfBlank(baseTransfer.getCopName(), chinaEportProperties.getCopName()));
baseTransfer.setDxpId(StringUtils.defaultIfBlank(baseTransfer.getDxpId(), chinaEportProperties.getDxpId()));
baseTransfer.setDxpMode(StringUtils.defaultIfBlank(baseTransfer.getDxpMode(), "DXP"));
return baseTransfer;
}
/**
* 组装最终请求数据,完成末三段加密
*/
public MessageRequest buildMessageRequest(AbstractCebMessage cebMessage, IMessageType messageType) {
if (cebMessage == null) {
return new MessageRequest();
}
String sourceXml = JAXBUtil.toXml(cebMessage);
MessageHead messageHead = new MessageHead();
messageHead.setMessageType(messageType.getType() + ".xml");
messageHead.setSendTime(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
sourceXml = sourceXml.replaceAll("xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\"", "");
SignRequest ukeyRequest = new SignRequest();
ukeyRequest.setId(1);
ukeyRequest.setData(sourceXml);
// 请求签名加密,2次组装数据, 请求加密(包含第1,2段加密)
String payload = signHandler.getSignDataParameter(ukeyRequest);
SignResult signResult = signHandler.sign(ukeyRequest, payload);
// 线程uppark时间可能会失败,增强方法兜底
if (Boolean.FALSE.equals(signResult.getSuccess())) {
this.enhanceSignResultIfDefective(signResult, sourceXml);
}
if (Boolean.FALSE.equals(signResult.getSuccess())) {
throw new SignException("CebXxxMessage XML数据加签失败, ukey加签入参: " + payload + ", 原始入参: " + toJson(ukeyRequest));
}
String xmlBody = buildXml(signResult, sourceXml, messageType);
// 第3次组装数据, 将加签后的 XM 进行 base64 加密
String encodedXml = Base64.encode(xmlBody);
MessageBody messageBody = MessageBody.builder().data(encodedXml).build();
Message message = Message.builder().MessageBody(messageBody).MessageHead(messageHead).build();
return new MessageRequest(message);
}
/**
* 增强 SignResult, 如果有缺陷: 如: 验签失败等场景
*
* @param signResult 签名结果
* @param sourceXml 原始XML
* @return 签名结果
* @apiNote 用于 SignHandler#sign 结果出现验签失败时的兜底手段
*/
@SuppressWarnings("all")
private SignResult enhanceSignResultIfDefective(SignResult signResult, String sourceXml) {
String signatureXml = SignHandler.getInitData(new SignRequest(sourceXml));
Map<String, Object> params = new HashMap<>();
params.put("inData", signatureXml);
params.put("passwd", ukeyProperties.getPassword());
Args args1 = signHandler.getUkeyResponseArgs(new UkeyRequest("cus-sec_SpcSignDataAsPEM", params));
Args args2 = signHandler.getUkeyResponseArgs(new UkeyRequest("cus-sec_SpcGetSignCertAsPEM", new HashMap<>()));
signResult.setCertNo(args1.getData().get(1));
signResult.setSignatureValue(args1.getData().get(0));
signResult.setX509Certificate(args2.getData().get(0));
if (signResult.getSuccess().equals(false)) {
signResult.setSuccess(true);
}
return signResult;
}
/**
* 组装Signature XML节点
*
* @param sourceXml 原始XML
*/
private String buildXml(SignResult signResult, String sourceXml, IMessageType messageType) {
String signatureNode = buildSignatureNode(signResult);
String xmlStart = "</ceb:" + messageType.getType() + ">";
int index = sourceXml.indexOf(xmlStart);
StringBuilder builder = new StringBuilder(sourceXml);
builder.insert(index, signatureNode.concat("\n"));
String finalXml = builder.toString();
if (finalXml.startsWith("<?xml ")) {
finalXml = finalXml.replace("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n", "");
}
log.info("上报海关的XML报文:\n{}", finalXml);
return finalXml;
}
}
@@ -0,0 +1,173 @@
package cn.alphahub.eport.signature.core;
import cn.alphahub.eport.signature.config.UkeyInitialConfig;
import cn.alphahub.eport.signature.config.UkeyProperties;
import cn.alphahub.eport.signature.entity.SignRequest;
import cn.alphahub.eport.signature.entity.SignResult;
import cn.alphahub.eport.signature.entity.UkeyRequest;
import cn.alphahub.eport.signature.entity.UkeyResponse;
import cn.alphahub.eport.signature.entity.UkeyResponse.Args;
import cn.alphahub.eport.signature.entity.UkeyResponseArgsWrapper;
import cn.alphahub.eport.signature.entity.WebSocketWrapper;
import cn.alphahub.eport.signature.support.XMLValidator;
import cn.hutool.core.lang.TypeReference;
import cn.hutool.json.JSONUtil;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.client.WebSocketConnectionManager;
import org.springframework.web.socket.client.standard.StandardWebSocketClient;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.LockSupport;
/**
* <p>电子口岸加签业务核心类</p>
* <a href='http://tool.qdhuaxun.cn/?signdoc'>华讯云业务帮助文档,签名的不要看误导人,华讯的业务文档可以参考</a>
*
* @author weasley
* @version 1.0
* @date 2022/2/12
*/
@Data
@Slf4j
@Service
@Validated
@Accessors(chain = true)
public class SignHandler {
@Autowired
private UkeyProperties ukeyProperties;
@Autowired
private WebSocketClientHandler webSocketClientHandler;
@Autowired
private StandardWebSocketClient standardWebSocketClient;
/**
* 获取ukey的socket入参的inData字段
*
* @param request 加签数据请求入参
* @return ukey的socket入参的inData字段
*/
public static String getInitData(@Valid SignRequest request) {
if (isSignXml(request)) {
return SignatureHandler.getSignatureNodeBeforeSend(request);
}
return request.getData();
}
/**
* 判断是否总署xml
*
* @param request 加签数据请求入参
* @return 总署xml返回true
*/
public static boolean isSignXml(@Valid SignRequest request) {
if (StringUtils.isBlank(request.getData())) {
throw new IllegalArgumentException("加签数据请求入参能为空!");
}
boolean isSignXmlString = XMLValidator.isValidXML(request.getData()) || StringUtils.startsWith(request.getData(), "<ceb:CEB");
boolean isSign179String = StringUtils.startsWith(request.getData(), "\"sessionID\"");
if (isSignXmlString && !isSign179String) {
return true;
}
if (isSign179String && !isSignXmlString) {
return false;
}
throw new IllegalArgumentException("加签数据请求入参不合法,请检查参数:" + request.getData());
}
/**
* 获取u-key签名参数
*
* @return 发送u-key的签名的入参
* @since 2022-11-27
*/
public String getSignDataParameter(@Valid SignRequest request) {
return UkeyInitialConfig.getSignDataAsPEM(request);
}
/**
* WebSocket发送消息给电子口岸u-key
*
* @param request 加签请求参数
* @param payload websocket发送的数据载荷
*/
public SignResult sign(@Valid SignRequest request, @NotBlank(message = "websocket发送的数据载荷不能为空") String payload) {
log.info("收到u-key加签数据: {}", payload);
WebSocketWrapper wrapper = webSocketClientHandler.getWebSocketWrapper();
wrapper.setPayload(payload);
wrapper.setRequest(request);
wrapper.setSignResult(new SignResult());
wrapper.setThreadReference(new AtomicReference<>(Thread.currentThread()));
if (SignHandler.isSignXml(request)) {
wrapper.getSignResult().setDigestValue(SignatureHandler.getDigestValueOfCEBXxxMessage(request.getData()));
wrapper.getSignResult().setSignatureNode(SignatureHandler.getSignatureNodeBeforeSend(request));
}
WebSocketConnectionManager manager = new WebSocketConnectionManager(standardWebSocketClient, webSocketClientHandler, ukeyProperties.getWsUrl());
manager.start();
try {
LockSupport.parkNanos(wrapper.getThreadReference().get(), 1000 * 1000 * 1000 * 5L);
} catch (Exception e) {
log.error("线程自动unpark异常 {}", e.getLocalizedMessage(), e);
wrapper.getSignResult().setSuccess(false);
} finally {
manager.stop();
}
return wrapper.getSignResult();
}
/**
* 获取u-key加签返回内层对象(我们要的数据在这里面)
*
* @apiNote 发送任意数据给ukey,实时响应结果
*/
public Args getUkeyResponseArgs(UkeyRequest ukeyRequest) {
AtomicReference<UkeyResponseArgsWrapper> reference = new AtomicReference<>();
reference.set(new UkeyResponseArgsWrapper(Thread.currentThread(), new Args()));
WebSocketConnectionManager webSocketConnectionManager = new WebSocketConnectionManager(standardWebSocketClient, new TextWebSocketHandler() {
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
session.sendMessage(new TextMessage(JSONUtil.toJsonStr(ukeyRequest)));
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) {
UkeyResponse response = JSONUtil.toBean(message.getPayload(), new TypeReference<>() {
}, true);
try {
if (Objects.equals(response.get_id(), ukeyRequest.get_id())) {
log.warn("从电子口岸ukey中获取获取到数据: {}", response.get_args());
reference.get().setResponseArgs(response.get_args());
}
} catch (Exception e) {
log.error("唤醒线程异常 {}", e.getLocalizedMessage(), e);
}
}
}, ukeyProperties.getWsUrl());
webSocketConnectionManager.start();
try {
LockSupport.parkNanos(reference.get().getThread(), 1000 * 1000 * 1000 * 1L);
} catch (Exception e) {
log.error("线程自动unpark异常 {}", e.getLocalizedMessage(), e);
} finally {
webSocketConnectionManager.stop();
}
return reference.get().getResponseArgs();
}
}
@@ -0,0 +1,28 @@
package cn.alphahub.eport.signature.entity;
import lombok.Data;
import java.io.Serializable;
/**
* 海关 179 数据抓取
* <p>
* 成功数据格式:
* <pre>
* {
* "code": "10000",
* "message": "",
* "serviceTime": 1567050097628
* }
* </pre>
*
* @author weasley
* @version 1.1.0
*/
@Data
public class Capture179DataResponse implements Serializable {
private String code;
private long total;
private String message;
private long serviceTime;
}
@@ -0,0 +1,43 @@
package cn.alphahub.eport.signature.util;
import jakarta.servlet.http.HttpServletRequest;
/**
* 获取客户端的真实 IP 地址
*
* @author weasley
* @since 1.1.0
*/
public final class ClientIPUtils {
static final String UNKNOWN = "unknown";
private ClientIPUtils() {
}
public static String getClientIP(HttpServletRequest request) {
String ipAddress = request.getHeader("X-Forwarded-For");
if (null == ipAddress || ipAddress.isEmpty() || UNKNOWN.equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("Proxy-Client-IP");
}
if (null == ipAddress || ipAddress.isEmpty() || UNKNOWN.equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("WL-Proxy-Client-IP");
}
if (null == ipAddress || ipAddress.isEmpty() || UNKNOWN.equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("HTTP_CLIENT_IP");
}
if (null == ipAddress || ipAddress.isEmpty() || UNKNOWN.equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (null == ipAddress || ipAddress.isEmpty() || UNKNOWN.equalsIgnoreCase(ipAddress)) {
ipAddress = request.getRemoteAddr();
}
// 如果客户端经过了多级反向代理,那么 X-Forwarded-For 中的值是以逗号分隔的 IP 地址列表,取第一个非 unknown 的 IP 地址
if (ipAddress != null && ipAddress.contains(",")) {
ipAddress = ipAddress.split(",")[0].trim();
}
return ipAddress;
}
}
@@ -0,0 +1,86 @@
logging:
level:
cn.alphahub.eport.signature: debug
org.apache.http: error
org.springframework.core: warn
org.springframework.beans: warn
org.springframework.context: warn
org.springframework.web: warn
org.springframework.web.servlet: info
org.springframework.cloud.openfeign: debug
org.springframework.web.servlet.DispatcherServlet: debug
org.springframework.context.support.PropertySourcesPlaceholderConfigurer: info
org.springframework.security.core.userdetails: debug
org.springframework.jdbc.datasource.DataSourceTransactionManager: debug
jdbc.sqltiming: debug
com.alibaba.cloud.nacos.client: error
com.alibaba.nacos: error
org.springframework.web.reactive.function.client.ExchangeFunctions: trace
reactor.netty.http.client.HttpClient: DEBUG
file:
path: ./logs/
name: ${logging.file.path}/${spring.application.name}.log
charset:
file: UTF-8
logback:
rollingpolicy:
max-file-size: 5MB
clean-history-on-start: true
spring:
jackson:
default-property-inclusion: non_null
#邮件通知配置,默认:不启用
mail:
enable: false
#主收件人邮箱
to: abc@qq.com
#抄送箱邮(非必填),抄送多个收件人箱邮用","隔开
cc: abc@outlook.com,cbd@qq.com
#以下发送邮件的客服端参数,使用哪个邮箱发出通知消息
host: smtp.189.cn #邮件服务商主机
port: 465 #端口号
username: xxx@189.cn
password: your_password
protocol: smtp" #协议
properties:
mail:
smtp:
ssl:
enable: true
debug: false #是否开始debug模式,终端会打印日志
mvc:
log-request-details: on
#中国电子口岸u-key的配置参数
eport:
signature:
algorithm: sm2_sm3
ukey:
ws-url: ws://127.0.0.1:61232
password: 88888888
health:
endpoint:
# Windows上重启的ukey可执行文件全限定文件名称
client-name: C:\中国电子口岸客户端控件\SetAccessControl.exe
# 客户端请求鉴权token, 生产环境不建议使用默认token
auth:
enable: on
token: DefaultAuthToken
# 报文传输企业的元数据配置(数据上报)
report:
# CEBMessage XML加签配置
ceb-message:
cop-code: 传输企业代码
cop-name: 海南省荣誉进出口贸易有限公司
dxp-id: 向中国电子口岸数据中心申请数据交换平台的用户编号
# 数据上报服务器地址,格式: http://ip:port, 缺省将采用默认server
server:
# 海关 179 数据抓取配置
customs179:
ebp-code: 电商平台代码
# server可为空,配置文件中指定时,将被优先使用
server: https://海关服务器域名不方便公开/ceb2grab/grab/realTimeDataUpload
@@ -0,0 +1,10 @@
${AnsiColor.BRIGHT_YELLOW}
________ _ ______ __ _____ _ __
/ ____/ /_ (_)___ ____ _ / ____/___ ____ _____/ /_ / ___/(_)___ _____ ____ _/ /___ __________
/ / / __ \/ / __ \/ __ `/ / __/ / __ \/ __ \/ ___/ __/ \__ \/ / __ `/ __ \/ __ `/ __/ / / / ___/ _ \
/ /___/ / / / / / / / /_/ / / /___/ /_/ / /_/ / / / /_ ___/ / / /_/ / / / / /_/ / /_/ /_/ / / / __/
\____/_/ /_/_/_/ /_/\__,_/ /_____/ .___/\____/_/ \__/ /____/_/\__, /_/ /_/\__,_/\__/\__,_/_/ \___/
/_/ /____/
${AnsiColor.BRIGHT_GREEN}${application.title} ${application.formatted-version}
Powered by Spring Boot ${spring-boot.formatted-version}${AnsiStyle.NORMAL}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,259 @@
let api = [];
const apiDocListSize = 1
api.push({
name: 'default',
order: '1',
list: []
})
api[0].list.push({
alias: 'EportTestController',
order: '1',
link: '电子口岸报文加签(测试)',
desc: '电子口岸报文加签(测试)',
list: []
})
api[0].list[0].list.push({
order: '1',
deprecated: 'false',
url: 'http://localhost:8080/rpc/eport/test/cebmessage/signature',
desc: '海关总署XML数据加签',
});
api[0].list[0].list.push({
order: '2',
deprecated: 'false',
url: 'http://localhost:8080/rpc/eport/test/179/signature',
desc: '海关179数据抓取加签',
});
api[0].list[0].list.push({
order: '3',
deprecated: 'false',
url: 'http://localhost:8080/rpc/eport/test/endpoint/exec',
desc: '执行脚本命令',
});
api[0].list.push({
alias: 'EportReportResultController',
order: '2',
link: '查询申报回执结果',
desc: '查询申报回执结果',
list: []
})
api[0].list[1].list.push({
order: '1',
deprecated: 'false',
url: 'http://localhost:8080/rpc/eport/result/ceb312msg',
desc: '查询311进口单申报结果',
});
api[0].list[1].list.push({
order: '2',
deprecated: 'false',
url: 'http://localhost:8080/rpc/eport/result/ceb622msg',
desc: '查询621进口单申报结果',
});
api[0].list.push({
alias: 'UkeyHealthController',
order: '3',
link: 'ukey健康状态恢复',
desc: 'Ukey健康状态恢复',
list: []
})
api[0].list[2].list.push({
order: '1',
deprecated: 'false',
url: 'http://localhost:8080/rpc/eport/ukey/health/endpoint/{command}',
desc: 'ukey健康指令',
});
api[0].list.push({
alias: 'EportUploadController',
order: '4',
link: '电子口岸报文推送',
desc: '电子口岸报文推送',
list: []
})
api[0].list[3].list.push({
order: '1',
deprecated: 'false',
url: 'http://localhost:8080/rpc/eport/upload/CEBMessage',
desc: '推送CEBMessage报文',
});
api[0].list[3].list.push({
order: '2',
deprecated: 'false',
url: 'http://localhost:8080/rpc/eport/upload/179/data',
desc: '推送海关179号数据',
});
api[0].list.push({
alias: 'EportCertController',
order: '5',
link: '电子口岸x509证书',
desc: '电子口岸X509证书',
list: []
})
api[0].list[4].list.push({
order: '1',
deprecated: 'false',
url: 'http://localhost:8080/rpc/eport/cert/download',
desc: '下载证书',
});
api[0].list.push({
alias: 'EportSignController',
order: '6',
link: '电子口岸报文加签',
desc: '电子口岸报文加签',
list: []
})
api[0].list[5].list.push({
order: '1',
deprecated: 'false',
url: 'http://localhost:8080/rpc/eport/signature',
desc: '海关数据加签',
});
document.onkeydown = keyDownSearch;
function keyDownSearch(e) {
const theEvent = e;
const code = theEvent.keyCode || theEvent.which || theEvent.charCode;
if (code === 13) {
const search = document.getElementById('search');
const searchValue = search.value.toLocaleLowerCase();
let searchGroup = [];
for (let i = 0; i < api.length; i++) {
let apiGroup = api[i];
let searchArr = [];
for (let i = 0; i < apiGroup.list.length; i++) {
let apiData = apiGroup.list[i];
const desc = apiData.desc;
if (desc.toLocaleLowerCase().indexOf(searchValue) > -1) {
searchArr.push({
order: apiData.order,
desc: apiData.desc,
link: apiData.link,
list: apiData.list
});
} else {
let methodList = apiData.list || [];
let methodListTemp = [];
for (let j = 0; j < methodList.length; j++) {
const methodData = methodList[j];
const methodDesc = methodData.desc;
if (methodDesc.toLocaleLowerCase().indexOf(searchValue) > -1) {
methodListTemp.push(methodData);
break;
}
}
if (methodListTemp.length > 0) {
const data = {
order: apiData.order,
desc: apiData.desc,
link: apiData.link,
list: methodListTemp
};
searchArr.push(data);
}
}
}
if (apiGroup.name.toLocaleLowerCase().indexOf(searchValue) > -1) {
searchGroup.push({
name: apiGroup.name,
order: apiGroup.order,
list: searchArr
});
continue;
}
if (searchArr.length === 0) {
continue;
}
searchGroup.push({
name: apiGroup.name,
order: apiGroup.order,
list: searchArr
});
}
let html;
if (searchValue === '') {
const liClass = "";
const display = "display: none";
html = buildAccordion(api,liClass,display);
document.getElementById('accordion').innerHTML = html;
} else {
const liClass = "open";
const display = "display: block";
html = buildAccordion(searchGroup,liClass,display);
document.getElementById('accordion').innerHTML = html;
}
const Accordion = function (el, multiple) {
this.el = el || {};
this.multiple = multiple || false;
const links = this.el.find('.dd');
links.on('click', {el: this.el, multiple: this.multiple}, this.dropdown);
};
Accordion.prototype.dropdown = function (e) {
const $el = e.data.el;
let $this = $(this), $next = $this.next();
$next.slideToggle();
$this.parent().toggleClass('open');
if (!e.data.multiple) {
$el.find('.submenu').not($next).slideUp("20").parent().removeClass('open');
}
};
new Accordion($('#accordion'), false);
}
}
function buildAccordion(apiGroups, liClass, display) {
let html = "";
if (apiGroups.length > 0) {
if (apiDocListSize === 1) {
let apiData = apiGroups[0].list;
let order = apiGroups[0].order;
for (let j = 0; j < apiData.length; j++) {
html += '<li class="'+liClass+'">';
html += '<a class="dd" href="#_'+order+'_'+apiData[j].order+'_' + apiData[j].link + '">' + apiData[j].order + '.&nbsp;' + apiData[j].desc + '</a>';
html += '<ul class="sectlevel2" style="'+display+'">';
let doc = apiData[j].list;
for (let m = 0; m < doc.length; m++) {
let spanString;
if (doc[m].deprecated === 'true') {
spanString='<span class="line-through">';
} else {
spanString='<span>';
}
html += '<li><a href="#_'+order+'_' + apiData[j].order + '_' + doc[m].order + '_' + doc[m].desc + '">' + apiData[j].order + '.' + doc[m].order + '.&nbsp;' + spanString + doc[m].desc + '<span></a> </li>';
}
html += '</ul>';
html += '</li>';
}
} else {
for (let i = 0; i < apiGroups.length; i++) {
let apiGroup = apiGroups[i];
html += '<li class="'+liClass+'">';
html += '<a class="dd" href="#_'+apiGroup.order+'_' + apiGroup.name + '">' + apiGroup.order + '.&nbsp;' + apiGroup.name + '</a>';
html += '<ul class="sectlevel1">';
let apiData = apiGroup.list;
for (let j = 0; j < apiData.length; j++) {
html += '<li class="'+liClass+'">';
html += '<a class="dd" href="#_'+apiGroup.order+'_'+ apiData[j].order + '_'+ apiData[j].link + '">' +apiGroup.order+'.'+ apiData[j].order + '.&nbsp;' + apiData[j].desc + '</a>';
html += '<ul class="sectlevel2" style="'+display+'">';
let doc = apiData[j].list;
for (let m = 0; m < doc.length; m++) {
let spanString;
if (doc[m].deprecated === 'true') {
spanString='<span class="line-through">';
} else {
spanString='<span>';
}
html += '<li><a href="#_'+apiGroup.order+'_' + apiData[j].order + '_' + doc[m].order + '_' + doc[m].desc + '">'+apiGroup.order+'.' + apiData[j].order + '.' + doc[m].order + '.&nbsp;' + spanString + doc[m].desc + '<span></a> </li>';
}
html += '</ul>';
html += '</li>';
}
html += '</ul>';
html += '</li>';
}
}
}
return html;
}
@@ -0,0 +1 @@
.hljs{display:block;overflow-x:auto;color:#eaeaea;background:#000;padding:.5em}.hljs-subst{color:#eaeaea}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}.hljs-builtin-name,.hljs-type{color:#eaeaea}.hljs-params{color:#da0000}.hljs-literal,.hljs-name,.hljs-number{color:red;font-weight:bolder}.hljs-comment{color:#969896}.hljs-quote,.hljs-selector-id{color:#0ff}.hljs-template-variable,.hljs-title,.hljs-variable{color:#0ff;font-weight:700}.hljs-keyword,.hljs-selector-class,.hljs-symbol{color:#fff000}.hljs-bullet,.hljs-string{color:#0f0}.hljs-section,.hljs-tag{color:#000fff}.hljs-selector-tag{color:#000fff;font-weight:700}.hljs-attribute,.hljs-built_in,.hljs-link,.hljs-regexp{color:#f0f}.hljs-meta{color:#fff;font-weight:bolder}
@@ -0,0 +1,115 @@
<ceb:CEB621Message xmlns:ceb="http://www.chinaport.gov.cn/ceb" guid="CEB621_HNZB_FXJK_20220208175054_0034"
version="v1.0">
<ceb:Inventory>
<ceb:InventoryHead>
<ceb:guid>CEB621_HNZB_FXJK_20220208175055_0035</ceb:guid>
<ceb:appType>1</ceb:appType>
<ceb:appTime>20220208175055</ceb:appTime>
<ceb:appStatus>2</ceb:appStatus>
<ceb:orderNo>48037605991776256</ceb:orderNo>
<ceb:ebpCode>46121601BC</ceb:ebpCode>
<ceb:ebpName>海南星创互联网医药有限公司</ceb:ebpName>
<ceb:ebcCode>46121601BC</ceb:ebcCode>
<ceb:ebcName>海南星创互联网医药有限公司</ceb:ebcName>
<ceb:logisticsNo>2222222</ceb:logisticsNo>
<ceb:logisticsCode>4101986180</ceb:logisticsCode>
<ceb:logisticsName>圆通</ceb:logisticsName>
<ceb:copNo>4101W68006</ceb:copNo>
<ceb:assureCode>4101960AGP</ceb:assureCode>
<ceb:ieFlag>I</ceb:ieFlag>
<ceb:declTime>20220208</ceb:declTime>
<ceb:customsCode>4601</ceb:customsCode>
<ceb:portCode>4601</ceb:portCode>
<ceb:ieDate>20220208</ceb:ieDate>
<ceb:buyerIdType>1</ceb:buyerIdType>
<ceb:buyerIdNumber>612326199005306719</ceb:buyerIdNumber>
<ceb:buyerName>用户_15150368</ceb:buyerName>
<ceb:buyerTelephone>18209182500</ceb:buyerTelephone>
<ceb:consigneeAddress>哈布斯堡</ceb:consigneeAddress>
<ceb:agentCode>46121601BC</ceb:agentCode>
<ceb:agentName>海南星创互联网医药有限公司</ceb:agentName>
<ceb:tradeMode>9610</ceb:tradeMode>
<ceb:trafMode>W</ceb:trafMode>
<ceb:country>142</ceb:country>
<ceb:freight>0.00</ceb:freight>
<ceb:insuredFee>0</ceb:insuredFee>
<ceb:currency>142</ceb:currency>
<ceb:packNo>1</ceb:packNo>
<ceb:grossWeight>0.07</ceb:grossWeight>
<ceb:netWeight>0.06</ceb:netWeight>
</ceb:InventoryHead>
<ceb:InventoryList>
<ceb:gnum>1</ceb:gnum>
<ceb:itemRecordNo>0797776159482</ceb:itemRecordNo>
<ceb:itemNo>HNGGKJ00000057</ceb:itemNo>
<ceb:itemName>通用铁剂胶囊</ceb:itemName>
<ceb:gcode>2106909090</ceb:gcode>
<ceb:gname>ACTIVE IRON</ceb:gname>
<ceb:gmodel>4|3|乳清蛋白(牛奶)65%,胶囊壳(羟苯基甲基纤维素)21%,硫酸亚铁1.2%,酸度调节剂2%,填充剂 10.8%|30粒/盒|无中文名称/ACTIVE IRON</ceb:gmodel>
<ceb:country>306</ceb:country>
<ceb:currency>142</ceb:currency>
<ceb:qty>1</ceb:qty>
<ceb:unit>140</ceb:unit>
<ceb:qty1>0.06</ceb:qty1>
<ceb:unit1>035</ceb:unit1>
<ceb:price>0.01</ceb:price>
<ceb:totalPrice>0.01</ceb:totalPrice>
</ceb:InventoryList>
</ceb:Inventory>
<ceb:BaseTransfer>
<ceb:copCode>46121601BC</ceb:copCode>
<ceb:copName>海南星创互联网医药有限公司</ceb:copName>
<ceb:dxpMode>DXP</ceb:dxpMode>
<ceb:dxpId>DXPENT0000458763</ceb:dxpId>
</ceb:BaseTransfer>
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:SignedInfo>
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
<ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
<ds:Reference URI="">
<ds:Transforms>
<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
</ds:Transforms>
<ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
<ds:DigestValue>b2FtM/jHvW4fnJpMMPWk/177RwA=</ds:DigestValue>
</ds:Reference>
</ds:SignedInfo>
<ds:SignatureValue>
mhWpJI3RsJZGiR8MqI3kcJAJPqgVOQi0IFJzp0CBWLG+Qa7ZnYJX+hi70J+/09diHYkCMDxcKuT5uogbFvi/9rOZnrn+YsL0h0/nGcO31lx/RpqT8i5GoSt/UcZe9COIbnrKwdh+90FLYIW4xS3Rsglv4CDL+e53GGmSdduSTUU=
</ds:SignatureValue>
<ds:KeyInfo>
<ds:KeyName>01691fe9</ds:KeyName>
<ds:X509Data>
<ds:X509Certificate>
bjEVMBMGA1UECh4MTi1W/XU1W1BT41y4MRUwEwYDVQQLHgyLwU5me6F0Bk4tX8Mx
wzENMAsGA1UEBx4EUxdOrDAeFw0yMTA1MzEwMDAwMDBaFw0zMTA1MzEwMDAwMDBa
MIIE+TCCBGKgAwIBAgIEAWkf6TANBgkqhkiG9w0BAQUFADB8MQswCQYDVQQGEwJj
DTALBgNVBAgeBFMXTqwxITAfBgNVBAMeGE4tVv11NVtQThpSoYvBTmZ7oXQGTi1f
A4GNADCBiQKBgQDt9T7JOQikKKL7oY7RWbRMp7y2VsrXIbFzSDh5DZQPimduUijZ
xgG4Kdr0TwxAQ+s3H85fCU81ROgqAS05/IzOR7eDSEYYT9CRmNYXIRTPVwIDAQAB
MBwxDTALBgNVBAMeBH8qdDMxCzAJBgNVBBIeAgAxMIGfMA0GCSqGSIb3DQEBAQUA
o4IC5jCCAuIwCwYDVR0PBAQDAgbAMAkGA1UdEwQCMAAwgacGA1UdIwSBnzCBnIAU
zZlK6AkZMDYJBM2/IJGI0QjWjBJVZ8hEUkgJb4UOOBCBM+cCGFCNY5LX/mAo5Boe
Ch4MTi1W/XU1W1BT41y4MRUwEwYDVQQLHgyLwU5me6F0Bk4tX8MxDTALBgNVBAge
Bx4EUxdOrIIBADAdBgNVHQ4EFgQU1eAz50lwZ6gidtkJNPIr7O0jKvUwQgYDVR0g
BDswOTA3BgYrgQcBAQIwLTArBggrBgEFBQcCARYfaHR0cDovL2Nwcy5jaGluYXBv
+XWjeEULQmjCBFOr68NOPlR4dGChgYCkfjB8MQswCQYDVQQGEwJjbjEVMBMGA1UE
BFMXTqwxITAfBgNVBAMeGE4tVv11NVtQThpSoYvBTmZ7oXQGTi1fwzENMAsGA1UE
cnQuZ292LmNuL0NQUzByBgNVHR8EazBpMDCgLqAshipsZGFwOi8vbGRhcC5jaGlu
YXBvcnQuZ292LmNuOjM4OS8wMDAtMS5jcmwwNaAzoDGGL2h0dHA6Ly9sZGFwLmNo
aW5hcG9ydC5nb3YuY246ODA4OC9kemthMDAwLTEuY3JsMG0GCCsGAQUFBwEBBGEw
LzAtBggrBgEFBQcwAYYhaHR0cDovL29jc3AuY2hpbmFwb3J0Lmdvdi5jbjo4MDg4
XzAuBggrBgEFBQcwAYYiaHR0cDovL29jc3AuY2hpbmFwb3J0Lmdvdi5jbjo4ODAw
KwYBBAGpQ2QFAwQEFgIwMTAiBgorBgEEAalDZAUIBBQWEjMxMDEwMjE5ODAwNzI0
MCoGCisGAQQBqUNkBQEEHBYauqPEz9DHtLS7pcGqzfjSvdKp09DP3rmry74wEgYK
CwQLFglNQTVUTFVBNTMwEgYKKwYBBAGpQ2QFDAQEFgIwMDASBgorBgEEAalDZAIE
MzIxNDAdBgorBgEEAalDZAUJBA8WDUpKMEc5MDAxNTkyNzIwGQYKKwYBBAGpQ2QF
MxAzM4bXvdlDcE6fODsCvQMFKctlA+LCllFQwl58HaBmcWx4T/ddKF9LBYc8A986
BAQWAjE0MBIGCisGAQQBqUNkAgEEBBYCMTIwDQYJKoZIhvcNAQEFBQADgYEAqSuO
LlcUw6Mkwxraj2WO+meXdDzRLEO8t3gyZk7tYp5aneV4zGYUsphwyMdLt8N8o4kV
gg16bQy43XgA1jRMv8nvhb908IqQQBxwv0SIuXU=
</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</ds:Signature>
</ceb:CEB621Message>