⚡SimpleDAO 企业实战教程(08)脱敏 + 审计扩展 · 框架不设限

发布时间:2026/7/2 8:38:43
⚡SimpleDAO 企业实战教程(08)脱敏 + 审计扩展 · 框架不设限 相关开源地址核心框架源码https://gitee.com/gao_zhenzhong/simple-dao系统底座https://gitee.com/gao_zhenzhong/simple-dao-starter代码生成器https://gitee.com/gao_zhenzhong/simple-dao-coder实战案例本集源码https://gitee.com/gao_zhenzhong/simple-dao-demo前言脱敏这件事在绝大多数 Java 项目里都被做错了。最常见的做法是MyBatis 里写一个Masks注解 TypeHandlerJPA 里写一个ConvertAttributeConverter在数据从数据库查出来的那一刻就把手机号、身份证号「当场掩码」。看起来没问题问题很大。脱敏的本质是“展示行为”不是“持久行为”。它只在“数据要返回给谁看”的那一刻才有意义——同一个手机号管理员要看全号客服要看中间四位用户自己只能看前后三位。同一个实体在不同接口里脱敏规则完全不同。你把脱敏塞进TypeHandler等于把“展示逻辑”塞进了“持久层”。于是脱敏规则和实体绑死了换一个接口就要改实体脱敏规则和框架绑死了换框架就要重写脱敏规则和数据库绑死了换数据源就要重写而真正正确的位置只有一个Service 层数据离开数据库、准备返回给前端的那一刻。本集要证明的只有一件事脱敏与实体无关与框架无关与数据库无关只与调用行为有关。怎么做用 AOP 做用注解标记方法在返回数据上做“后处理”。这不是 SimpleDAO 提供的功能而是 SimpleDAO“不设限”的体现——你完全可以用它做任何你该做的事。 前置知识✅ 你只需要会❌ 你完全不需要会的Java 基础类、注解、泛型MyBatis/MP、复杂ORM插件基础 SQLSELECT、WHERE、JOINXML动态标签、OGNL表达式Spring Boot 基础配置Spring高级自动配置、复杂Maven配置了解 Spring AOP 基本概念TypeHandler / Converter 细节知道自定义注解长什么样注解处理器、反射底层原理 全套教程总览集数 · 标题时长核心内容01 · 单表 CRUD 审计 逻辑删除约 6 min零代码单表操作、自动填充审计、内置逻辑删除02 · 联表查询 分页约 5 min单/多表统一API无需ResultMap03 · 条件进阶IN 子查询约 5 min替代MyBatis foreach标签动态片段拼接04 · 多表联查 复杂条件约 5 min多表关联、区间筛选通用写法05 · 报表聚合GROUP BY 聚合函数约 6 min原生SQL报表无限制不受ORM束缚06 · mergeParams 多组条件合并约 6 min复杂报表拆分多条件解耦复用07 · 多租户 数据权限 · AOP 破局约 7 min不用MyBatis拦截器Spring原生AOP扩展08 · 脱敏 审计扩展 · 框架不设限约 7 min纠正MyBatis持久层脱敏错误分层思路 项目快速上手本案例内置 H2 内存库无需安装本地数据库克隆案例直接启动即可运行。完整项目层级结构demo08_desensitize/ ├── pom.xml └── src/main/ ├── java/example/ │ ├── DemoApplication.java // 启动类 │ ├── common/ │ │ ├── config/ │ │ │ └── CustomUserIdProvider.java // 自定义用户ID提供者 │ │ └── desensitize/ │ │ ├── Desensitize.java // 脱敏注解 │ │ └── DesensitizeAspect.java // 脱敏AOP切面 │ └── sys/user/ │ ├── User.java // 用户实体 │ ├── UserCond.java // 查询条件类 │ ├── UserDao.java // 数据访问层 │ └── UserService.java // 业务层含Desensitize └── resources/ ├── application.yml // 配置文件 ├── logback-spring.xml // 日志配置 └── schema.sql // 建表脚本1. Maven 依赖 pom.xml说明仅依赖 Spring JDBC SimpleDAO AOP无任何冗余插件。dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-aop/artifactId/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-jdbc/artifactId/dependencydependencygroupIdio.gitee.simpledao/groupIdartifactIdsimple-dao/artifactIdversion1.2.1/version/dependencydependencygroupIdcom.h2database/groupIdartifactIdh2/artifactId/dependencydependencygroupIdorg.projectlombok/groupIdartifactIdlombok/artifactId/dependency2. 配置文件 application.yml说明无框架专属复杂配置仅标准 Spring 数据源 一行逻辑删除字段配置。spring:datasource:url:jdbc:h2:mem:testdb;DB_CLOSE_DELAY-1driver-class-name:org.h2.Driverusername:sapassword:sql:init:schema-locations:classpath:schema.sqlmode:alwayssimple-dao:show-sql:truelogic-delete:field:is_deleted# 逻辑删除字段名默认 dr可任意替换3. 建表脚本 schema.sql说明逻辑删除字段名改为is_deleted演示配置覆盖能力。CREATETABLEIFNOTEXISTSsys_user(idBIGINTPRIMARYKEY,nameVARCHAR(50)NOTNULL,ageINTEGER,id_cardVARCHAR(30)UNIQUENOTNULL,phoneVARCHAR(20)UNIQUE,create_timeTIMESTAMP,create_byBIGINT,update_timeTIMESTAMP,update_byBIGINT,is_deletedTINYINTDEFAULT0);INSERTINTOsys_user(id,name,age,id_card,phone,create_time,create_by,is_deleted)VALUES(3681877765507776511,张三,25,110101199001011234,13812345678,CURRENT_TIMESTAMP,1000,0),(3681877765507776512,李四,30,110101199502021235,13987654321,CURRENT_TIMESTAMP,1000,0),(3681877765507776513,王五,28,110101199703031236,13611223344,CURRENT_TIMESTAMP,1000,0); 核心业务代码演示第一层实体 User.java说明与前面案例一致Table绑定表名Id标记主键。审计字段自动填充无额外配置。packageexample.sys.user;importcom.simple.common.base.annotation.Id;importcom.simple.common.base.annotation.Table;importlombok.AllArgsConstructor;importlombok.Builder;importlombok.Data;importlombok.NoArgsConstructor;importjava.time.LocalDateTime;DataBuilderAllArgsConstructorNoArgsConstructorTable(sys_user)publicclassUser{IdprivateLongid;privateStringname;privateIntegerage;privateStringidCard;privateStringphone;privateLocalDateTimecreateTime;privateLongcreateBy;privateLocalDateTimeupdateTime;privateLongupdateBy;privateIntegerisDeleted;// 逻辑删除字段名改为 is_deleted}第二层条件类 UserCond.java说明完全继承BaseConditionaddCondition()中定义查询条件。与前面案例完全相同一行未改。packageexample.sys.user;importcom.simple.common.base.BaseCondition;importlombok.AllArgsConstructor;importlombok.Builder;importlombok.Getter;importlombok.NoArgsConstructor;importlombok.Setter;SetterGetterBuilderAllArgsConstructorNoArgsConstructorpublicclassUserCondextendsBaseCondition{privateStringname;privateIntegerage;privateStringphone;privateStringidCard;privateIntegerisDeleted;OverrideprotectedvoidaddCondition(){add(AND name LIKE ?,name,3);add(AND age ?,age);add(AND phone LIKE ?,phone,3);add(AND id_card LIKE ?,idCard,3);add(AND is_deleted ?,isDeleted);}}第三层DAO 层 UserDao.java说明空类继承BaseDao自动拥有 50 CRUD 方法。本集演示中DAO 层零改动。packageexample.sys.user;importcom.simple.common.base.BaseDao;importorg.springframework.stereotype.Repository;RepositorypublicclassUserDaoextendsBaseDaoUser{// 空类拥有全部单表 CRUD 能力}第四层Service 层 UserService.java本集核心说明脱敏是展示层行为和框架无关和数据库无关和实体无关。本集在 Service 层用Desensitize注解 AOP 实现脱敏纠正 MyBatisTypeHandler的错误分层思路。packageexample.sys.user;importexample.common.desensitize.Desensitize;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.stereotype.Service;importjava.util.List;ServicepublicclassUserService{AutowiredprivateUserDaouserDao;/** * 方法级脱敏对返回列表中的 phone 和 idCard 字段进行脱敏 * 与框架无关与数据库无关与实体无关——只和“展示行为”有关 */Desensitize(types{phone,idCard},fields{phone,idCard})publicListUserlist(UserCondcond){returnuserDao.list(cond);}publicUsersave(Useruser){returnuserDao.save(user);}publicUserfindById(Longid){returnuserDao.findById(id);}}第五层脱敏注解 Desensitize说明方法级注解声明哪些字段需要脱敏以及对应的脱敏类型。packageexample.common.desensitize;importjava.lang.annotation.ElementType;importjava.lang.annotation.Retention;importjava.lang.annotation.RetentionPolicy;importjava.lang.annotation.Target;Target(ElementType.METHOD)Retention(RetentionPolicy.RUNTIME)publicinterfaceDesensitize{String[]types();// 脱敏类型phone / idCardString[]fields();// 对应的字段名}第六层脱敏切面 DesensitizeAspect说明AOP 在 Service 方法返回后对List中的每个对象按注解配置做字段替换。脱敏逻辑在展示层完成不污染 DAO 层。packageexample.common.desensitize;importorg.apache.commons.lang3.reflect.FieldUtils;importorg.aspectj.lang.ProceedingJoinPoint;importorg.aspectj.lang.annotation.Around;importorg.aspectj.lang.annotation.Aspect;importorg.springframework.stereotype.Component;importjava.lang.reflect.Field;importjava.util.List;AspectComponentpublicclassDesensitizeAspect{Around(execution(public * example.sys..*.*Service.*(..)) annotation(anno))publicObjectaround(ProceedingJoinPointjoinPoint,Desensitizeanno)throwsThrowable{ObjectresultjoinPoint.proceed();if(resultinstanceofList?list){for(Objectitem:list){for(inti0;ianno.fields().length;i){FieldfieldFieldUtils.getField(item.getClass(),anno.fields()[i],true);if(field!null){maskField(item,field,anno.types()[i]);}}}}returnresult;}privatevoidmaskField(Objectitem,Fieldfield,Stringtype)throwsIllegalAccessException{Stringoriginal(String)FieldUtils.readField(field,item,true);if(originalnull)return;Stringmaskedswitch(type){casephone-original.substring(0,3)****original.substring(7);caseidCard-original.substring(0,6)********original.substring(14);default-original;};FieldUtils.writeField(field,item,masked,true);}}第七层自定义 UserIdProvider审计字段扩展说明SimpleDAO 默认UserIdProvider返回1000L开发环境会打印警告。本集演示如何通过实现接口让createBy/updateBy从业务上下文获取真实用户 ID。packageexample.common.config;importcom.simple.common.base.UserIdProvider;importorg.springframework.stereotype.Component;ComponentpublicclassCustomUserIdProviderimplementsUserIdProvider{OverridepublicLonguserId(){// 实际项目可从 Session / Shiro / SpringSecurity / Token 中获取// 示例SessionUtils.getUserId()// 示例ShiroUtils.getUserId()// 示例SecurityContextHolder.getContext().getAuthentication()return9999L;}} 运行日志效果完整可执行 SQL说明SimpleDAO 打印带真实参数的完整 SQL脱敏前后数据对比清晰可见。# 1. 插入审计字段自动填充 createBy9999来自 CustomUserIdProvider [INFO] INSERT INTO sys_user (id,name,age,id_card,phone,create_time,create_by,is_deleted) VALUES (3708628123456789000,张三,25,110101199001011234,13800138000,2026-06-30 12:34:57,9999,0) # 2. 主键查询完整数据含敏感字段 [INFO] SELECT t.id,t.name,t.age,t.id_card,t.phone,t.create_time,t.create_by,t.is_deleted FROM sys_user t WHERE t.id3708628123456789000 [INFO] 查询结果User(id3708628123456789000, name张三, age25, idCard110101199001011234, phone13800138000, createBy9999, isDeleted0) # 3. 脱敏列表查询phone 和 idCard 自动脱敏 [INFO] SELECT t.id,t.name,t.age,t.id_card,t.phone,t.create_time,t.create_by,t.is_deleted FROM sys_user t [INFO] 脱敏结果User(id..., name张三, idCard110101********1234, phone138****8000) [INFO] 脱敏结果User(id..., name李四, idCard110101********1235, phone139****4321) [INFO] 脱敏结果User(id..., name王五, idCard110101********1236, phone136****3344) # 4. 逻辑删除is_deleted 被置为 1配置覆盖生效 [INFO] UPDATE sys_user t SET is_deleted 1 WHERE t.id IN (3708628123456789000) 本集核心总结脱敏与实体无关实体只负责数据存取脱敏是展示层行为不该写在实体里或TypeHandler中。脱敏与框架无关无论是 MyBatis、JPA 还是 SimpleDAO脱敏的正确做法都是在 Service 层用 AOP 做。SimpleDAO 不提供脱敏功能它只提供“不设限”的扩展能力。脱敏与数据库无关脱敏逻辑在数据离开数据库、准备返回给前端时执行和底层数据源MySQL/Oracle/MongoDB/CSV无关。审计字段自定义通过实现UserIdProvider接口让createBy/updateBy从业务上下文获取真实用户 ID一行配置覆盖默认行为。逻辑删除字段名可配置simple-dao.logic-delete.field: is_deleted一行配置即可覆盖默认的dr字段适配不同项目规范。分层规范脱敏、字典翻译等业务逻辑放在 Service 层不侵入 DAO 层纠正 MyBatis 写TypeHandler的错误分层方式。全程零 XMLSQL 完整可见所有 SQL 日志打印完整带参语句复制即跑DBA 可直接优化。系列八集至此完结。感谢陪伴源码见置顶。