add 新增Excel 导出脱敏处理器

This commit is contained in:
AprilWind 2026-06-23 16:20:31 +08:00
parent 612a965c26
commit 34ee552a5e
7 changed files with 210 additions and 2 deletions

View File

@ -22,6 +22,12 @@
<artifactId>ruoyi-common-json</artifactId> <artifactId>ruoyi-common-json</artifactId>
</dependency> </dependency>
<!-- 脱敏模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-sensitive</artifactId>
</dependency>
<!-- excel --> <!-- excel -->
<dependency> <dependency>
<groupId>org.apache.fesod</groupId> <groupId>org.apache.fesod</groupId>

View File

@ -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.Cell;
import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.util.CellRangeAddress; import org.apache.poi.ss.util.CellRangeAddress;
import org.dromara.common.excel.handler.CellMergeHandler;
import java.util.List; import java.util.List;

View File

@ -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.collection.CollUtil;
import cn.hutool.core.util.ReflectUtil; import cn.hutool.core.util.ReflectUtil;

View File

@ -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.collection.CollUtil;
import cn.hutool.core.convert.Convert; 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.ExcelDictFormat;
import org.dromara.common.excel.annotation.ExcelDynamicOptions; import org.dromara.common.excel.annotation.ExcelDynamicOptions;
import org.dromara.common.excel.annotation.ExcelEnumFormat; 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.lang.reflect.Field;
import java.util.*; import java.util.*;

View File

@ -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;
/**
* 有脱敏注解的列缓存
* <p>
* key 同时包含 sheet/table holder 身份与列号避免同一个写处理器在多 sheet 或多 table 写出时列号相同但字段不同
*/
private final Map<ColumnKey, Sensitive> sensitiveColumnMap = new ConcurrentHashMap<>();
/**
* 无脱敏注解的列缓存非敏感列命中后直接跳过避免每行重复解析字段注解
*/
private final Set<ColumnKey> 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
* <p>
* 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) {
}
}

View File

@ -27,6 +27,8 @@ import org.dromara.common.core.utils.file.FileUtils;
import org.dromara.common.excel.convert.ExcelBigNumberConvert; import org.dromara.common.excel.convert.ExcelBigNumberConvert;
import org.dromara.common.excel.core.*; import org.dromara.common.excel.core.*;
import org.dromara.common.excel.handler.DataWriteHandler; 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.io.*;
import java.net.URLEncoder; import java.net.URLEncoder;
@ -474,6 +476,8 @@ public final class ExcelBuilder<T> {
.autoCloseStream(false) .autoCloseStream(false)
// 大数值自动转换 防止失真 // 大数值自动转换 防止失真
.registerConverter(new ExcelBigNumberConvert()) .registerConverter(new ExcelBigNumberConvert())
// 敏感字段脱敏
.registerWriteHandler(new SensitiveWriteHandler())
// 批注必填项处理 // 批注必填项处理
.registerWriteHandler(new DataWriteHandler(headType)); .registerWriteHandler(new DataWriteHandler(headType));
if (columnWidth == null) { if (columnWidth == null) {

View File

@ -1,7 +1,13 @@
package org.dromara.demo.controller; package org.dromara.demo.controller;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import lombok.Data; 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.core.domain.R;
import org.dromara.common.excel.utils.ExcelBuilder;
import org.dromara.common.sensitive.annotation.Sensitive; import org.dromara.common.sensitive.annotation.Sensitive;
import org.dromara.common.sensitive.core.SensitiveService; import org.dromara.common.sensitive.core.SensitiveService;
import org.dromara.common.sensitive.core.SensitiveStrategy; 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.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/** /**
* 测试数据脱敏控制器 * 测试数据脱敏控制器
* <p> * <p>
@ -38,39 +46,61 @@ public class TestSensitiveController extends BaseController {
return R.ok(testSensitive); 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 @Data
@ExcelIgnoreUnannotated
@AllArgsConstructor
@NoArgsConstructor
static class TestSensitive { static class TestSensitive {
/** /**
* 身份证 * 身份证
*/ */
@ExcelProperty(value = "身份证", index = 0)
@Sensitive(strategy = SensitiveStrategy.ID_CARD) @Sensitive(strategy = SensitiveStrategy.ID_CARD)
private String idCard; private String idCard;
/** /**
* 电话 * 电话
*/ */
@ExcelProperty(value = "电话", index = 1)
@Sensitive(strategy = SensitiveStrategy.PHONE, roleKey = "common") @Sensitive(strategy = SensitiveStrategy.PHONE, roleKey = "common")
private String phone; private String phone;
/** /**
* 地址 * 地址
*/ */
@ExcelProperty(value = "地址", index = 2)
@Sensitive(strategy = SensitiveStrategy.ADDRESS, perms = "system:user:query") @Sensitive(strategy = SensitiveStrategy.ADDRESS, perms = "system:user:query")
private String address; private String address;
/** /**
* 邮箱 * 邮箱
*/ */
@ExcelProperty(value = "邮箱", index = 3)
@Sensitive(strategy = SensitiveStrategy.EMAIL, roleKey = "common", perms = "system:user:query1") @Sensitive(strategy = SensitiveStrategy.EMAIL, roleKey = "common", perms = "system:user:query1")
private String email; private String email;
/** /**
* 银行卡 * 银行卡
*/ */
@ExcelProperty(value = "银行卡", index = 4)
@Sensitive(strategy = SensitiveStrategy.BANK_CARD, roleKey = "common1", perms = "system:user:query") @Sensitive(strategy = SensitiveStrategy.BANK_CARD, roleKey = "common1", perms = "system:user:query")
private String bankCard; private String bankCard;