diff --git a/ruoyi-common/ruoyi-common-excel/pom.xml b/ruoyi-common/ruoyi-common-excel/pom.xml index 1ae034f05..556470e50 100644 --- a/ruoyi-common/ruoyi-common-excel/pom.xml +++ b/ruoyi-common/ruoyi-common-excel/pom.xml @@ -22,6 +22,12 @@ ruoyi-common-json + + + org.dromara + ruoyi-common-sensitive + + org.apache.fesod diff --git a/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/CellMergeStrategy.java b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/CellMergeStrategy.java index 8c1092398..5bd976e88 100644 --- a/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/CellMergeStrategy.java +++ b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/CellMergeStrategy.java @@ -10,6 +10,7 @@ import org.apache.fesod.sheet.write.metadata.holder.WriteWorkbookHolder; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.util.CellRangeAddress; +import org.dromara.common.excel.handler.CellMergeHandler; import java.util.List; diff --git a/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/CellMergeHandler.java b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/handler/CellMergeHandler.java similarity index 99% rename from ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/CellMergeHandler.java rename to ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/handler/CellMergeHandler.java index 55d6267f2..b82198af5 100644 --- a/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/CellMergeHandler.java +++ b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/handler/CellMergeHandler.java @@ -1,4 +1,4 @@ -package org.dromara.common.excel.core; +package org.dromara.common.excel.handler; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ReflectUtil; diff --git a/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/ExcelDownHandler.java b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/handler/ExcelDownHandler.java similarity index 99% rename from ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/ExcelDownHandler.java rename to ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/handler/ExcelDownHandler.java index d84580acb..5db6310e8 100644 --- a/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/ExcelDownHandler.java +++ b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/handler/ExcelDownHandler.java @@ -1,4 +1,4 @@ -package org.dromara.common.excel.core; +package org.dromara.common.excel.handler; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.convert.Convert; @@ -26,6 +26,8 @@ import org.dromara.common.core.utils.StringUtils; import org.dromara.common.excel.annotation.ExcelDictFormat; import org.dromara.common.excel.annotation.ExcelDynamicOptions; import org.dromara.common.excel.annotation.ExcelEnumFormat; +import org.dromara.common.excel.core.DropDownOptions; +import org.dromara.common.excel.core.ExcelOptionsProvider; import java.lang.reflect.Field; import java.util.*; diff --git a/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/handler/SensitiveWriteHandler.java b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/handler/SensitiveWriteHandler.java new file mode 100644 index 000000000..4e2442033 --- /dev/null +++ b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/handler/SensitiveWriteHandler.java @@ -0,0 +1,165 @@ +package org.dromara.common.excel.handler; + +import cn.hutool.core.util.ObjectUtil; +import org.apache.fesod.sheet.enums.CellDataTypeEnum; +import org.apache.fesod.sheet.metadata.Head; +import org.apache.fesod.sheet.metadata.data.WriteCellData; +import org.apache.fesod.sheet.metadata.property.ExcelContentProperty; +import org.apache.fesod.sheet.write.handler.CellWriteHandler; +import org.apache.fesod.sheet.write.handler.context.CellWriteHandlerContext; +import org.apache.fesod.sheet.write.metadata.holder.WriteSheetHolder; +import org.apache.fesod.sheet.write.metadata.holder.WriteTableHolder; +import org.dromara.common.core.utils.SpringUtils; +import org.dromara.common.sensitive.annotation.Sensitive; +import org.dromara.common.sensitive.core.SensitiveService; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; + +import java.lang.reflect.Field; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Excel 导出脱敏处理器。 + */ +public class SensitiveWriteHandler implements CellWriteHandler { + + /** + * 脱敏服务 Bean。服务本身只取一次,具体是否脱敏仍由服务在当前线程内按登录态判断。 + */ + private static volatile SensitiveService sensitiveService; + + /** + * 是否已尝试加载脱敏服务。没有引入实现时也只查一次,避免每个单元格反复访问 Spring 容器。 + */ + private static volatile boolean sensitiveServiceLoaded; + + /** + * 有脱敏注解的列缓存。 + *

+ * key 同时包含 sheet/table holder 身份与列号,避免同一个写处理器在多 sheet 或多 table 写出时列号相同但字段不同。 + */ + private final Map sensitiveColumnMap = new ConcurrentHashMap<>(); + + /** + * 无脱敏注解的列缓存。非敏感列命中后直接跳过,避免每行重复解析字段注解。 + */ + private final Set noneSensitiveColumnSet = ConcurrentHashMap.newKeySet(); + + /** + * 延迟获取脱敏服务 + * + * @return 脱敏服务 + */ + private static SensitiveService getSensitiveService() { + if (!sensitiveServiceLoaded) { + synchronized (SensitiveWriteHandler.class) { + if (!sensitiveServiceLoaded) { + try { + sensitiveService = SpringUtils.getBean(SensitiveService.class); + } catch (NoSuchBeanDefinitionException ignored) { + sensitiveService = null; + } + sensitiveServiceLoaded = true; + } + } + } + return sensitiveService; + } + + /** + * 在单元格数据转换后按字段脱敏注解改写导出值。 + * + * @param context 单元格写入上下文 + */ + @Override + public void afterCellDataConverted(CellWriteHandlerContext context) { + if (Boolean.TRUE.equals(context.getHead()) || context.getColumnIndex() == null + || !(context.getOriginalValue() instanceof String text)) { + return; + } + Sensitive sensitive = getSensitive(context); + if (sensitive == null) { + return; + } + SensitiveService service = getSensitiveService(); + if (ObjectUtil.isNotNull(service) && service.isSensitive(sensitive.roleKey(), sensitive.perms())) { + WriteCellData cellData = context.getFirstCellData(); + if (cellData != null) { + String value = sensitive.strategy().desensitizer().apply(text); + if (value == null) { + cellData.setType(CellDataTypeEnum.EMPTY); + return; + } + cellData.setType(CellDataTypeEnum.STRING); + cellData.setStringValue(value); + } + } + } + + /** + * 获取当前单元格对应字段上的脱敏注解。 + * + * @param context 单元格写入上下文 + * @return 脱敏注解 + */ + private Sensitive getSensitive(CellWriteHandlerContext context) { + ColumnKey columnKey = buildColumnKey(context); + Sensitive cached = sensitiveColumnMap.get(columnKey); + if (cached != null) { + return cached; + } + if (noneSensitiveColumnSet.contains(columnKey)) { + return null; + } + Sensitive sensitive = resolveSensitive(context); + if (sensitive == null) { + noneSensitiveColumnSet.add(columnKey); + return null; + } + sensitiveColumnMap.put(columnKey, sensitive); + return sensitive; + } + + /** + * 解析当前列对应字段上的脱敏注解。 + * + * @param context 单元格写入上下文 + * @return 脱敏注解 + */ + private Sensitive resolveSensitive(CellWriteHandlerContext context) { + Head headData = context.getHeadData(); + Field field = headData == null ? null : headData.getField(); + if (field == null) { + ExcelContentProperty contentProperty = context.getExcelContentProperty(); + field = contentProperty == null ? null : contentProperty.getField(); + } + return field == null ? null : field.getAnnotation(Sensitive.class); + } + + /** + * 构建列缓存 key。 + *

+ * Fesod 在自定义写出时同一个处理器可能服务多个 sheet/table + * + * @param context 单元格写入上下文 + * @return 列缓存 key + */ + private ColumnKey buildColumnKey(CellWriteHandlerContext context) { + WriteTableHolder tableHolder = context.getWriteTableHolder(); + if (tableHolder != null) { + return new ColumnKey(System.identityHashCode(tableHolder), context.getColumnIndex()); + } + WriteSheetHolder sheetHolder = context.getWriteSheetHolder(); + return new ColumnKey(System.identityHashCode(sheetHolder), context.getColumnIndex()); + } + + /** + * 列缓存 key。 + * + * @param holderIdentity sheet/table holder 对象身份 + * @param columnIndex 列索引 + */ + private record ColumnKey(int holderIdentity, int columnIndex) { + } +} diff --git a/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/utils/ExcelBuilder.java b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/utils/ExcelBuilder.java index d1ce4f6c0..412c6aa4e 100644 --- a/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/utils/ExcelBuilder.java +++ b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/utils/ExcelBuilder.java @@ -27,6 +27,8 @@ import org.dromara.common.core.utils.file.FileUtils; import org.dromara.common.excel.convert.ExcelBigNumberConvert; import org.dromara.common.excel.core.*; import org.dromara.common.excel.handler.DataWriteHandler; +import org.dromara.common.excel.handler.ExcelDownHandler; +import org.dromara.common.excel.handler.SensitiveWriteHandler; import java.io.*; import java.net.URLEncoder; @@ -474,6 +476,8 @@ public final class ExcelBuilder { .autoCloseStream(false) // 大数值自动转换 防止失真 .registerConverter(new ExcelBigNumberConvert()) + // 敏感字段脱敏 + .registerWriteHandler(new SensitiveWriteHandler()) // 批注必填项处理 .registerWriteHandler(new DataWriteHandler(headType)); if (columnWidth == null) { diff --git a/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/controller/TestSensitiveController.java b/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/controller/TestSensitiveController.java index fe4ed6c3c..de1ef4da6 100644 --- a/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/controller/TestSensitiveController.java +++ b/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/controller/TestSensitiveController.java @@ -1,7 +1,13 @@ package org.dromara.demo.controller; +import jakarta.servlet.http.HttpServletResponse; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated; +import org.apache.fesod.sheet.annotation.ExcelProperty; import org.dromara.common.core.domain.R; +import org.dromara.common.excel.utils.ExcelBuilder; import org.dromara.common.sensitive.annotation.Sensitive; import org.dromara.common.sensitive.core.SensitiveService; import org.dromara.common.sensitive.core.SensitiveStrategy; @@ -10,6 +16,8 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import java.util.List; + /** * 测试数据脱敏控制器 *

@@ -38,39 +46,61 @@ public class TestSensitiveController extends BaseController { return R.ok(testSensitive); } + /** + * 测试导出 Excel 脱敏 + */ + @GetMapping("/export") + public void exporttest(HttpServletResponse response) { + TestSensitive testSensitive = new TestSensitive(); + testSensitive.setIdCard("210397198608215431"); + testSensitive.setPhone("17640125371"); + testSensitive.setAddress("北京市朝阳区某某四合院1203室"); + testSensitive.setEmail("17640125371@163.com"); + testSensitive.setBankCard("6226456952351452853"); + ExcelBuilder.of(List.of(testSensitive), TestSensitive.class).sheetName("测试树表").toResponse(response); + } + /** * 脱敏测试对象。 */ @Data + @ExcelIgnoreUnannotated + @AllArgsConstructor + @NoArgsConstructor static class TestSensitive { /** * 身份证 */ + @ExcelProperty(value = "身份证", index = 0) @Sensitive(strategy = SensitiveStrategy.ID_CARD) private String idCard; /** * 电话 */ + @ExcelProperty(value = "电话", index = 1) @Sensitive(strategy = SensitiveStrategy.PHONE, roleKey = "common") private String phone; /** * 地址 */ + @ExcelProperty(value = "地址", index = 2) @Sensitive(strategy = SensitiveStrategy.ADDRESS, perms = "system:user:query") private String address; /** * 邮箱 */ + @ExcelProperty(value = "邮箱", index = 3) @Sensitive(strategy = SensitiveStrategy.EMAIL, roleKey = "common", perms = "system:user:query1") private String email; /** * 银行卡 */ + @ExcelProperty(value = "银行卡", index = 4) @Sensitive(strategy = SensitiveStrategy.BANK_CARD, roleKey = "common1", perms = "system:user:query") private String bankCard;