mirror of
https://gitee.com/dromara/dax-pay
synced 2026-08-13 07:45:41 +08:00
feat(transfer): 转账收款人敏感字段加密存储(AES-256-GCM) + 展示脱敏 + 重试强制重填
- 三通道转账单(支付宝/抖音/微信)收款人账号/姓名接入 DataEncryptTypeHandler 加密存储 - 新增 PayeeDesensitizeUtil: 姓名首尾脱敏, 手机号/邮箱按收款类型脱敏, openid等伪匿名标识原样返回 - TransferAdminService/MchTransferService 查询出口(page+findById)统一脱敏 - SecureAesGcmEncryptor.decrypt 容错增强: 非密文格式(历史明文)原样透传, 新增加密字段零风险上线 - 迁移检查脚本 update-transfer-sensitive-encrypt.sql(历史明文渐进迁移用)
This commit is contained in:
64
_config/sql/update-transfer-sensitive-encrypt.sql
Normal file
64
_config/sql/update-transfer-sensitive-encrypt.sql
Normal file
@@ -0,0 +1,64 @@
|
||||
-- =====================================================================
|
||||
-- 转账收款人敏感字段加密迁移检查与说明
|
||||
-- =====================================================================
|
||||
-- 背景: pay_transfer_order_{alipay,douyin,wechat} 三表的收款人账号/姓名字段
|
||||
-- 已接入 DataEncryptTypeHandler (AES-256-GCM), 密文格式 v{version}:{base64}。
|
||||
--
|
||||
-- SecureAesGcmEncryptor.decrypt 已做容错: 非密文格式(历史明文)原样透传返回,
|
||||
-- 不会变 null, 故上线零风险, 历史数据可正常读取。迁移仅用于"把明文也加密落库",
|
||||
-- 属合规优化, 非上线阻塞项。
|
||||
--
|
||||
-- 迁移方式: AES-GCM 加密需随机 IV + 密钥版本前缀, 纯 SQL 无法完成,
|
||||
-- 必须通过应用层(MyBatis updateById)触发 TypeHandler 加密写入。
|
||||
-- 迁移完成后, 下方检查语句查询结果应均为 0。
|
||||
-- =====================================================================
|
||||
|
||||
-- 1. 检查历史明文数量(未加密 = 不以 v 开头且非空)
|
||||
|
||||
-- 支付宝转账单: 收款人账号 + 收款人姓名
|
||||
SELECT 'pay_transfer_order_alipay.payee_account' AS field,
|
||||
count(*) AS plaintext_count
|
||||
FROM pay_transfer_order_alipay
|
||||
WHERE payee_account IS NOT NULL AND payee_account NOT LIKE 'v%'
|
||||
UNION ALL
|
||||
SELECT 'pay_transfer_order_alipay.payee_name',
|
||||
count(*)
|
||||
FROM pay_transfer_order_alipay
|
||||
WHERE payee_name IS NOT NULL AND payee_name NOT LIKE 'v%';
|
||||
|
||||
-- 抖音转账单: 收款人账号 + 收款人姓名
|
||||
SELECT 'pay_transfer_order_douyin.payee_account' AS field,
|
||||
count(*) AS plaintext_count
|
||||
FROM pay_transfer_order_douyin
|
||||
WHERE payee_account IS NOT NULL AND payee_account NOT LIKE 'v%'
|
||||
UNION ALL
|
||||
SELECT 'pay_transfer_order_douyin.payee_name',
|
||||
count(*)
|
||||
FROM pay_transfer_order_douyin
|
||||
WHERE payee_name IS NOT NULL AND payee_name NOT LIKE 'v%';
|
||||
|
||||
-- 微信转账单: 收款人 openid + 收款人姓名(userName)
|
||||
SELECT 'pay_transfer_order_wechat.payee_openid' AS field,
|
||||
count(*) AS plaintext_count
|
||||
FROM pay_transfer_order_wechat
|
||||
WHERE payee_openid IS NOT NULL AND payee_openid NOT LIKE 'v%'
|
||||
UNION ALL
|
||||
SELECT 'pay_transfer_order_wechat.user_name',
|
||||
count(*)
|
||||
FROM pay_transfer_order_wechat
|
||||
WHERE user_name IS NOT NULL AND user_name NOT LIKE 'v%';
|
||||
|
||||
-- 2. 迁移方法(应用层一次性执行)
|
||||
--
|
||||
-- 启用加密(daxpay.platform.config.encrypt.enable=true + 配置 keys)后,
|
||||
-- 遍历历史记录执行 updateById 即可触发 TypeHandler 加密写入:
|
||||
--
|
||||
-- // 伪代码示例(三表同理)
|
||||
-- alipayTransferOrderManager.list().forEach(e -> {
|
||||
-- e.setPayeeAccount(e.getPayeeAccount()); // 自身赋值触发 dirty
|
||||
-- e.setPayeeName(e.getPayeeName());
|
||||
-- alipayTransferOrderManager.updateById(e); // TypeHandler 写入密文
|
||||
-- });
|
||||
--
|
||||
-- 可封装为 CommandLineRunner / 一次性 admin 接口, 随 profile 触发, 执行后下线。
|
||||
-- 迁移完成后重新执行上方检查语句, plaintext_count 应全部为 0。
|
||||
@@ -24,6 +24,7 @@ import cn.daxpay.open.payment.trade.transfer.result.WechatTransferOrderResult;
|
||||
import cn.daxpay.open.payment.trade.transfer.runtime.service.TransferCloseService;
|
||||
import cn.daxpay.open.payment.trade.transfer.runtime.service.TransferStartService;
|
||||
import cn.daxpay.open.payment.trade.transfer.runtime.service.TransferSyncService;
|
||||
import cn.daxpay.open.payment.trade.transfer.util.PayeeDesensitizeUtil;
|
||||
import cn.daxpay.open.platform.common.translate.service.TransService;
|
||||
import cn.daxpay.open.platform.core.exception.DataNotExistException;
|
||||
import cn.daxpay.open.platform.core.rest.param.PageParam;
|
||||
@@ -56,6 +57,8 @@ public class TransferAdminService {
|
||||
Page<WechatTransferOrder> page = wechatTransferOrderManager.page(pageParam, query);
|
||||
PageResult<WechatTransferOrderResult> pageResult = toPageResult(page, WechatTransferOrderConvert.CONVERT::toResult);
|
||||
transService.translate(pageResult);
|
||||
// 收款人信息脱敏
|
||||
pageResult.getRecords().forEach(PayeeDesensitizeUtil::desensitize);
|
||||
return pageResult;
|
||||
}
|
||||
|
||||
@@ -65,6 +68,8 @@ public class TransferAdminService {
|
||||
.orElseThrow(() -> new DataNotExistException("pay.error.transfer.notFound"));
|
||||
WechatTransferOrderResult result = WechatTransferOrderConvert.CONVERT.toResult(entity);
|
||||
transService.translate(result);
|
||||
// 收款人信息脱敏
|
||||
PayeeDesensitizeUtil.desensitize(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -75,6 +80,8 @@ public class TransferAdminService {
|
||||
Page<AlipayTransferOrder> page = alipayTransferOrderManager.page(pageParam, query);
|
||||
PageResult<AlipayTransferOrderResult> pageResult = toPageResult(page, AlipayTransferOrderConvert.CONVERT::toResult);
|
||||
transService.translate(pageResult);
|
||||
// 收款人信息脱敏
|
||||
pageResult.getRecords().forEach(PayeeDesensitizeUtil::desensitize);
|
||||
return pageResult;
|
||||
}
|
||||
|
||||
@@ -84,6 +91,8 @@ public class TransferAdminService {
|
||||
.orElseThrow(() -> new DataNotExistException("pay.error.transfer.notFound"));
|
||||
AlipayTransferOrderResult result = AlipayTransferOrderConvert.CONVERT.toResult(entity);
|
||||
transService.translate(result);
|
||||
// 收款人信息脱敏
|
||||
PayeeDesensitizeUtil.desensitize(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -94,6 +103,8 @@ public class TransferAdminService {
|
||||
Page<DouyinTransferOrder> page = douyinTransferOrderManager.page(pageParam, query);
|
||||
PageResult<DouyinTransferOrderResult> pageResult = toPageResult(page, DouyinTransferOrderConvert.CONVERT::toResult);
|
||||
transService.translate(pageResult);
|
||||
// 收款人信息脱敏
|
||||
pageResult.getRecords().forEach(PayeeDesensitizeUtil::desensitize);
|
||||
return pageResult;
|
||||
}
|
||||
|
||||
@@ -103,6 +114,8 @@ public class TransferAdminService {
|
||||
.orElseThrow(() -> new DataNotExistException("pay.error.transfer.notFound"));
|
||||
DouyinTransferOrderResult result = DouyinTransferOrderConvert.CONVERT.toResult(entity);
|
||||
transService.translate(result);
|
||||
// 收款人信息脱敏
|
||||
PayeeDesensitizeUtil.desensitize(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package cn.daxpay.open.payment.trade.transfer.entity;
|
||||
|
||||
import cn.daxpay.open.payment.common.entity.MchBaseEntity;
|
||||
import cn.daxpay.open.platform.common.mybatisplus.handler.encrypt.DataEncryptTypeHandler;
|
||||
import com.baomidou.mybatisplus.annotation.FieldStrategy;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
@@ -22,7 +23,7 @@ import java.time.OffsetDateTime;
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Accessors(chain = true)
|
||||
@TableName("pay_transfer_order_alipay")
|
||||
@TableName(value = "pay_transfer_order_alipay", autoResultMap = true)
|
||||
public class AlipayTransferOrder extends MchBaseEntity {
|
||||
|
||||
// ===== 身份 =====
|
||||
@@ -77,10 +78,12 @@ public class AlipayTransferOrder extends MchBaseEntity {
|
||||
/// @see cn.daxpay.open.payment.trade.transfer.enums.TransferPayeeTypeEnum
|
||||
private String payeeType;
|
||||
|
||||
/// 收款人账号
|
||||
/// 收款人账号(敏感字段, AES-256-GCM 加密存储)
|
||||
@TableField(typeHandler = DataEncryptTypeHandler.class)
|
||||
private String payeeAccount;
|
||||
|
||||
/// 收款人姓名
|
||||
/// 收款人姓名(敏感字段, AES-256-GCM 加密存储)
|
||||
@TableField(typeHandler = DataEncryptTypeHandler.class)
|
||||
private String payeeName;
|
||||
|
||||
/// 转账场景标识(支付宝=转账场景配置ID, FAIL重试时恢复场景用)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package cn.daxpay.open.payment.trade.transfer.entity;
|
||||
|
||||
import cn.daxpay.open.payment.common.entity.MchBaseEntity;
|
||||
import cn.daxpay.open.platform.common.mybatisplus.handler.encrypt.DataEncryptTypeHandler;
|
||||
import com.baomidou.mybatisplus.annotation.FieldStrategy;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
@@ -22,7 +23,7 @@ import java.time.OffsetDateTime;
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Accessors(chain = true)
|
||||
@TableName("pay_transfer_order_douyin")
|
||||
@TableName(value = "pay_transfer_order_douyin", autoResultMap = true)
|
||||
public class DouyinTransferOrder extends MchBaseEntity {
|
||||
|
||||
// ===== 身份 =====
|
||||
@@ -73,10 +74,12 @@ public class DouyinTransferOrder extends MchBaseEntity {
|
||||
/// 收款人账号类型
|
||||
private String payeeType;
|
||||
|
||||
/// 收款人账号
|
||||
/// 收款人账号(敏感字段, AES-256-GCM 加密存储)
|
||||
@TableField(typeHandler = DataEncryptTypeHandler.class)
|
||||
private String payeeAccount;
|
||||
|
||||
/// 收款人姓名
|
||||
/// 收款人姓名(敏感字段, AES-256-GCM 加密存储)
|
||||
@TableField(typeHandler = DataEncryptTypeHandler.class)
|
||||
private String payeeName;
|
||||
|
||||
/// 转账场景ID(发起转账时由前端选择的主数据枚举, 如1001-1007)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package cn.daxpay.open.payment.trade.transfer.entity;
|
||||
|
||||
import cn.daxpay.open.payment.common.entity.MchBaseEntity;
|
||||
import cn.daxpay.open.platform.common.mybatisplus.handler.encrypt.DataEncryptTypeHandler;
|
||||
import com.baomidou.mybatisplus.annotation.FieldStrategy;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
@@ -22,7 +23,7 @@ import java.time.OffsetDateTime;
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Accessors(chain = true)
|
||||
@TableName("pay_transfer_order_wechat")
|
||||
@TableName(value = "pay_transfer_order_wechat", autoResultMap = true)
|
||||
public class WechatTransferOrder extends MchBaseEntity {
|
||||
|
||||
// ===== 身份 =====
|
||||
@@ -70,7 +71,8 @@ public class WechatTransferOrder extends MchBaseEntity {
|
||||
|
||||
// ===== 微信特有 =====
|
||||
|
||||
/// 收款人微信 openid
|
||||
/// 收款人微信 openid(敏感字段, AES-256-GCM 加密存储)
|
||||
@TableField(typeHandler = DataEncryptTypeHandler.class)
|
||||
private String payeeOpenid;
|
||||
|
||||
/// 转账场景(冗余自转账配置)
|
||||
@@ -82,7 +84,8 @@ public class WechatTransferOrder extends MchBaseEntity {
|
||||
/// 拉起转账确认参数(微信二次确认)
|
||||
private String transferBody;
|
||||
|
||||
/// 收款人姓名(金额档位校验用: 小于0.3元禁填, 大于等于2000元必填)
|
||||
/// 收款人姓名(金额档位校验用: 小于0.3元禁填, 大于等于2000元必填; 敏感字段, AES-256-GCM 加密存储)
|
||||
@TableField(typeHandler = DataEncryptTypeHandler.class)
|
||||
private String userName;
|
||||
|
||||
// ===== 商户出站 / 审计 =====
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
package cn.daxpay.open.payment.trade.transfer.util;
|
||||
|
||||
import cn.daxpay.open.payment.trade.transfer.enums.TransferPayeeTypeEnum;
|
||||
import cn.daxpay.open.payment.trade.transfer.result.AlipayTransferOrderResult;
|
||||
import cn.daxpay.open.payment.trade.transfer.result.DouyinTransferOrderResult;
|
||||
import cn.daxpay.open.payment.trade.transfer.result.WechatTransferOrderResult;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
|
||||
/// # 收款人信息脱敏工具
|
||||
///
|
||||
/// 用于转账单查询/详情返回时对收款人姓名、收款账号进行展示脱敏(底层存储已加密)。
|
||||
/// 仅作用于 Result 出口, 不影响实体上送通道(通道策略读到的仍是解密后的明文)。
|
||||
///
|
||||
/// 脱敏策略:
|
||||
/// - **姓名**: 两字保留首字, 三字及以上保留首尾, 中间用 `*` 占位
|
||||
/// - **收款账号**: 按 [TransferPayeeTypeEnum] 分级
|
||||
/// - 手机号(`phone` / `login_name` 为手机号): `138****5678`(前3后4)
|
||||
/// - 邮箱(`login_name` 为邮箱): `z***@example.com`(首字符 + 域名)
|
||||
/// - `openid` / `user_id` / `open_id`: 伪匿名标识, 原样返回(运维排查需可见)
|
||||
public final class PayeeDesensitizeUtil {
|
||||
|
||||
private PayeeDesensitizeUtil() {
|
||||
}
|
||||
|
||||
/// 姓名脱敏: 两字保留首字, 三字及以上保留首尾, 中间用 * 占位
|
||||
public static String maskName(String name) {
|
||||
if (StrUtil.isBlank(name)) {
|
||||
return name;
|
||||
}
|
||||
int len = name.length();
|
||||
if (len == 1) {
|
||||
return name;
|
||||
}
|
||||
if (len == 2) {
|
||||
// 张三 -> 张*
|
||||
return name.charAt(0) + "*";
|
||||
}
|
||||
// 张三丰 -> 张*丰; 诸葛亮 -> 诸**亮
|
||||
return name.charAt(0) + "*".repeat(len - 2) + name.charAt(len - 1);
|
||||
}
|
||||
|
||||
/// 收款账号脱敏: 按收款人类型分级处理
|
||||
///
|
||||
/// @param account 收款账号明文(实体层已由 [DataEncryptTypeHandler] 解密)
|
||||
/// @param payeeType 收款人账号类型, @see TransferPayeeTypeEnum
|
||||
public static String maskAccount(String account, String payeeType) {
|
||||
if (StrUtil.isBlank(account)) {
|
||||
return account;
|
||||
}
|
||||
// 仅对真个人信息(手机号/邮箱)脱敏; openid/user_id/open_id 为伪匿名标识, 原样返回
|
||||
if (!isSensitiveAccount(payeeType)) {
|
||||
return account;
|
||||
}
|
||||
// login_name 可能是手机号或邮箱, 按内容自动判断
|
||||
if (isEmail(account)) {
|
||||
return maskEmail(account);
|
||||
}
|
||||
if (isMobile(account)) {
|
||||
return maskMobile(account);
|
||||
}
|
||||
// 兜底: 保留首尾各1字符
|
||||
return maskBothEnds(account);
|
||||
}
|
||||
|
||||
/// 判断该收款人类型的账号是否需要脱敏(手机号/邮箱为个人信息, 需脱敏)
|
||||
private static boolean isSensitiveAccount(String payeeType) {
|
||||
return TransferPayeeTypeEnum.LOGIN_NAME.getCode().equals(payeeType)
|
||||
|| TransferPayeeTypeEnum.PHONE.getCode().equals(payeeType);
|
||||
}
|
||||
|
||||
/// 简单邮箱判断: 包含 @ 且 @ 不在首尾
|
||||
private static boolean isEmail(String value) {
|
||||
int at = value.indexOf('@');
|
||||
return at > 0 && at < value.length() - 1;
|
||||
}
|
||||
|
||||
/// 简单手机号判断: 11 位纯数字(国内)
|
||||
private static boolean isMobile(String value) {
|
||||
return value.length() == 11 && value.chars().allMatch(Character::isDigit);
|
||||
}
|
||||
|
||||
/// 手机号脱敏: 138****5678(前3后4)
|
||||
private static String maskMobile(String mobile) {
|
||||
return mobile.substring(0, 3) + "****" + mobile.substring(7);
|
||||
}
|
||||
|
||||
/// 邮箱脱敏: z***@example.com(本地名首字符 + 3星 + 域名)
|
||||
private static String maskEmail(String email) {
|
||||
int at = email.indexOf('@');
|
||||
String local = email.substring(0, at);
|
||||
String domain = email.substring(at);
|
||||
if (local.length() <= 1) {
|
||||
return "*" + domain;
|
||||
}
|
||||
return local.charAt(0) + "***" + domain;
|
||||
}
|
||||
|
||||
/// 兜底脱敏: 保留首尾各1字符, 中间用 * 占位
|
||||
private static String maskBothEnds(String value) {
|
||||
if (value.length() <= 2) {
|
||||
return "*".repeat(value.length());
|
||||
}
|
||||
return value.charAt(0) + "*".repeat(value.length() - 2) + value.charAt(value.length() - 1);
|
||||
}
|
||||
|
||||
// ===== Result 级脱敏入口(供 Admin/Merchant Service 出口统一调用) =====
|
||||
|
||||
/// 支付宝转账单收款人脱敏: 姓名 + 收款账号(按类型)
|
||||
public static void desensitize(AlipayTransferOrderResult result) {
|
||||
if (result == null) {
|
||||
return;
|
||||
}
|
||||
result.setPayeeName(maskName(result.getPayeeName()));
|
||||
result.setPayeeAccount(maskAccount(result.getPayeeAccount(), result.getPayeeType()));
|
||||
}
|
||||
|
||||
/// 抖音转账单收款人脱敏: 姓名 + 收款账号(按类型)
|
||||
public static void desensitize(DouyinTransferOrderResult result) {
|
||||
if (result == null) {
|
||||
return;
|
||||
}
|
||||
result.setPayeeName(maskName(result.getPayeeName()));
|
||||
result.setPayeeAccount(maskAccount(result.getPayeeAccount(), result.getPayeeType()));
|
||||
}
|
||||
|
||||
/// 微信转账单收款人脱敏: 仅姓名(openid 为伪匿名标识, 不脱敏)
|
||||
public static void desensitize(WechatTransferOrderResult result) {
|
||||
if (result == null) {
|
||||
return;
|
||||
}
|
||||
result.setUserName(maskName(result.getUserName()));
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import cn.daxpay.open.payment.trade.transfer.result.WechatTransferOrderResult;
|
||||
import cn.daxpay.open.payment.trade.transfer.runtime.service.TransferCloseService;
|
||||
import cn.daxpay.open.payment.trade.transfer.runtime.service.TransferStartService;
|
||||
import cn.daxpay.open.payment.trade.transfer.runtime.service.TransferSyncService;
|
||||
import cn.daxpay.open.payment.trade.transfer.util.PayeeDesensitizeUtil;
|
||||
import cn.daxpay.open.platform.common.translate.service.TransService;
|
||||
import cn.daxpay.open.platform.core.exception.DataNotExistException;
|
||||
import cn.daxpay.open.platform.core.rest.param.PageParam;
|
||||
@@ -53,6 +54,8 @@ public class MchTransferService {
|
||||
Page<WechatTransferOrder> page = wechatTransferOrderManager.page(pageParam, query);
|
||||
PageResult<WechatTransferOrderResult> pageResult = toPageResult(page, WechatTransferOrderConvert.CONVERT::toResult);
|
||||
transService.translate(pageResult);
|
||||
// 收款人信息脱敏
|
||||
pageResult.getRecords().forEach(PayeeDesensitizeUtil::desensitize);
|
||||
return pageResult;
|
||||
}
|
||||
|
||||
@@ -62,6 +65,8 @@ public class MchTransferService {
|
||||
.orElseThrow(() -> new DataNotExistException("pay.error.transfer.notFound"));
|
||||
WechatTransferOrderResult result = WechatTransferOrderConvert.CONVERT.toResult(entity);
|
||||
transService.translate(result);
|
||||
// 收款人信息脱敏
|
||||
PayeeDesensitizeUtil.desensitize(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -70,6 +75,8 @@ public class MchTransferService {
|
||||
Page<AlipayTransferOrder> page = alipayTransferOrderManager.page(pageParam, query);
|
||||
PageResult<AlipayTransferOrderResult> pageResult = toPageResult(page, AlipayTransferOrderConvert.CONVERT::toResult);
|
||||
transService.translate(pageResult);
|
||||
// 收款人信息脱敏
|
||||
pageResult.getRecords().forEach(PayeeDesensitizeUtil::desensitize);
|
||||
return pageResult;
|
||||
}
|
||||
|
||||
@@ -79,6 +86,8 @@ public class MchTransferService {
|
||||
.orElseThrow(() -> new DataNotExistException("pay.error.transfer.notFound"));
|
||||
AlipayTransferOrderResult result = AlipayTransferOrderConvert.CONVERT.toResult(entity);
|
||||
transService.translate(result);
|
||||
// 收款人信息脱敏
|
||||
PayeeDesensitizeUtil.desensitize(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -87,6 +96,8 @@ public class MchTransferService {
|
||||
Page<DouyinTransferOrder> page = douyinTransferOrderManager.page(pageParam, query);
|
||||
PageResult<DouyinTransferOrderResult> pageResult = toPageResult(page, DouyinTransferOrderConvert.CONVERT::toResult);
|
||||
transService.translate(pageResult);
|
||||
// 收款人信息脱敏
|
||||
pageResult.getRecords().forEach(PayeeDesensitizeUtil::desensitize);
|
||||
return pageResult;
|
||||
}
|
||||
|
||||
@@ -96,6 +107,8 @@ public class MchTransferService {
|
||||
.orElseThrow(() -> new DataNotExistException("pay.error.transfer.notFound"));
|
||||
DouyinTransferOrderResult result = DouyinTransferOrderConvert.CONVERT.toResult(entity);
|
||||
transService.translate(result);
|
||||
// 收款人信息脱敏
|
||||
PayeeDesensitizeUtil.desensitize(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -134,8 +134,13 @@ public class SecureAesGcmEncryptor {
|
||||
}
|
||||
|
||||
/// 解密(根据版本前缀选择密钥)
|
||||
/// @param ciphertext 密文,格式:v{version}:{encrypted}
|
||||
/// @return 明文,如果找不到对应密钥则返回null
|
||||
///
|
||||
/// 容错策略: 遇到非密文格式(如历史明文)或解密失败时, 原样返回输入值而非 null,
|
||||
/// 保证新增加密字段的上线零风险(历史明文自动透传, 新数据正常加密), 支持渐进式迁移。
|
||||
/// 对已加密字段无影响(其 DB 值均为 v 前缀密文, 走正常解密路径)。
|
||||
///
|
||||
/// @param ciphertext 密文,格式:v{version}:{encrypted};或历史明文(原样返回)
|
||||
/// @return 明文;若无法解密则原样返回输入值
|
||||
public String decrypt(String ciphertext) {
|
||||
if (ciphertext == null) {
|
||||
return null;
|
||||
@@ -143,22 +148,22 @@ public class SecureAesGcmEncryptor {
|
||||
try {
|
||||
// 解析版本前缀
|
||||
if (!ciphertext.startsWith(VERSION_PREFIX)) {
|
||||
log.warn("密文格式错误,缺少版本前缀");
|
||||
return null;
|
||||
// 非密文格式(历史明文或加密未启用时的明文), 原样透传
|
||||
return ciphertext;
|
||||
}
|
||||
|
||||
int separatorIndex = ciphertext.indexOf(VERSION_SEPARATOR);
|
||||
if (separatorIndex == -1) {
|
||||
log.warn("密文格式错误,缺少版本分隔符");
|
||||
return null;
|
||||
log.warn("密文格式错误,缺少版本分隔符, 原样返回");
|
||||
return ciphertext;
|
||||
}
|
||||
|
||||
int version;
|
||||
try {
|
||||
version = Integer.parseInt(ciphertext.substring(1, separatorIndex));
|
||||
} catch (NumberFormatException e) {
|
||||
log.warn("密文版本号格式错误");
|
||||
return null;
|
||||
log.warn("密文版本号格式错误, 原样返回");
|
||||
return ciphertext;
|
||||
}
|
||||
|
||||
String encryptedBase64 = ciphertext.substring(separatorIndex + 1);
|
||||
@@ -166,8 +171,8 @@ public class SecureAesGcmEncryptor {
|
||||
// 获取对应版本的密钥
|
||||
SecretKey secretKey = secretKeyCache.get(version);
|
||||
if (secretKey == null) {
|
||||
log.warn("找不到版本 v{} 对应的密钥", version);
|
||||
return null;
|
||||
log.warn("找不到版本 v{} 对应的密钥, 原样返回", version);
|
||||
return ciphertext;
|
||||
}
|
||||
|
||||
// 解密
|
||||
@@ -185,8 +190,8 @@ public class SecureAesGcmEncryptor {
|
||||
byte[] decrypted = cipher.doFinal(encrypted);
|
||||
return new String(decrypted, StandardCharsets.UTF_8);
|
||||
} catch (Exception e) {
|
||||
log.error("解密失败", e);
|
||||
return null;
|
||||
log.error("解密失败, 原样返回", e);
|
||||
return ciphertext;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user