diff --git a/README.md b/README.md index 9f7f84b9..ad99f495 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ - [x] 数据查询-多表关联 - [x] 数据查询-主子表关联 - [x] 数据查询-树形构建 - - [ ] 数据脱敏 + - [x] 数据脱敏 * [ ] 扩展能力接入 - [x] 代码内创建表 - [x] 软删除 diff --git a/my-boot/src/test/java/net/ximatai/muyun/test/core/TestDesensitizationAbility.java b/my-boot/src/test/java/net/ximatai/muyun/test/core/TestDesensitizationAbility.java new file mode 100644 index 00000000..ca37cf30 --- /dev/null +++ b/my-boot/src/test/java/net/ximatai/muyun/test/core/TestDesensitizationAbility.java @@ -0,0 +1,87 @@ +package net.ximatai.muyun.test.core; + +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusTest; +import jakarta.inject.Inject; +import jakarta.ws.rs.Path; +import net.ximatai.muyun.ability.IDesensitizationAbility; +import net.ximatai.muyun.ability.ITableCreateAbility; +import net.ximatai.muyun.ability.curd.std.ICURDAbility; +import net.ximatai.muyun.core.Scaffold; +import net.ximatai.muyun.core.desensitization.Desensitizer; +import net.ximatai.muyun.core.desensitization.MaskMiddleAlgorithm; +import net.ximatai.muyun.core.security.SMEncryptor; +import net.ximatai.muyun.database.IDatabaseAccess; +import net.ximatai.muyun.database.builder.Column; +import net.ximatai.muyun.database.builder.TableWrapper; +import net.ximatai.muyun.test.testcontainers.PostgresTestResource; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +@QuarkusTest +@QuarkusTestResource(value = PostgresTestResource.class, restrictToAnnotatedClass = true) +class TestDesensitizationAbility { + + private String path = "/TestSecurityAbility"; + + @Inject + SMEncryptor smEncryptor; + + @Inject + IDatabaseAccess databaseAccess; + + @Inject + TestDesensitizationAbilityController testController; + + @Test + void test() { + String text = "hello world!"; + String id = testController.create(Map.of( + "v_name", text + )); + Map response = testController.view(id); + + String responseVName = (String) response.get("v_name"); + assertEquals(text.length(), responseVName.length()); + assertNotEquals(text, responseVName); + assertEquals("h**********!", responseVName); + assertNull(response.get("v_name2")); + } + +} + +@Path("/TestDesensitizationAbility") +class TestDesensitizationAbilityController extends Scaffold implements ICURDAbility, ITableCreateAbility, IDesensitizationAbility { + + @Inject + SMEncryptor smEncryptor; + + @Override + public String getSchemaName() { + return "test"; + } + + @Override + public String getMainTable() { + return "testdesensitizationability"; + } + + @Override + public TableWrapper fitOutTable() { + return TableWrapper.withName(getMainTable()) + .setSchema(getSchemaName()) + .setPrimaryKey(Column.ID_POSTGRES) + .addColumn(Column.of("v_name").setType("varchar")) + .addColumn(Column.of("v_name2").setType("varchar")) + .addColumn(Column.of("t_create").setDefaultValue("now()")); + } + + + @Override + public Desensitizer getDesensitizer() { + return new Desensitizer().registerAlgorithm("v_name", new MaskMiddleAlgorithm()); + } +} diff --git a/my-core/src/main/java/net/ximatai/muyun/ability/IDesensitizationAbility.java b/my-core/src/main/java/net/ximatai/muyun/ability/IDesensitizationAbility.java new file mode 100644 index 00000000..1a04161f --- /dev/null +++ b/my-core/src/main/java/net/ximatai/muyun/ability/IDesensitizationAbility.java @@ -0,0 +1,17 @@ +package net.ximatai.muyun.ability; + +import net.ximatai.muyun.core.desensitization.Desensitizer; + +import java.util.Map; + +public interface IDesensitizationAbility { + + Desensitizer getDesensitizer(); + + default void desensitize(Map map) { + Desensitizer desensitizer = getDesensitizer(); + if (desensitizer == null) return; + + map.replaceAll((k, v) -> v != null ? desensitizer.desensitize(k, v.toString()) : null); + } +} diff --git a/my-core/src/main/java/net/ximatai/muyun/ability/curd/std/ISelectAbility.java b/my-core/src/main/java/net/ximatai/muyun/ability/curd/std/ISelectAbility.java index a76d1709..af4def95 100644 --- a/my-core/src/main/java/net/ximatai/muyun/ability/curd/std/ISelectAbility.java +++ b/my-core/src/main/java/net/ximatai/muyun/ability/curd/std/ISelectAbility.java @@ -5,6 +5,7 @@ import jakarta.ws.rs.PathParam; import jakarta.ws.rs.QueryParam; import net.ximatai.muyun.ability.IDatabaseAbilityStd; +import net.ximatai.muyun.ability.IDesensitizationAbility; import net.ximatai.muyun.ability.IMetadataAbility; import net.ximatai.muyun.ability.IReferenceAbility; import net.ximatai.muyun.ability.ISecurityAbility; @@ -89,6 +90,9 @@ default String getSelectSql() { securityAbility.decrypt(row); securityAbility.checkSign(row); } + if (this instanceof IDesensitizationAbility desensitizationAbility) { + desensitizationAbility.desensitize(row); + } return row; } @@ -247,6 +251,10 @@ default PageResult view(Integer page, list.forEach(securityAbility::checkSign); } + if (this instanceof IDesensitizationAbility desensitizationAbility) { + list.forEach(desensitizationAbility::desensitize); + } + return new PageResult<>(list, total, size, page); } diff --git a/my-core/src/main/java/net/ximatai/muyun/core/desensitization/Desensitizer.java b/my-core/src/main/java/net/ximatai/muyun/core/desensitization/Desensitizer.java new file mode 100644 index 00000000..c572f5a1 --- /dev/null +++ b/my-core/src/main/java/net/ximatai/muyun/core/desensitization/Desensitizer.java @@ -0,0 +1,45 @@ +package net.ximatai.muyun.core.desensitization; + +import java.util.HashMap; +import java.util.Map; + +public class Desensitizer { + private final Map columnAlgorithmMap = new HashMap<>(); + + /** + * 注册列名及对应的脱敏算法 + * + * @param columnName 列名 + * @param algorithm 脱敏算法 + */ + public Desensitizer registerAlgorithm(String columnName, IDesensitizationAlgorithm algorithm) { + columnAlgorithmMap.put(columnName, algorithm); + return this; + } + + /** + * 根据列名获取对应的脱敏算法 + * + * @param columnName 列名 + * @return 对应的脱敏算法 + */ + public IDesensitizationAlgorithm getAlgorithm(String columnName) { + return columnAlgorithmMap.get(columnName); + } + + /** + * 根据列名和原始值进行脱敏 + * + * @param columnName 列名 + * @param source 原始数据 + * @return 脱敏后的数据 + */ + public String desensitize(String columnName, String source) { + IDesensitizationAlgorithm algorithm = getAlgorithm(columnName); + if (algorithm != null) { + return algorithm.desensitize(source); + } + return source; // 没有注册脱敏算法则返回原始数据 + } + +} diff --git a/my-core/src/main/java/net/ximatai/muyun/core/desensitization/IDesensitizationAlgorithm.java b/my-core/src/main/java/net/ximatai/muyun/core/desensitization/IDesensitizationAlgorithm.java new file mode 100644 index 00000000..e3c15b3e --- /dev/null +++ b/my-core/src/main/java/net/ximatai/muyun/core/desensitization/IDesensitizationAlgorithm.java @@ -0,0 +1,7 @@ +package net.ximatai.muyun.core.desensitization; + +public interface IDesensitizationAlgorithm { + + String desensitize(String source); + +} diff --git a/my-core/src/main/java/net/ximatai/muyun/core/desensitization/MaskMiddleAlgorithm.java b/my-core/src/main/java/net/ximatai/muyun/core/desensitization/MaskMiddleAlgorithm.java new file mode 100644 index 00000000..47aeca69 --- /dev/null +++ b/my-core/src/main/java/net/ximatai/muyun/core/desensitization/MaskMiddleAlgorithm.java @@ -0,0 +1,13 @@ +package net.ximatai.muyun.core.desensitization; + +public class MaskMiddleAlgorithm implements IDesensitizationAlgorithm { + @Override + public String desensitize(String source) { + if (source == null || source.length() <= 2) { + return source; // 短字符串不进行脱敏 + } + // 只显示首尾字符,中间使用 * 号代替 + int length = source.length(); + return source.charAt(0) + "*".repeat(length - 2) + source.charAt(length - 1); + } +}