xiaochan 6 місяців тому
батько
коміт
7629b9b573

+ 27 - 0
thyy-thirdpart-api/src/main/java/org/thyy/thirdpartapi/YesOrNo.java

@@ -0,0 +1,27 @@
+package org.thyy.thirdpartapi;
+
+/**
+ * @description: 1是0否
+ * @author: DingJie
+ * @create: 2021/7/915:55
+ */
+public enum YesOrNo {
+    YES(1),
+
+    NO(0);
+
+    private final int code;
+
+    YesOrNo(int code) {
+        this.code = code;
+    }
+
+    public Integer getCode() {
+        return code;
+    }
+
+    public String getCodeStr() {
+        return String.valueOf(code);
+    }
+
+}

+ 50 - 0
thyy-thirdpart-api/src/main/java/org/thyy/thirdpartapi/casign/CaUtils.java

@@ -0,0 +1,50 @@
+package org.thyy.thirdpartapi.casign;
+
+import cn.hutool.core.codec.Base64;
+import cn.hutool.crypto.SecureUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.nio.charset.StandardCharsets;
+import java.security.KeyFactory;
+import java.security.PrivateKey;
+import java.security.Signature;
+import java.security.spec.PKCS8EncodedKeySpec;
+
+@Slf4j
+public class CaUtils {
+    private static final String SIGN_ALGORITHMS = "SHA256WithRSA";
+
+    public static String GetSHA256FormString(String inData) {
+        byte[] digest = SecureUtil.sha256().digest(inData.getBytes(StandardCharsets.UTF_8));
+        return Base64.encode(digest);
+    }
+
+    public static String getSign(String privateKey, String data) {
+        byte[] signData = signature(privateKey, data);
+        String sign = null;
+        if (signData != null) {
+            sign = Base64.encode(signData);
+        }
+        return sign;
+    }
+
+    public static byte[] signature(String privateKey, String data) {
+        try {
+            PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(
+                    Base64.decode(privateKey));
+            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+            PrivateKey priKey = keyFactory.generatePrivate(keySpec);
+
+            Signature oSig = Signature.getInstance(SIGN_ALGORITHMS);
+            oSig.initSign(priKey);
+            oSig.update(data.getBytes(StandardCharsets.UTF_8));
+            return oSig.sign();
+        } catch (Exception e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+
+
+}

+ 7 - 0
thyy-thirdpart-api/src/main/java/org/thyy/thirdpartapi/casign/PatientCaInterface.java

@@ -0,0 +1,7 @@
+package org.thyy.thirdpartapi.casign;
+
+public interface PatientCaInterface {
+
+
+
+}

+ 31 - 0
thyy-thirdpart-api/src/main/java/org/thyy/thirdpartapi/casign/UserCaInterface.java

@@ -0,0 +1,31 @@
+package org.thyy.thirdpartapi.casign;
+
+import org.thyy.thirdpartapi.casign.dto.CaSignClass;
+import org.thyy.thirdpartapi.emr.vo.CaSendByCodeVo;
+import org.thyy.utils.result.ResultVo;
+
+import java.util.List;
+
+/**
+ * 用户ca签名
+ */
+public interface UserCaInterface {
+
+
+    /**
+     * 根据用户的code 发送签名
+     *
+     * @param send 数据
+     * @return ca的返回值
+     */
+    ResultVo<CaSendByCodeVo> sendByCode(CaSignClass.Send send);
+
+
+    /**
+     * 一次性签名多个
+     *
+     * @param send 前面数据
+     * @return ca的返回值
+     */
+    ResultVo<List<CaSendByCodeVo>> sendBatchByCode(CaSignClass.Send send);
+}

+ 30 - 0
thyy-thirdpart-api/src/main/java/org/thyy/thirdpartapi/casign/controller/CaController.java

@@ -0,0 +1,30 @@
+package org.thyy.thirdpartapi.casign.controller;
+
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.thyy.thirdpartapi.casign.UserCaInterface;
+import org.thyy.thirdpartapi.casign.dto.CaSignClass;
+import org.thyy.utils.result.ResultVo;
+
+@RestController
+@RequestMapping("/ca/sign")
+public class CaController {
+    private final UserCaInterface service;
+
+    public CaController(UserCaInterface caInterface) {
+        this.service = caInterface;
+    }
+
+    @PostMapping("/sendByCode")
+    public ResultVo<?> sendByCode(@RequestBody CaSignClass.Send send) {
+        return service.sendByCode(send);
+    }
+
+    @PostMapping("/sendBatchByCode")
+    public ResultVo<?> sendBatchByCode(@RequestBody CaSignClass.Send send) {
+        return service.sendBatchByCode(send);
+    }
+
+}

+ 64 - 0
thyy-thirdpart-api/src/main/java/org/thyy/thirdpartapi/casign/dto/CaSendParams.java

@@ -0,0 +1,64 @@
+package org.thyy.thirdpartapi.casign.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class CaSendParams {
+    /**
+     * 移动安全认证系统分配的应用Id
+     */
+    private String appId;
+    /**
+     * 用户标识,用于关联应用系统的用户身份,对应用户注册信息中的uuid
+     */
+    private String id;
+    /**
+     * 业务流水号,需要保持唯一性,用于标识每笔业务的关联,签名操作后会将对应的数据回调给应用
+     */
+    private String bizSn;
+    /**
+     * 待签名数据,传输前对摘要计算之后的msg做URLEncode编码(UTF-8)。
+     * 对有举证意义的数据(原文)做摘要处理,计算公式:
+     * msg=new String(Base64.encode(SHA256(原文)))
+     * 参考示例代码8.1SHA-256摘要转换
+     */
+    private String msg;
+    /**
+     * 是否对待签名数据进行Base64解码,默认为0 0:不解码
+     * 1:解码
+     */
+    private String msgWrapper;
+
+    /**
+     * 签名动作描述(URLEncode,utf-8),最大长度512。内容会在APP端展现
+     */
+    private String desc;
+
+    /**
+     * 应用接收签名后数据的回调地址
+     * (URLEncode,utf-8),最大长度512。
+     * 由应用系统提供接口地址,接口定义参见6.2.1
+     */
+    private String url;
+
+    /**
+     * 数据传递方式,推荐使用redirect。redirect:由服务器传递给应用
+     * forward:由APP传递给应用
+     */
+    private String mode;
+
+    /**
+     * 用应用私钥对msg值(URLEncode编码之前,摘要处理之后的数据)进行参数签名,
+     * 得到Base64 编码格式签名值后进行URLEncode(UTF-8)传
+     * 输。当后台开启鉴权时,此参数必填。
+     * 应用私钥由Mkey后台分配,示例代码参考8.2参
+     */
+    private String sign;
+
+}

+ 101 - 0
thyy-thirdpart-api/src/main/java/org/thyy/thirdpartapi/casign/dto/CaSignClass.java

@@ -0,0 +1,101 @@
+package org.thyy.thirdpartapi.casign.dto;
+
+import cn.hutool.json.JSONObject;
+import lombok.*;
+
+import java.util.List;
+
+public class CaSignClass {
+
+    /**
+     * 发送医生签名的类
+     */
+    @Data
+    public static class Send {
+        // 用户的id
+        private String id;
+        // 签名消息
+        private String msg;
+        // 签名的描述
+        private String desc;
+        // 传 1
+        private Integer count = 1;
+    }
+
+    @Data
+    public static class MoreEventSignData {
+        private String content;
+
+        /**
+         * 签署人身份类型。例如患者本人、亲属、朋友
+         * 、伴侣。进行 URLEncode 编码的数据,并且中
+         * 文长度不得大于 8
+         */
+        private String signType;
+        private String signTypeName;
+        private String signName;
+        private String idCard;
+        private String name;
+        private String signOpinion;
+    }
+
+    /**
+     * 移动平板实体类
+     */
+    @Data
+    public static class MoreEventSign {
+        private String documentId;
+        private String sendCode;
+        private String sendCodeName;
+        private List<MoreEventSignData> data;
+    }
+
+    /**
+     * 页面扫码的类
+     */
+    @Data
+    public static class H5EventSign {
+        private String uuid;
+        private String documentId;
+        private String content;
+        private String idCard;
+        private Integer signType;
+        private Boolean clear = true;
+        private String name;
+        private String signOpinion;
+    }
+
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class H5Return {
+        private String caDocumentId;
+        private String result;
+        private String uuid;
+    }
+
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class H5EventSignRedis {
+        private String uuid;
+        private String pdf;
+
+        private List<JSONObject> data;
+    }
+
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class Upload {
+        private String file;
+        private String fileName;
+        private String patNo;
+        private Integer times;
+        private String caData;
+    }
+
+}

+ 183 - 0
thyy-thirdpart-api/src/main/java/org/thyy/thirdpartapi/casign/service/ShangHaiCa.java

@@ -0,0 +1,183 @@
+package org.thyy.thirdpartapi.casign.service;
+
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.util.IdUtil;
+import cn.hutool.core.util.URLUtil;
+import cn.hutool.http.HttpRequest;
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+import jakarta.validation.constraints.NotNull;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.thyy.thirdpartapi.YesOrNo;
+import org.thyy.thirdpartapi.casign.CaConfig;
+import org.thyy.thirdpartapi.casign.UserCaInterface;
+import org.thyy.thirdpartapi.casign.CaUtils;
+import org.thyy.thirdpartapi.casign.dto.CaSendParams;
+import org.thyy.thirdpartapi.casign.dto.CaSignClass;
+import org.thyy.thirdpartapi.casign.vo.CaReturn;
+import org.thyy.thirdpartapi.emr.service.CaWSDL;
+import org.thyy.thirdpartapi.emr.vo.CaSendByCodeVo;
+import org.thyy.utils.exception.BizException;
+import org.thyy.utils.exception.ExceptionEnum;
+import org.thyy.utils.result.R;
+import org.thyy.utils.result.ResultVo;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+@Service
+@Slf4j
+public class ShangHaiCa implements UserCaInterface {
+    private final CaConfig config;
+    private final String SUCCRSS = "success";
+
+    public ShangHaiCa(CaConfig config) {
+        this.config = config;
+    }
+
+    @Override
+    public ResultVo<List<CaSendByCodeVo>> sendBatchByCode(CaSignClass.Send send) {
+        List<CaSendByCodeVo> returnValue = new ArrayList<>();
+        send.setCount(send.getCount() == null ? 1 : send.getCount());
+        for (int i = 0; i < send.getCount(); i++) {
+            ResultVo<CaSendByCodeVo> caSendByCodeVoResultVo = sendByCode(send);
+            if (ExceptionEnum.SUCCESS.getCode().equals(caSendByCodeVoResultVo.getCode())) {
+                returnValue.add(caSendByCodeVoResultVo.getData());
+            } else {
+                return R.fail(caSendByCodeVoResultVo.getCode(), caSendByCodeVoResultVo.getMessage());
+            }
+        }
+        return R.ok(returnValue);
+    }
+
+
+    @Override
+    public ResultVo<CaSendByCodeVo> sendByCode(CaSignClass.Send send) {
+        if (config.getMobileApp() == null) {
+            return R.fail(ExceptionEnum.LOGICAL_ERROR, "未配置ca签名请联系管理员");
+        }
+        String msg = CaUtils.GetSHA256FormString(send.getMsg());
+        CaSendParams params = CaSendParams.builder()
+                .id(send.getId())
+                .bizSn(IdUtil.simpleUUID())
+                .msg(msg)
+                .desc(send.getDesc())
+                .appId(config.getMobileApp().getAppId())
+                .msgWrapper("0")
+                .desc(URLUtil.encode(send.getDesc()))
+                .url(URLUtil.encode("http://127.0.0.1"))
+                .mode("redirect")
+                .sign(CaUtils.getSign(config.getMobileApp().getPrivatekey(), msg))
+                .build();
+
+        Map<String, Object> stringObjectMap = BeanUtil.beanToMap(params);
+
+        String execute = HttpRequest.post(config.getMobileApp().getUrl() + "/v1/push/sign")
+                .header("Content-Type", "application/x-www-form-urlencoded")
+                .form(stringObjectMap)
+                .execute().body();
+        String decode = URLUtil.decode(execute);
+        log.info("返回数据:{}", decode);
+        CaReturn caReturn = JSONUtil.toBean(decode, CaReturn.class);
+
+        if (!SUCCRSS.equals(caReturn.getRet())) {
+            return R.fail(ExceptionEnum.LOGICAL_ERROR, "签名失败。" + caReturn.getMsg());
+        }
+        if (Integer.valueOf(1).equals(caReturn.getData().getIsTrust())) {
+            return R.fail(ExceptionEnum.LOGICAL_ERROR, "签名失败,请用户打开免密签名。");
+        }
+
+        CaReturn.CaData data = caReturn.getData();
+
+        signatureAuthentication(msg, caReturn);
+        String timeStamp = generateTimestampByInData(msg);
+
+        CaSendByCodeVo build = CaSendByCodeVo
+                .builder()
+                .timeStamp(timeStamp)
+                .bizSn(params.getBizSn())
+                .cert(data.getCert())
+                .signAlg(data.getSignAlg())
+                .certSn(data.getCertSn())
+                .signValue(data.getSignValue())
+                .build();
+
+        return R.ok(build);
+    }
+
+    private void signatureAuthentication(String msg, CaReturn caReturn) {
+        CaConfig.SignAuthentication signData = config.getSignAuthentication();
+        CaWSDL caWSDL = new CaWSDL(signData.getUrl());
+        String replace = getVerifySignDataParams(msg, caReturn, signData);
+        JSONObject rst = caWSDL.invoke("verifySignData", replace);
+        log.info("认证签名xml:{}\n认证签名返回值:{}\n", replace, rst.toString());
+        Integer code = rst.getByPath("Root.RetCode", Integer.class);
+        if (!YesOrNo.YES.getCode().equals(code)) {
+            String rstmsg = rst.getByPath("Root.RetMsg", String.class);
+            throw new BizException(ExceptionEnum.LOGICAL_ERROR, rstmsg);
+        }
+    }
+
+    private static @NotNull String getVerifySignDataParams(String msg, CaReturn caReturn, CaConfig.SignAuthentication signData) {
+        String request = """
+                <?xml version="1.0" encoding="UTF-8" ?>
+                <Root>
+                \t<AppCode>${AppCode}</AppCode>
+                \t<AppPWD>${AppPWD}</AppPWD>
+                \t<Request>
+                \t\t<Cert>${Cert}</Cert>
+                \t\t<SignAlg>${SignAlg}</SignAlg>
+                \t\t<InData>${InData}</InData>
+                \t\t<SignData>${SignData}</SignData>
+                \t</Request>
+                </Root>""";
+
+        return request.replace("${AppCode}", signData.getAppCode())
+                .replace("${AppPWD}", signData.getAppPwd())
+                .replace("${Cert}", caReturn.getData().getCert())
+                .replace("${SignAlg}", signData.getSignAig())
+                .replace("${InData}", msg)
+                .replace("${SignData}", caReturn.getData().getSignValue());
+    }
+
+
+    /**
+     * 认证时间戳并返回
+     *
+     * @param msg 数据
+     * @return
+     */
+    private String generateTimestampByInData(String msg) {
+        String data = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n" +
+                      "<Root>\n" +
+                      "\t<AppCode>${AppCode}</AppCode>\n" +
+                      "\t<AppPWD>${AppPWD}</AppPWD>\n" +
+                      "\t<Request>\n" +
+                      "\t\t<InData>${InData}</InData>\n" +
+                      "<HashAlg>${HashAlg}</HashAlg>\n" +
+                      "<CertReq>${CertReq}</CertReq>\n" +
+                      "\t</Request>\n" +
+                      "</Root>";
+
+        CaConfig.TimestampAuthentication timeData = config.getTimestampAuthentication();
+        String request = data.replace("${AppCode}", timeData.getAppCode())
+                .replace("${AppPWD}", timeData.getAppPwd())
+                .replace("${InData}", msg)
+                .replace("${HashAlg}", "SHA256")
+                .replace("${CertReq}", "true");
+
+        CaWSDL caWSDL = new CaWSDL(timeData.getUrl());
+        JSONObject ret = caWSDL.invoke("generateTimestampByInData", request);
+        Integer code = ret.getByPath("Root.RetCode", Integer.class);
+        log.info("认证时间戳xml:{}\n认证时间戳返回值:{}\n", request, ret);
+        if (code != 1) {
+            String retmsg = ret.getByPath("Root.RetMsg", String.class);
+            throw new BizException(ExceptionEnum.LOGICAL_ERROR, retmsg);
+        }
+        return ret.getByPath("Root.Response.TimeStamp", String.class);
+    }
+
+
+}

+ 32 - 0
thyy-thirdpart-api/src/main/java/org/thyy/thirdpartapi/casign/vo/CaReturn.java

@@ -0,0 +1,32 @@
+package org.thyy.thirdpartapi.casign.vo;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+public class CaReturn {
+
+    private String ret;
+    private String msg;
+    private CaData data;
+
+    @EqualsAndHashCode(callSuper = true)
+    @Data
+    public static class CaData extends EmrCaData {
+        private Integer isTrust;
+        private String cert;
+        private String signAlg;
+        private String signValue;
+        private String certSn;
+        private String msg;
+        private String code;
+    }
+
+    @Data
+    public static class EmrCaData {
+        private String timeStamp;
+        private String bizSn;
+        private String signature;
+        private String name;
+    }
+}

+ 49 - 0
thyy-thirdpart-api/src/main/java/org/thyy/thirdpartapi/emr/service/CaWSDL.java

@@ -0,0 +1,49 @@
+package org.thyy.thirdpartapi.emr.service;
+
+import cn.hutool.http.HtmlUtil;
+import cn.hutool.http.HttpRequest;
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class CaWSDL {
+
+    private final String regex = "<ns1:out>(.*?)</ns1:out>";
+    // 创建 Pattern 对象
+    private final Pattern pattern = Pattern.compile(regex);
+
+    private final String url;
+
+    public CaWSDL(String url) {
+        this.url = url;
+    }
+
+    public JSONObject invoke(String method, String request) {
+        String WSDL = "<soapenv:Envelope xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:web=\"http://webservice.security.openapi.sp.custle.com\">\n" +
+                      "   <soapenv:Header/>\n" +
+                      "   <soapenv:Body>\n" +
+                      "      <web:${name}>\n" +
+                      "         <web:in0>\n" +
+                      "    <![CDATA[${data}]]>\n" +
+                      "         </web:in0>\n" +
+                      "      </web:${name}>\n" +
+                      "   </soapenv:Body>\n" +
+                      "</soapenv:Envelope>";
+        String body = WSDL.replace("${name}", method).replace("${data}", request);
+        String execute = HttpRequest.post(url).contentType("application/xml").body(body).execute().body();
+        String data = findData(execute);
+        return JSONUtil.xmlToJson(data);
+    }
+
+    private String findData(String input) {
+        Matcher matcher = pattern.matcher(input);
+        if (matcher.find()) {
+            // 获取第一个捕获组
+            return HtmlUtil.unescape(matcher.group(1));
+        } else {
+            return "";
+        }
+    }
+}

+ 19 - 0
thyy-thirdpart-api/src/main/java/org/thyy/thirdpartapi/emr/vo/CaSendByCodeVo.java

@@ -0,0 +1,19 @@
+package org.thyy.thirdpartapi.emr.vo;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@Builder
+public class CaSendByCodeVo {
+    private String timeStamp;
+    private String bizSn;
+    private String cert;
+    private String signAlg;
+    private String signValue;
+    private String certSn;
+}

+ 0 - 3
thyy-thirdpart-api/src/main/resources/application.yml

@@ -75,9 +75,6 @@ thyy:
     url: "http://172.16.32.125:8001"
     authorization: "user:dc71ccfec05b799ad52360c48d504019"
     pdf-url: "http://webhis.thyy.cn:8080/thyyemrpdfserver/emr/archive/pdf"
-  spd:
-    service: scpangu
-    url: ""
   config:
     hosp-code: H43010500370
     hosp-name: 长沙泰和医院