刘光辉
昨天 bb638871a7fb692d80f1b7a758f991dc0879002c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
package jnpf.bizcommon.audit.service;
 
import jnpf.audit.AuditConsts;
import jnpf.audit.model.AuditEventDTO;
import jnpf.bizcommon.audit.service.sign.AuditSignEvidence;
import jnpf.bizcommon.audit.service.sign.AuditSignEvidenceReader;
import jnpf.bizcommon.audit.service.sign.AuditSignMetaData;
import jnpf.util.JsonUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
 
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
 
/** 为数据接口操作事实补充请求所携带的电子签名事实。 */
@Slf4j
@Service
@RequiredArgsConstructor
public class DataInterfaceSignAuditService {
 
    private static final String SIGN_ACTION = "SIGN";
    private static final String REQUEST_SIGN_FIELD = "__request_biz_sign__";
 
    private final AuditSignEvidenceReader signEvidenceReader;
    private final AuditEventService auditEventService;
 
    /**
     * 只处理显式携带 relatedSignId 的数据接口事件。异常向上交给审计投递器进入 spool 重试,
     * 调用发生在审计服务内部,不会影响原始业务请求。
     */
    public void recordRelatedSign(AuditEventDTO businessEvent) {
        if (businessEvent == null || isBlank(businessEvent.getRelatedSignId())
                || !AuditConsts.TYPE_DATA_CHANGE.equals(businessEvent.getEventType())
                || !"DATA_INTERFACE".equals(businessEvent.getBizType())) {
            return;
        }
 
        String signId = businessEvent.getRelatedSignId().trim();
        Map<String, AuditSignEvidence> evidences = signEvidenceReader.read(
                Collections.singletonList(signId), businessEvent.getTenantId());
        AuditSignEvidence evidence = evidences.get(signId);
        if (evidence == null) {
            log.warn("[DATA-INTERFACE-AUDIT] 未查询到同租户签名记录,跳过签名事件 clientEventId={}",
                    businessEvent.getClientEventId());
            return;
        }
        if (isBlank(evidence.operatorId())
                || !evidence.operatorId().equals(businessEvent.getOperatorId())) {
            log.warn("[DATA-INTERFACE-AUDIT] 签名归属与当前操作人不一致,跳过签名事件 clientEventId={}",
                    businessEvent.getClientEventId());
            return;
        }
 
        AuditSignMetaData metaData = AuditSignMetaData.parse(evidence.metaData());
        String signatureRole = metaData.isReviewButton() ? "REVIEW" : "PRIMARY";
        Map<String, Object> extra = new LinkedHashMap<>();
        extra.put("signField", REQUEST_SIGN_FIELD);
        extra.put("signatureRole", signatureRole);
        extra.put("auditSource", "DATA_INTERFACE");
        if (!isBlank(evidence.accountName())) {
            extra.put("accountName", evidence.accountName());
        }
        if (!isBlank(evidence.opType())) {
            extra.put("opType", evidence.opType());
        }
 
        AuditEventDTO signEvent = AuditEventDTO.builder()
                .clientEventId(signClientEventId(businessEvent.getOperationId(), signId))
                .operationId(businessEvent.getOperationId())
                .eventTime(evidence.signedAt() == null
                        ? businessEvent.getEventTime() : evidence.signedAt())
                .tenantId(businessEvent.getTenantId())
                .operatorId(evidence.operatorId())
                .operatorName(businessEvent.getOperatorName())
                .operatorOrgId(businessEvent.getOperatorOrgId())
                .appName(businessEvent.getAppName())
                .bizModule(businessEvent.getBizModule())
                .ip(businessEvent.getIp())
                .ipRegion(businessEvent.getIpRegion())
                .browser(businessEvent.getBrowser())
                .os(businessEvent.getOs())
                .requestUri(businessEvent.getRequestUri())
                .eventType(AuditConsts.TYPE_E_SIGNATURE)
                .actionCode(SIGN_ACTION)
                .actionLabel(metaData.isReviewButton() ? "复审签名" : "电子签名")
                .sourceLayer(3)
                .targetTable(AuditSignEvidenceReader.SIGN_TABLE)
                .targetId(signId)
                .bizType(businessEvent.getBizType())
                .bizCode(businessEvent.getBizCode())
                .reason(evidence.note())
                .extra(JsonUtil.getObjectToString(extra))
                .recordTitle(businessEvent.getRecordTitle())
                .entryType(businessEvent.getEntryType())
                .entryId(businessEvent.getEntryId())
                .entryName(businessEvent.getEntryName())
                .build();
        auditEventService.saveIdempotent(signEvent);
    }
 
    private static String signClientEventId(String operationId, String signId) {
        String source = value(operationId) + "\n" + value(signId);
        try {
            byte[] digest = MessageDigest.getInstance("SHA-256")
                    .digest(source.getBytes(StandardCharsets.UTF_8));
            StringBuilder hex = new StringBuilder("DIS-");
            for (byte item : digest) {
                hex.append(String.format("%02x", item & 0xff));
            }
            return hex.toString();
        } catch (Exception e) {
            throw new IllegalStateException("生成数据接口签名事件幂等键失败", e);
        }
    }
 
    private static String value(String text) {
        return text == null ? "" : text;
    }
 
    private static boolean isBlank(String text) {
        return text == null || text.trim().isEmpty();
    }
}