package jnpf.bizcommon.audit.service.support;
|
|
import com.alibaba.fastjson.JSON;
|
import jnpf.audit.diff.AuditFieldDiff;
|
|
import java.util.ArrayList;
|
import java.util.LinkedHashSet;
|
import java.util.List;
|
import java.util.Locale;
|
import java.util.Map;
|
import java.util.Set;
|
|
/** 审计查询响应的字段名装饰与签名凭证脱敏;不修改原始事件。 */
|
public final class AuditQueryResponseSanitizer {
|
|
public static final String SIGN_REFERENCE_LABEL = "电子签名已关联";
|
|
private AuditQueryResponseSanitizer() {
|
}
|
|
/**
|
* 装饰 field_diffs 的展示名,并对历史 biz_sign / *_sign 值做响应级脱敏。
|
* JSON 已损坏时返回 null,避免绕过结构化脱敏。
|
*/
|
@SuppressWarnings("unchecked")
|
public static String sanitizeFieldDiffs(
|
String json, Map<String, AuditFieldDiff.FieldMeta> metadata) {
|
if (json == null || json.trim().isEmpty()) {
|
return json;
|
}
|
try {
|
Object parsed = JSON.parse(json);
|
if (!(parsed instanceof List)) {
|
return null;
|
}
|
List<Object> effective = new ArrayList<>();
|
for (Object raw : (List<?>) parsed) {
|
if (!(raw instanceof Map)) {
|
continue;
|
}
|
Map<String, Object> item = (Map<String, Object>) raw;
|
if (isNoopDiff(item)) {
|
continue;
|
}
|
sanitizeChildDiff(item);
|
String field = string(item.get("field"));
|
if (field == null) {
|
continue;
|
}
|
AuditFieldDiff.FieldMeta meta = lookup(metadata, field);
|
if (isCredentialReference(field, item, meta)) {
|
item.put("fieldName", SIGN_REFERENCE_LABEL);
|
item.put("masked", true);
|
item.put("valueType", "masked");
|
item.put("componentType", "sign");
|
if (item.containsKey("oldData")) {
|
item.put("oldData", AuditFieldDiff.MASKED);
|
}
|
if (item.containsKey("newData")) {
|
item.put("newData", AuditFieldDiff.MASKED);
|
}
|
item.remove("oldDisplay");
|
item.remove("newDisplay");
|
effective.add(item);
|
continue;
|
}
|
|
String fieldName = string(item.get("fieldName"));
|
if (fieldName == null || (field != null && fieldName.equalsIgnoreCase(field))) {
|
String label = meta == null ? null : meta.label();
|
item.put("fieldName", label == null || label.trim().isEmpty()
|
? databaseFieldName(metadata, field) : label);
|
}
|
effective.add(item);
|
}
|
return JSON.toJSONString(effective);
|
} catch (Throwable t) {
|
return null;
|
}
|
}
|
|
/** 深度脱敏 extra / data_snapshot 中以敏感键承载的凭证值。 */
|
public static String sanitizeJson(String json) {
|
return sanitizeJson(json, null);
|
}
|
|
/** 深度脱敏,并用字段元数据识别非固定列名的签名控件。 */
|
public static String sanitizeJson(
|
String json, Map<String, AuditFieldDiff.FieldMeta> metadata) {
|
if (json == null || json.trim().isEmpty()) {
|
return json;
|
}
|
try {
|
Object parsed = JSON.parse(json);
|
sanitizeValue(parsed, metadata);
|
return JSON.toJSONString(parsed);
|
} catch (Throwable t) {
|
return null;
|
}
|
}
|
|
/** 从层 0 extra 取 sourceModelId,只用于查询当前字段元数据。 */
|
@SuppressWarnings("unchecked")
|
public static String sourceModelId(String extra) {
|
if (extra == null || extra.trim().isEmpty()) {
|
return null;
|
}
|
try {
|
Object parsed = JSON.parse(extra);
|
if (!(parsed instanceof Map)) {
|
return null;
|
}
|
return string(((Map<String, Object>) parsed).get("sourceModelId"));
|
} catch (Throwable t) {
|
return null;
|
}
|
}
|
|
public static boolean hidesTargetId(String targetTable) {
|
return targetTable != null && "lims_sign".equalsIgnoreCase(targetTable.trim());
|
}
|
|
/** 列表摘要与详情共用的有效 diff 计数;会排除空到空及相同值伪变更。 */
|
@SuppressWarnings("unchecked")
|
public static int effectiveDiffCount(String json) {
|
return effectiveDiffFields(json).size();
|
}
|
|
/** 返回有效变更字段,供同一操作的多条业务事件按字段并集合并摘要。 */
|
@SuppressWarnings("unchecked")
|
public static Set<String> effectiveDiffFields(String json) {
|
Set<String> fields = new LinkedHashSet<>();
|
if (json == null || json.trim().isEmpty()) {
|
return fields;
|
}
|
try {
|
Object parsed = JSON.parse(json);
|
if (!(parsed instanceof List)) {
|
return fields;
|
}
|
for (Object raw : (List<?>) parsed) {
|
if (!(raw instanceof Map)) {
|
continue;
|
}
|
Map<String, Object> item = (Map<String, Object>) raw;
|
String field = string(item.get("field"));
|
if (field != null && !isNoopDiff(item)) {
|
String targetTable = string(item.get("targetTable"));
|
fields.add(AuditDiffKey.of(targetTable, field));
|
}
|
}
|
} catch (Throwable ignored) {
|
// 非法历史 JSON 按无变更降级,不能影响列表查询。
|
}
|
return fields;
|
}
|
|
public static boolean isCredentialReference(String field) {
|
if (field == null) {
|
return false;
|
}
|
String trimmed = field.trim();
|
String lower = trimmed.toLowerCase(Locale.ROOT);
|
return "biz_sign".equals(lower)
|
|| "bizsign".equals(lower)
|
|| "sign_id".equals(lower)
|
|| "signid".equals(lower)
|
|| lower.endsWith("_sign")
|
|| trimmed.endsWith("Sign")
|
|| trimmed.endsWith("SignId");
|
}
|
|
/** 字段名、事件快照元数据和当前表单元数据任一确认是签名凭证,都必须脱敏。 */
|
public static boolean isCredentialReference(String field, Map<String, Object> item,
|
AuditFieldDiff.FieldMeta metadata) {
|
return isCredentialReference(field)
|
|| bool(item == null ? null : item.get("masked"))
|
|| "masked".equalsIgnoreCase(string(item == null ? null : item.get("valueType")))
|
|| isSignComponent(item == null ? null : item.get("componentType"))
|
|| isSignComponent(item == null ? null : item.get("jnpfKey"))
|
|| AuditFieldDiff.isCredentialMetadata(metadata);
|
}
|
|
private static boolean isNoopDiff(Map<String, Object> item) {
|
// 历史坏数据可能只保留 field/fieldName。没有完整前后值时无法证明是伪变更,
|
// 应继续返回供证据追踪;只有两侧值键都存在时才过滤空到空或完全相同。
|
if (!item.containsKey("oldData") || !item.containsKey("newData")) {
|
return false;
|
}
|
Object oldValue = item.get("oldData");
|
Object newValue = item.get("newData");
|
if (AuditFieldDiff.MASKED.equals(String.valueOf(oldValue))
|
|| AuditFieldDiff.MASKED.equals(String.valueOf(newValue))) {
|
return false;
|
}
|
return canonical(oldValue).equals(canonical(newValue));
|
}
|
|
@SuppressWarnings("unchecked")
|
private static void sanitizeChildDiff(Map<String, Object> item) {
|
Object fieldsRaw = item.get("chidField");
|
Object dataRaw = item.get("chidData");
|
if (!(fieldsRaw instanceof List) || !(dataRaw instanceof List)) {
|
return;
|
}
|
for (Object rawField : (List<?>) fieldsRaw) {
|
if (!(rawField instanceof Map)) {
|
continue;
|
}
|
Map<String, Object> field = (Map<String, Object>) rawField;
|
String prop = string(field.get("prop"));
|
boolean credential = prop != null && (isCredentialReference(prop)
|
|| isSignComponent(field.get("jnpfKey"))
|
|| bool(field.get("masked")));
|
if (!credential) {
|
continue;
|
}
|
field.put("masked", true);
|
for (Object rawRow : (List<?>) dataRaw) {
|
if (!(rawRow instanceof Map)) {
|
continue;
|
}
|
Map<String, Object> row = (Map<String, Object>) rawRow;
|
if (row.containsKey(prop)) {
|
row.put(prop, AuditFieldDiff.MASKED);
|
}
|
String oldProp = "jnpf_old_" + prop;
|
if (row.containsKey(oldProp)) {
|
row.put(oldProp, AuditFieldDiff.MASKED);
|
}
|
}
|
}
|
}
|
|
private static String canonical(Object value) {
|
if (value == null) {
|
return "";
|
}
|
if (value instanceof CharSequence) {
|
String text = value.toString();
|
return text.trim().isEmpty() ? "" : "string:" + text;
|
}
|
if (value instanceof java.util.Collection && ((java.util.Collection<?>) value).isEmpty()) {
|
return "";
|
}
|
if (value instanceof Map && ((Map<?, ?>) value).isEmpty()) {
|
return "";
|
}
|
return "json:" + JSON.toJSONString(value);
|
}
|
|
@SuppressWarnings("unchecked")
|
private static void sanitizeValue(
|
Object value, Map<String, AuditFieldDiff.FieldMeta> metadata) {
|
if (value instanceof Map) {
|
Map<Object, Object> map = (Map<Object, Object>) value;
|
for (Map.Entry<Object, Object> entry : map.entrySet()) {
|
String field = String.valueOf(entry.getKey());
|
if (isCredentialReference(field)
|
|| AuditFieldDiff.isCredentialMetadata(lookup(metadata, field))) {
|
entry.setValue(AuditFieldDiff.MASKED);
|
} else {
|
sanitizeValue(entry.getValue(), metadata);
|
}
|
}
|
} else if (value instanceof List) {
|
for (Object item : (List<?>) value) {
|
sanitizeValue(item, metadata);
|
}
|
}
|
}
|
|
private static boolean isSignComponent(Object value) {
|
String component = string(value);
|
return "sign".equalsIgnoreCase(component) || "signature".equalsIgnoreCase(component);
|
}
|
|
private static boolean bool(Object value) {
|
return value instanceof Boolean ? (Boolean) value
|
: value != null && "true".equalsIgnoreCase(String.valueOf(value));
|
}
|
|
private static AuditFieldDiff.FieldMeta lookup(
|
Map<String, AuditFieldDiff.FieldMeta> metadata, String field) {
|
if (metadata == null || field == null) {
|
return null;
|
}
|
AuditFieldDiff.FieldMeta exact = metadata.get(field);
|
if (exact != null) {
|
return exact;
|
}
|
AuditFieldDiff.FieldMeta normalized = metadata.get(field.toLowerCase(Locale.ROOT));
|
return normalized != null ? normalized : metadata.get(camelToSnake(field));
|
}
|
|
private static String databaseFieldName(
|
Map<String, AuditFieldDiff.FieldMeta> metadata, String field) {
|
String snake = camelToSnake(field);
|
return metadata != null && metadata.containsKey(snake) ? snake : field;
|
}
|
|
private static String camelToSnake(String value) {
|
StringBuilder out = new StringBuilder(value.length() + 8);
|
for (int i = 0; i < value.length(); i++) {
|
char ch = value.charAt(i);
|
if (Character.isUpperCase(ch)) {
|
out.append('_').append(Character.toLowerCase(ch));
|
} else {
|
out.append(Character.toLowerCase(ch));
|
}
|
}
|
return out.toString();
|
}
|
|
private static String string(Object value) {
|
if (value == null) {
|
return null;
|
}
|
String text = String.valueOf(value).trim();
|
return text.isEmpty() ? null : text;
|
}
|
}
|