ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

数据脱敏实战:注解+策略+执行器实现敏感字段保护

数据脱敏实战:注解+策略+执行器实现敏感字段保护 最近我们测试环境出了件奇怪的事订单表里所有人的手机号都变成了138****5678。一开始还以为是数据同步出了问题查了半天才发现是上一个接手的人写了个粗暴的脱敏工具把整列数据全替换成了同一个固定值。这下报表没法看联调也没法做用户下单记录全都对不上号了。这件事让我下决心把这个事做对。数据脱敏看起来是个不起眼的小需求但真要落地涉及的问题一点都不少哪些字段要脱、脱成什么样、能不能还原、同一个身份证在不同表里会不会脱出两个不同的值、脱敏逻辑会不会拖慢接口响应、日志里会不会又把真实手机号打出来……每一条都是坑。这篇文章我就从头到尾拆一遍我们在生产环境里落地的自定义脱敏方案从需求梳理到架构设计再到每个核心代码块的实现思路以及踩过的坑和优化方案。适合正在做数据安全相关改造、准备在项目里引入自定义脱敏规则的朋友参考。1. 需求梳理脱敏范围、脱敏规则和业务约束1.1 先回答三个问题脱什么、脱成什么、脱完还能不能用任何脱敏方案都绕不开这三个问题。我在做第一版设计时就是先和业务、运维、测试三方对齐这些边界避免后续反复改。脱什么敏感数据清单通常分布在四类地方——用户信息表姓名、手机号、身份证、银行卡、订单表收货人、联系电话、收货地址、日志请求参数里的手机号、邮箱、导出文件报表、对账单。我们把这些字段统一盘点列进一个敏感字段清单后续所有脱敏逻辑都以这份清单为准。脱成什么同一个字段在不同场景下要求不一样。比如手机号在测试环境需要保留格式方便测试人员判断归属地在日志里只需要中间四位打码在对外展示的页面上则可能保留前3后4。所以脱敏不是一个固定算法而是一组策略的集合。脱完还能不能用这是最容易忽略的点。测试环境需要脱敏后的数据仍然具备业务可操作性——脱敏后的手机号要能被测试工具调用、验证码要能发到某一台测试机上、身份证要满足校验规则。还有一些场景需要还原真实数据比如客服受理投诉时查看订单这就必须采用可逆加密方案而不是不可逆的替换或哈希。1.2 动态脱敏和静态脱敏两类方案的选择逻辑脱敏方案按实施时机可以分为两类我把它俩分开说因为很多人混在一起做后面维护起来特别累。静态脱敏对数据库中的存量数据进行一次性处理适用于生产数据导出到测试库、数据仓库做离线分析等场景。特点是可以做深度处理比如关联外键需要保持一致、批量清洗可以跑很久。我们用的是脱敏工具 定时任务的方式把生产库的备份导到测试库之后触发一次脱敏脚本把敏感字段全部处理掉。动态脱敏在数据被读取、展示、打印的那一刻实时处理适用于接口返回、日志输出、页面展示等场景。特点是响应时间敏感不能引入太多性能开销。现在大多数互联网公司都推荐优先做动态脱敏因为应用层不需要维护两份数据真实数据始终留在库里。我做的方案里这两类并存静态脱敏解决测试环境数据可用性问题动态脱敏解决线上展示和日志泄露问题。1.3 格式保留、幂等和一致性三个容易忽略的硬约束除了上面说的能不能用还有三个硬约束必须在需求阶段定清楚否则代码写到一半指定返工。格式保留手机号脱敏成138****5678是没有问题的但如果你直接把手机号替换成随机11位数字测试环境里的短信验证码就没办法路由了。身份证、银行卡、邮箱同理。所以大部分场景要求长度不变、前缀后缀保留、字符类型一致。幂等性同一个字段值经过 N 次脱敏结果必须一致。第一版我用的替换算法是用随机生成的假数据去替换真实值结果任务重跑了两次同一批数据出现两套不同结果下游核对直接崩了。后来全部换成确定性算法——同样的输入永远得到同样的输出。一致性同一个身份证号出现在 user 表、order 表、log 表三张表里脱敏后必须映射成同一个假值否则跨表关联数据分析就废了。这意味着脱敏算法不能依赖随机数要基于原始值做确定性转换。哈希算法天然满足但哈希结果一般不能保持位数和格式所以后来我们选用了固定盐的格式化哈希也就是对原始值做sha256之后截断取片段再映射回对应的格式位。这三个约束看起来简单实际上任何一条没满足脱敏方案上线后都会变成运维的噩梦。2. 架构设计注解描述规则、枚举定义类型、策略实现算法2.1 为什么用注解而不是写死判断梳理完需求之后就到了做架构选型的阶段。我能想到的最直观做法是写一个脱敏工具类里面放一堆if (fieldName.equals(mobile))判断但这种写法的可维护性太差了——字段一多、规则一变、嵌套对象一出现代码就成了一团浆糊。我最终选用的方案是注解驱动 策略模式用注解声明哪个字段是敏感的、用哪种策略脱敏用枚举定义脱敏类型用一组策略类实现具体算法最后用一个执行器统一扫描对象并完成处理。这样做的好处有三点字段声明和脱敏逻辑解耦实体类上只声明规则改规则不碰实体代码。新增脱敏类型很轻松加一个枚举值、加一个策略类不需要动执行器。支持多层嵌套执行器递归扫描遇到对象里的对象、对象里的集合、集合里的对象都能覆盖。2.2 脱敏算法的分类与选择我整理了实际用到过的脱敏算法大概分成以下五类它们在底层实现方式上完全不同算法类型实现方式是否可逆典型场景掩码保留部分字符其余替换为*否页面展示、日志打印替换用字典中的假数据替换原值否测试数据生成哈希对原值加盐后做sha256再格式化否跨表一致性要求高的场景加密使用AES/DES加密后转码是客服调单、仲裁取证泛化将精确值变成范围值如年龄变年龄段否数据分析、画像选择原则其实很朴素展示类场景用掩码测试数据用替换跨表关联用哈希可逆需求用加密统计场景用泛化。不要试图用一个算法包打天下那必然会顾此失彼。2.3 规则配置的三种形态编码、配置文件和数据库规则的定义粒度也是需要提前想清楚的。我见过三种做法各有适用场景注解里硬编码直接在字段上指定SensitiveField(type SensitiveType.MOBILE)。最简单直观适合字段和规则绑定很死的场景。缺点是一旦规则改了要重新发版对字段多的老系统不太友好。配置文件集中管理把全限定类名.字段名 脱敏类型写到 yml 文件里用一个ConfigurationProperties类读取。好处是运维和测试可以直接改配置文件不需要懂代码。适合字段相对稳定但规则经常微调的场景。数据库动态配置把规则表建在数据库里支持运行时热加载。灵活度最高但是引入了额外的查询开销和缓存一致性复杂度一般只有在规则高度动态的平台上才值得这么做。我推荐中小项目直接用注解已经有一定规模的项目用配置文件。我们最终选的是注解 配置文件兜底的混合模式默认规则写在注解里需要临时调整的字段在配置文件里覆盖两边合并时配置文件优先级更高。3. 核心代码实现从上往下捋一遍脱敏执行的完整链路3.1 注解和脱敏类型枚举先看最基础的注解定义。这里我额外加了一个group属性用来区分同一字段在展示场景和测试环境下的不同脱敏行为实际使用中很有用Target({ElementType.FIELD, ElementType.TYPE}) Retention(RetentionPolicy.RUNTIME) Documented public interface SensitiveField { // 脱敏类型见枚举 SensitiveType type() default SensitiveType.MASK_MOBILE; // 脱敏场景分组通过分组可以控制同一字段在不同场景下的策略 String group() default default; // 自定义掩码使用的占位符 char placeholder() default *; }脱敏类型枚举我拆得比较细因为不同的字段格式需要不同的保留位置public enum SensitiveType { // 掩码类 MASK_MOBILE, // 138****5678 MASK_ID_CARD, // 110101********1234 MASK_NAME, // 张* MASK_BANK_CARD, // 6222**********5678 MASK_EMAIL, // a****qq.com MASK_ADDRESS, // 北京市朝阳区*** // 哈希类 HASH_SHA256, // 确定性哈希保持格式 // 加密类 ENCRYPT_AES, // AES对称加密可逆 // 替换类 REPLACE_FIXED, // 整体替换为固定值 // 泛化类 GENERALIZE, // 精确值转范围值 }3.2 脱敏策略处理器每个枚举值对应一个具体的策略实现统一实现一个接口。这里用掩码和确定性哈希两个例子展示核心逻辑public interface DesensitizeStrategy { String handle(String origin); } // 手机号掩码保留前3后4 public class MaskMobileStrategy implements DesensitizeStrategy { Override public String handle(String origin) { if (origin null || origin.length() 7) { return origin; } return origin.substring(0, 3) **** origin.substring(origin.length() - 4); } } // 银行卡掩码保留前6后4 public class MaskBankCardStrategy implements DesensitizeStrategy { Override public String handle(String origin) { if (origin null || origin.length() 10) { return origin; } return origin.substring(0, 6) ********** origin.substring(origin.length() - 4); } } // 确定性哈希固定盐 sha256 按原格式截断 // 同一个身份证号进来永远得到同一个假号且位数一致 public class HashSha256Strategy implements DesensitizeStrategy { private static final String SALT a-fixed-salt-value; Override public String handle(String origin) { if (origin null) { return null; } String hash sha256Hex(origin SALT); // 把哈希转成数字序列再映射成和原值长度一致的字符串 char[] result new char[origin.length()]; for (int i 0; i origin.length(); i) { int hexPair Integer.parseInt(hash.substring((i * 2) % 60, (i * 2) % 60 2), 16); result[i] (char) (0 (hexPair % 10)); } return new String(result); } }关于这个哈希策略我要多说一句它的核心只有三个点——固定盐保证确定性、取哈希的同一段位保证稳定、数字映射保证位数和原值一致。很多人在实现时忘了固定盐结果换个进程跑出来结果就变了还有人直接把整个哈希字符串截断导致长度和原值不一致。这两个细节都是我在实际测试中吃了亏才改对的。3.3 反射执行器递归处理嵌套对象、集合、Map有了注解和策略真正的核心是执行器。执行器要做的事很简单遍历对象的字段如果字段上有SensitiveField注解就调用对应策略处理如果字段是对象、集合、Map 就递归进去继续扫描。这套逻辑我写成了独立的DesensitizeExecutorpublic class DesensitizeExecutor { private static final MapString, DesensitizeStrategy STRATEGY_MAP new ConcurrentHashMap(); static { STRATEGY_MAP.put(SensitiveType.MASK_MOBILE.name(), new MaskMobileStrategy()); STRATEGY_MAP.put(SensitiveType.MASK_BANK_CARD.name(), new MaskBankCardStrategy()); STRATEGY_MAP.put(SensitiveType.HASH_SHA256.name(), new HashSha256Strategy()); // ... 其他策略注册 } public static void process(Object target) { if (target null) { return; } // 缓存处理过的Class避免反复反射 Class? clazz target.getClass(); if (isPrimitiveOrWrap(clazz) || clazz.getName().startsWith(java.lang)) { return; } Field[] fields clazz.getDeclaredFields(); for (Field field : fields) { try { field.setAccessible(true); Object value field.get(target); if (value null) { continue; } SensitiveField sensitive field.getAnnotation(SensitiveField.class); // 被注解标记的字段执行脱敏策略 if (sensitive ! null value instanceof String) { DesensitizeStrategy strategy STRATEGY_MAP.get(sensitive.type().name()); if (strategy ! null) { field.set(target, strategy.handle((String) value)); continue; } } // 无注解的字段继续递归判断 if (value instanceof Collection?) { for (Object item : (Collection?) value) { process(item); } } else if (value instanceof Map?, ?) { for (Object item : ((Map?, ?) value).values()) { process(item); } } else if (value.getClass().isArray()) { for (Object item : (Object[]) value) { process(item); } } else if (!isPrimitiveOrWrap(value.getClass()) !value.getClass().getName().startsWith(java.lang) !value.getClass().getName().startsWith(java.time)) { process(value); } } catch (IllegalAccessException e) { // 记录日志并跳过 } } } private static boolean isPrimitiveOrWrap(Class? clazz) { return clazz.isPrimitive() || Number.class.isAssignableFrom(clazz) || Boolean.class.isAssignableFrom(clazz) || Character.class.isAssignableFrom(clazz); } }这里的关键逻辑在于先判断注解再决定要不要递归。如果一个字段是对象但不是敏感字段就直接递归进去扫描它的内部字段。集合和Map的处理也一样——很多人的脱敏工具一遇到ListUser就漏了就是因为没有做集合内元素的递归。3.4 如何使用这个工具类工具类的使用场景非常直观。比如查询用户详情的时候Controller 层拿到完整的用户对象在返回给前端之前调用一次DesensitizeExecutor.process(user)所有带注解的字段就被处理掉了RestController public class UserController { GetMapping(/user/{id}) public ResultUserVO getUser(PathVariable Long id) { UserVO user userService.getById(id); // 在返回前统一执行脱敏 DesensitizeExecutor.process(user); return Result.ok(user); } }UserVO的字段定义长这样public class UserVO { private Long id; private String name; SensitiveField(type SensitiveType.MASK_MOBILE) private String mobile; SensitiveField(type SensitiveType.MASK_ID_CARD) private String idCard; SensitiveField(type SensitiveType.MASK_EMAIL) private String email; private ListOrderVO orders; }看到没有orders本身没加注解但是执行器会自动递归进去处理OrderVO里的收货人手机号。这样写的好处是业务代码不需要知道脱敏的存在加字段、加注解都在实体类上完成改动范围很小。4. 与Spring Boot、MyBatis、日志框架的集成方案4.1 Jackson自定义序列化器出口处无感脱敏手动调用DesensitizeExecutor.process()有一个问题容易漏。尤其是接口多、团队大的情况下很难保证每个返回入口都记得调用。更好的方式是在序列化层做统一拦截——Spring Boot 默认使用 Jackson 把对象转成 JSON只要在ObjectMapper上挂一个自定义序列化器所有接口返回的时候自动执行脱敏。我当时的实现方式是通过JsonSerialize注解绑定自定义序列化器public class SensitiveFieldJsonSerializer extends JsonSerializerString { Override public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException { // 通过上下文拿到被序列化字段的注解 BeanProperty property serializers.getConfig().getAnnotationIntrospector() .findPropertyAnnotation(gen.getOutputContext().getCurrentValue().getClass(), currentFieldName(gen), SensitiveField.class); if (property null || value null) { gen.writeString(value); return; } SensitiveType type property.getAnnotation(SensitiveField.class).type(); DesensitizeStrategy strategy STRATEGY_MAP.get(type.name()); gen.writeString(strategy.handle(value)); } }然后在需要脱敏的字段上加两个注解JsonSerialize(using SensitiveFieldJsonSerializer.class) SensitiveField(type SensitiveType.MASK_MOBILE) private String mobile;这个方案的关键优势是完全侵入业务代码不需要在每个 Controller 里额外调用工具类。只要返回的对象里有这个注解Jackson 序列化成 JSON 的那一刻就自动完成了脱敏。序列化失败导致的接口报错也会被框架的异常处理捕获不会拖垮整个接口。有个细节必须提醒Jackson 序列化器里拿到的对象可能本身已经是脱敏后的值如果之前手动调用过process()这里就会脱第二次。所以我在项目里的约定是二选一——要么全部走手动调用要么全部走 Jackson 序列化不要混着用。4.2 logback日志脱敏MessageConverter的实现思路日志泄露是个隐蔽问题。很多人接口返回做了脱敏但访问日志、异常日志里打印的参数快照仍然是明文。比如 logback 配置里打印了请求参数%d{yyyy-MM-dd HH:mm:ss} [%thread] %logger - %msg%n如果请求体里带了手机号日志里就是明文。解决思路是自定义 logback 的MessageConverter在日志输出前对所有消息做一次正则脱敏public class SensitiveLogConverter extends MessageConverter { private static final Pattern MOBILE_PATTERN Pattern.compile((1[3-9]\\d)\\d{4}(\\d{4})); private static final Pattern ID_CARD_PATTERN Pattern.compile((\\d{6})\\d{8}(\\d{3}[0-9Xx])); Override public String convert(ILoggingEvent event) { String message event.getFormattedMessage(); if (message null) { return super.convert(event); } message MOBILE_PATTERN.matcher(message).replaceAll($1****$2); message ID_CARD_PATTERN.matcher(message).replaceAll($1********$2); return message; } }logback.xml 里只需要把转换器注册进去conversionRule conversionWordmsg converterClasscom.example.log.SensitiveLogConverter /这里我实际踩过一个坑正则如果写得不够严谨会把正常文本里不是手机号的11位数字也脱敏掉影响排查日志。后来调整成了1[3-9]\d开头然后再配合参数位置判断才把误报率降下来。注意一点日志脱敏只防日志落盘这一层。如果应用把日志直接传到 ElasticSearch 之类的第三方平台还是要保证传输链路安全不然脱敏等于白做。4.3 MyBatis TypeHandler参数和结果集的脱敏控制还有一种需求是写入时脱敏、读取时还原。比如有些合规场景要求数据库里不能存明文手机号但业务上又需要能查出来打客服电话。这种场景就需要 MyBatis 的TypeHandler在参数和结果集之间做转换。一个简单的实现思路是定义一个SensitiveTypeHandlersetParameter的时候加密getResult的时候解密public class SensitiveTypeHandler extends BaseTypeHandlerString { private final EncryptService encryptService new EncryptService(); Override public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException { ps.setString(i, encryptService.encrypt(parameter)); } Override public String getNullableResult(ResultSet rs, String columnName) throws SQLException { return encryptService.decrypt(rs.getString(columnName)); } // getNullableResult(ResultSet rs, int columnIndex) 和 getNullableResult(CallableStatement cs, int columnIndex) // 同理调用 decrypt }在 mapper 的 resultMap 里指定对应字段用这个 TypeHandler或者直接在实体类字段上加TableField(typeHandler SensitiveTypeHandler.class)如果是 MyBatis-Plus。这个方案的好处是数据库层面就不存在明文数据文件被拖走也不怕。但要注意加密字段没法走普通索引查询——你用WHERE mobile 138...查的时候数据库里存的是密文SQL 匹配不上。解决的思路一般是把加密后的值单独存一个等值检索字段比如加密后再做一次确定性哈希或者调整为只在写入时脱敏、读取时还原不加密的方案。这个问题在选型阶段一定要明确不然上线后会发现所有按手机号查用户的接口都失灵了。4.4 集成时的坑重复脱敏和脱敏时机集成层变多之后最头疼的问题就是重复脱敏。我遇到过这种情况日志管道里脱了一次Jackson 序列化里又脱了一次手动调用process()的地方还脱一次——三重脱敏之后手机号从138****5678变成了1****5678直接破坏了展示效果。我的建议是建立一个脱敏流程图明确每一层管什么层级管什么实现方式存储层写入时加密/脱敏读取时还原MyBatis TypeHandler服务层静态脱敏、批量任务工具类手动调用日志层去除日志中的敏感明文logback MessageConverter输出层接口返回脱敏Jackson 自定义序列化器在这个图里输出层和日志层是互不干扰的因为日志走的是 logback、接口走的是 Jackson两条链路天然隔离。真正容易重复的是服务层手动调用 输出层 Jackson所以要么只在服务层调用、要么只在输出层做二选一。5. 落地中的踩坑记录与性能优化5.1 坑一脱敏后的数据替换了真实数据测试环境无法还原第一次上线静态脱敏时我直接写了个 SQL 脚本把测试库里的手机号全部update成了138****5678这种样式。结果测试跑了两天发现所有用户收到的验证码都指向同一个测试手机号根本没法验证不同用户之间的数据隔离。后来改成了脱敏同时保留一份映射表把真实值和脱敏值的关系记录下来需要还原的时候查映射表就行。这里我建议的方案是静态脱敏永远不要原地覆盖先备份、写映射表、再替换同时要留任务日志。映射表本身是敏感数据要做权限控制不能随便查。5.2 坑二反射频繁调用导致接口RT明显上升反射是脱敏方案逃不过的性能点。第一版工具类每处理一个对象都重新getDeclaredFields()一次高峰期接口平均响应时间涨了将近40毫秒压测直接红灯。后来做了两层优化一是缓存 Class 元数据。把每个 Class 的字段列表、哪些字段带注解缓存到本地ConcurrentHashMap字段增删改的时候通过启动时扫描来刷新缓存不让反射在请求链路上反复做元数据读取。二是减少非必要的对象创建。原先每次脱敏都新建策略对象后面改成了单例策略注册进一个 Map执行器只做查找和调用不创建新对象。这两步做完接口 RT 增量从40毫秒降到了5毫秒以内基本可以忽略不计。5.3 坑三一致性hash没有处理好同一个身份证出来两个值我第一次用哈希策略时直接在sha256Hex(origin SALT)之后对整个哈希字符串做了截断结果出现了两个问题一是同一个身份证因为换了一个 salt 值就变了二是有极端情况哈希截断后映射出来的字符串带有非数字字符导致身份证校验失败。后来统一改成固定盐 截取固定片段 纯数字映射并补了一轮针对边界字符全0、全空格、长度不一致的单元测试才算稳定下来。这里也引申出一个经验脱敏算法必须配套单元测试并且测试数据要覆盖边界值比如手机号长度不足7位、身份证有字母X、银行卡中间有空格的情况。算法在正常数据上好用在异常数据上崩掉才是上线后才被发现的坑这种坑定位起来极为痛苦。5.4 优化实践经验元数据缓存、并行处理和脱敏开关除了上面那条反射优化还有两个优化手段值得提一下。批量任务并行化静态脱敏跑全表的时候如果是几千万行的表单线程跑要几个小时。我们后来用线程池按主键区间分片每片一个独立事务跑完再汇总统计。分片大小要根据数据量和表结构调整不能盲目加大线程数否则数据库连接池先被打满。脱敏开关动态脱敏方案上线初期业务方可能会有各种各样的顾虑。我加了一个全局开关通过配置中心控制desensitize.enabled这个布尔值。切流量的时候先打开开关观察日志没问题再全量放开要是出了问题一键关闭马上恢复明文输出。这个开关看着简单但在大促前的灰度阶段非常有用。性能上还有一个细节值得注意日志脱敏的正则也是性能消耗点。在高并发场景下每行日志都跑一遍Pattern.matcher还是有一定开销。后来我把日志脱敏改成了只在 debug 级别和部分关键词命中时才做正则检查。这个优化看起来很鸡贼但确实把压测数据拉回了正常水位。6. 方案扩展把脱敏做成一个可配置化的平台能力6.1 配置化改造把规则从代码中抽离做完整套方案后我发现一个更实际的问题脱敏规则的主语往往是业务字段而业务字段的名称、所属模块、是否启用脱敏经常在项目运行中发生变化。如果每次都靠改实体类注解来调整规则效率太低而且不利于安全审计。所以我做了一次配置化改造在数据库里建一张sensitive_rule表记录实体类全限定名、字段名、场景、脱敏策略、启停标记。系统启动时加载到本地缓存执行器处理时优先读取配置表中的规则再回退到注解默认值。这样一来运营和安全团队不需要改代码就能在后台把某个字段的脱敏策略从掩码调整成加密或不脱敏。这张表本身也需要做权限控制我建议只允许指定角色修改并且每一次变更都留审计日志。规则一旦配错了比没有脱敏还要麻烦因为它给了你一个已经处理过了的错觉。6.2 与其他安全组件的配合权限、水印和审计脱敏不是孤立的。我们落地时还做了三块配合权限分级同样是用户详情接口客服角色看到的应该是完整手机号用于联系客户普通运营角色看到的是脱敏后的号码。这个通过规则配置里的角色维度来实现——不同角色加载不同的脱敏策略组。数据水印导出报表的时候在文件里嵌入不可见水印比如行的行距、列的顺序里加入隐藏信息这样即使数据被转发出去也能追踪到下载人。这个属于防泄露的辅助手段成本不高收益还不错。脱敏审计所有脱敏动作都要记录谁在什么时间、通过哪个接口、对哪个字段执行了脱敏。尤其对跳过脱敏的权限也就是谁有权限查看明文必须审计得清清楚楚。这块在合规审计的时候是重点检查对象没有日志等于没做。6.3 从脱敏工具到数据安全网关的演进路径如果你所在团队的数据安全需求越来越复杂可以考虑把脱敏从业务代码里的工具类升级成一个独立的数据安全网关。网关统一拦截请求在协议解析之后做脱敏、加密、鉴权、审计业务服务根本感知不到脱敏的存在。这样有几个好处业务团队零改造上线、安全策略集中管理、多语言场景下不用每个语言都实现一套脱敏逻辑。网关方案也有代价链路多了一层转发会增加网络耗时而且要求团队有较强的中间件开发能力。对大多数项目来说先把本文提到的注解 策略 执行器这套方案落地再逐步考虑网关化是比较稳妥的路径。我在实际推进这个项目时还有一个体会数据脱敏做得再完美最后还是会有漏网之鱼。比如异常堆栈里打出数据库连接串、第三方回调请求里带了明文参数这些都不是一套脱敏框架能覆盖的。真正抗泄密的是全员对数据安全的意识 制度 技术手段三层叠加。所以我今天分享的这套方案可以帮你解决80%的常见敏感字段问题剩下20%需要靠流程和习惯来补齐。特别是新增接口时先检查返回结构里有没有敏感字段这个动作每次代码评审都应该有人专门盯一遍比事后发现再补脱敏要省事得多。
返回列表