fix(matchmaker): 受控联系审计 afterData 改 JSON 序列化(运行时实测抓到 PG jsonb 500)
- 根因:ContactAuthorizationService 7 处 saveAudit afterData 传 key=value 裸串,PG audit_logs.after_data 为 jsonb 严格解析 → request/disclose 全链路 500;H2 测试库该列 CLOB 不校验 → 集成测试假绿 - 修复:新增 jsonOf 辅助(Jackson writeValueAsString,成对 kv),7 调用点全部改写;purpose/reason 用户输入由 Jackson 转义防 JSON 注入 - 守卫加固:MatchmakerContactAuthorizationTest 审计断言补「afterData 必须可 JSON 解析且为对象」(readTree 真解析)——运行时事故进入自动化基线 - 同型排查:全仓 recordSensitiveAccess/saveAuditLog 其他调用点 afterData 干净,污染面仅本文件 - 验证:定向 20/20(授权 13+disabled 2+守卫 5);负向注入还原裸串 → 恰好 JSON 解析守卫判红(错误信息与生产一致 Unrecognized token 'channel')→ 还原复绿 - 发现渠道:本地栈运行时实测(V056 上线后首次真 PG 验证),非测试环境
This commit is contained in:
+33
-7
@@ -1,5 +1,7 @@
|
|||||||
package com.danyang.match.platform.matchmaker.service;
|
package com.danyang.match.platform.matchmaker.service;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.danyang.match.platform.audit.entity.AuditLog;
|
import com.danyang.match.platform.audit.entity.AuditLog;
|
||||||
import com.danyang.match.platform.audit.entity.SensitiveAccessLog;
|
import com.danyang.match.platform.audit.entity.SensitiveAccessLog;
|
||||||
import com.danyang.match.platform.audit.mapper.AuditLogMapper;
|
import com.danyang.match.platform.audit.mapper.AuditLogMapper;
|
||||||
@@ -23,6 +25,7 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@@ -76,6 +79,9 @@ public class ContactAuthorizationService {
|
|||||||
private final SensitiveAccessLogMapper sensitiveAccessLogMapper;
|
private final SensitiveAccessLogMapper sensitiveAccessLogMapper;
|
||||||
private final UserMapper userMapper;
|
private final UserMapper userMapper;
|
||||||
|
|
||||||
|
/** afterData JSON 序列化(静态初始化,不参与构造注入,避免测试桩连带改动)。 */
|
||||||
|
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||||
|
|
||||||
// ==================== ① request:红娘发起申请 ====================
|
// ==================== ① request:红娘发起申请 ====================
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@@ -122,7 +128,7 @@ public class ContactAuthorizationService {
|
|||||||
auth.getId(), operatorId, userId, channel);
|
auth.getId(), operatorId, userId, channel);
|
||||||
saveAudit(operatorId, Constants.CONTACT_AUDIT_ACTION_REQUEST, auth.getId(),
|
saveAudit(operatorId, Constants.CONTACT_AUDIT_ACTION_REQUEST, auth.getId(),
|
||||||
assignment != null ? assignment.getId() : null,
|
assignment != null ? assignment.getId() : null,
|
||||||
"matchmaker.contact.request", "channel=" + channel + ";purpose=" + auth.getPurpose());
|
"matchmaker.contact.request", jsonOf("channel", channel, "purpose", auth.getPurpose()));
|
||||||
return auth;
|
return auth;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,7 +213,7 @@ public class ContactAuthorizationService {
|
|||||||
log.info("受控联系授权审批通过: authId={}, approver={}", authId, approverOperatorId);
|
log.info("受控联系授权审批通过: authId={}, approver={}", authId, approverOperatorId);
|
||||||
saveAudit(approverOperatorId, Constants.CONTACT_AUDIT_ACTION_APPROVE, authId,
|
saveAudit(approverOperatorId, Constants.CONTACT_AUDIT_ACTION_APPROVE, authId,
|
||||||
activeAssignmentIdOf(auth), "matchmaker.contact.approve",
|
activeAssignmentIdOf(auth), "matchmaker.contact.approve",
|
||||||
"result=approved;channel=" + auth.getChannel());
|
jsonOf("result", "approved", "channel", auth.getChannel()));
|
||||||
return auth;
|
return auth;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,7 +236,7 @@ public class ContactAuthorizationService {
|
|||||||
log.info("受控联系授权审批拒绝: authId={}, approver={}", authId, approverOperatorId);
|
log.info("受控联系授权审批拒绝: authId={}, approver={}", authId, approverOperatorId);
|
||||||
saveAudit(approverOperatorId, Constants.CONTACT_AUDIT_ACTION_APPROVE, authId,
|
saveAudit(approverOperatorId, Constants.CONTACT_AUDIT_ACTION_APPROVE, authId,
|
||||||
activeAssignmentIdOf(auth), "matchmaker.contact.approve",
|
activeAssignmentIdOf(auth), "matchmaker.contact.approve",
|
||||||
"result=rejected;channel=" + auth.getChannel());
|
jsonOf("result", "rejected", "channel", auth.getChannel()));
|
||||||
return auth;
|
return auth;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,7 +257,7 @@ public class ContactAuthorizationService {
|
|||||||
log.info("受控联系授权用户同意: authId={}, userId={}", authId, userId);
|
log.info("受控联系授权用户同意: authId={}, userId={}", authId, userId);
|
||||||
saveAudit(userId, Constants.CONTACT_AUDIT_ACTION_CONSENT, authId,
|
saveAudit(userId, Constants.CONTACT_AUDIT_ACTION_CONSENT, authId,
|
||||||
activeAssignmentIdOf(auth), "matchmaker.contact.consent",
|
activeAssignmentIdOf(auth), "matchmaker.contact.consent",
|
||||||
"channel=" + auth.getChannel());
|
jsonOf("channel", auth.getChannel()));
|
||||||
return auth;
|
return auth;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,7 +274,7 @@ public class ContactAuthorizationService {
|
|||||||
log.info("受控联系授权用户拒绝: authId={}, userId={}", authId, userId);
|
log.info("受控联系授权用户拒绝: authId={}, userId={}", authId, userId);
|
||||||
saveAudit(userId, Constants.CONTACT_AUDIT_ACTION_DECLINE, authId,
|
saveAudit(userId, Constants.CONTACT_AUDIT_ACTION_DECLINE, authId,
|
||||||
activeAssignmentIdOf(auth), "matchmaker.contact.decline",
|
activeAssignmentIdOf(auth), "matchmaker.contact.decline",
|
||||||
"channel=" + auth.getChannel());
|
jsonOf("channel", auth.getChannel()));
|
||||||
return auth;
|
return auth;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,7 +307,7 @@ public class ContactAuthorizationService {
|
|||||||
log.info("受控联系授权撤销: authId={}, by={}", authId, operatorId);
|
log.info("受控联系授权撤销: authId={}, by={}", authId, operatorId);
|
||||||
saveAudit(operatorId, Constants.CONTACT_AUDIT_ACTION_REVOKE, authId,
|
saveAudit(operatorId, Constants.CONTACT_AUDIT_ACTION_REVOKE, authId,
|
||||||
activeAssignmentIdOf(auth), "matchmaker.contact.revoke",
|
activeAssignmentIdOf(auth), "matchmaker.contact.revoke",
|
||||||
"reason=" + auth.getRevokeReason());
|
jsonOf("reason", auth.getRevokeReason()));
|
||||||
return auth;
|
return auth;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,7 +353,7 @@ public class ContactAuthorizationService {
|
|||||||
auth.getId(), operatorId, userId, auth.getChannel());
|
auth.getId(), operatorId, userId, auth.getChannel());
|
||||||
saveAudit(operatorId, Constants.CONTACT_AUDIT_ACTION_DISCLOSE, auth.getId(),
|
saveAudit(operatorId, Constants.CONTACT_AUDIT_ACTION_DISCLOSE, auth.getId(),
|
||||||
assignment != null ? assignment.getId() : null,
|
assignment != null ? assignment.getId() : null,
|
||||||
"matchmaker.contact.disclose", "channel=" + auth.getChannel());
|
"matchmaker.contact.disclose", jsonOf("channel", auth.getChannel()));
|
||||||
|
|
||||||
Map<String, Object> result = new HashMap<>();
|
Map<String, Object> result = new HashMap<>();
|
||||||
result.put("phone", phone);
|
result.put("phone", phone);
|
||||||
@@ -447,4 +453,24 @@ public class ContactAuthorizationService {
|
|||||||
auditLog.setRequestTraceId(MDC.get("requestId"));
|
auditLog.setRequestTraceId(MDC.get("requestId"));
|
||||||
auditLogMapper.insert(auditLog);
|
auditLogMapper.insert(auditLog);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* afterData 必须是合法 JSON(PG audit_logs.after_data 为 jsonb 严格解析;
|
||||||
|
* H2 测试库 CLOB 不校验——裸 key=value 拼接在测试侧假绿、生产侧 500)。
|
||||||
|
* kv 成对传入,Jackson 负责用户输入(purpose/reason)的 JSON 转义,防 JSON 注入。
|
||||||
|
*/
|
||||||
|
private String jsonOf(Object... kvPairs) {
|
||||||
|
if (kvPairs.length % 2 != 0) {
|
||||||
|
throw new IllegalStateException("jsonOf requires key-value pairs");
|
||||||
|
}
|
||||||
|
Map<String, Object> data = new LinkedHashMap<>();
|
||||||
|
for (int i = 0; i < kvPairs.length; i += 2) {
|
||||||
|
data.put(String.valueOf(kvPairs[i]), kvPairs[i + 1]);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return OBJECT_MAPPER.writeValueAsString(data);
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
throw new IllegalStateException("audit afterData JSON serialization failed", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+7
@@ -238,6 +238,13 @@ class MatchmakerContactAuthorizationTest {
|
|||||||
pair[0] + " 必须携带请求追踪 ID");
|
pair[0] + " 必须携带请求追踪 ID");
|
||||||
assertFalse(String.valueOf(audit.getAfterData()).contains(userPhone),
|
assertFalse(String.valueOf(audit.getAfterData()).contains(userPhone),
|
||||||
pair[0] + " 审计内容不得含 phone");
|
pair[0] + " 审计内容不得含 phone");
|
||||||
|
// afterData 必须是合法 JSON(PG audit_logs.after_data 为 jsonb 严格解析;
|
||||||
|
// H2 CLOB 不校验——裸 key=value 拼接在测试侧假绿、生产侧 500,此断言为运行时事故回归守卫)
|
||||||
|
com.fasterxml.jackson.databind.JsonNode afterJson =
|
||||||
|
assertDoesNotThrow(() -> new com.fasterxml.jackson.databind.ObjectMapper()
|
||||||
|
.readTree(String.valueOf(audit.getAfterData())),
|
||||||
|
pair[0] + " afterData 必须可 JSON 解析(PG jsonb 严格校验口径)");
|
||||||
|
assertTrue(afterJson.isObject(), pair[0] + " afterData 必须是 JSON 对象");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user