--- title: "03-敏感数据保护:展示脱敏与加密存储深度解析" created: 2026-01-09 tags: - 博客 --- # 敏感数据保护:展示脱敏与加密存储深度解析 ## **核心理念** > ***敏感数据保护是两道防线:存储时加密防止数据泄露,展示时脱敏防止信息暴露。*** 这不仅是技术问题,更是合规要求。在金融、医疗、电商等领域,数据保护是法律红线。 ## **整体架构一览** ![[7-Blog/后端与微服务/assets/敏感数据保护双重防线-2b73dd0e.jpg]] ## **第一部分:展示脱敏** ### **Step 1:业务理解** ![[7-Blog/后端与微服务/assets/展示脱敏的业务场景-029e8481.jpg]] ### **Step 2:方案评估** ![[7-Blog/后端与微服务/assets/image-548d39f7.png]] #### 1. 基于自定义注解 + Jackson 序列化器 利用 Jackson 的 `ContextualSerializer` 接口,在 JSON 序列化时动态读取字段上的注解,应用脱敏逻辑。 - **实现原理**:自定义 `@Sensitive` 注解 + 自定义 `JsonSerializer`。 ![[7-Blog/后端与微服务/assets/Jackson_注解_+_序列化器-056e3fdf.jpg]] - **优点**: - **性能较好**:作为序列化过程的一部分,无额外反射开销。 - **支持嵌套**:Jackson 天然支持 List、Map 和复杂对象的递归。 - **无侵入**:业务代码无感知,仅需加注解。 - **缺点**: - **强绑定 JSON 库**:如果项目从 Jackson 切换到 Gson 或 FastJson,需要重写序列化器。 ##### **完整代码实现** ###### **定义脱敏类型枚举** ```java /** * 脱敏类型枚举 * 定义各种敏感数据的脱敏规则 */ public enum SensitiveType { /** * 手机号: 138****5678 */ MOBILE_PHONE { @Override public String desensitize(String value) { if (value == null || value.length() < 11) return value; return value.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2"); } }, /** * 身份证号: 110***********0312 */ ID_CARD { @Override public String desensitize(String value) { if (value == null || value.length() < 15) return value; return value.replaceAll("(\\d{3})\\d+(\\d{4})", "$1***********$2"); } }, /** * 中文姓名: 张** */ CHINESE_NAME { @Override public String desensitize(String value) { if (value == null || value.isEmpty()) return value; if (value.length() == 2) { return value.charAt(0) + "*"; } return value.charAt(0) + "*".repeat(value.length() - 1); } }, /** * 邮箱: t***@example.com */ EMAIL { @Override public String desensitize(String value) { if (value == null || !value.contains("@")) return value; int atIndex = value.indexOf("@"); if (atIndex <= 1) return value; return value.charAt(0) + "***" + value.substring(atIndex); } }, /** * 银行卡号: 6222 **** **** 1234 */ BANK_CARD { @Override public String desensitize(String value) { if (value == null || value.length() < 12) return value; return value.substring(0, 4) + " **** **** " + value.substring(value.length() - 4); } }, /** * 地址: 北京市朝阳区**** */ ADDRESS { @Override public String desensitize(String value) { if (value == null || value.length() < 8) return value; return value.substring(0, 6) + "****"; } }, /** * 固定电话: ****-1234 */ FIXED_PHONE { @Override public String desensitize(String value) { if (value == null || value.length() < 4) return value; return "****" + value.substring(value.length() - 4); } }, /** * 密码: ****** */ PASSWORD { @Override public String desensitize(String value) { return "******"; } }, /** * 自定义 - 使用注解参数 */ CUSTOM { @Override public String desensitize(String value) { // 自定义类型需要额外参数,由序列化器处理 return value; } }; /** * 脱敏处理抽象方法 */ public abstract String desensitize(String value); } ``` ###### **定义 @Sensitive 注解** ```java import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import java.lang.annotation.*; /** * 数据脱敏注解 * 用于标记需要脱敏的字段 * * 使用示例: * @Sensitive(type = SensitiveType.MOBILE_PHONE) * private String phone; */ @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @Documented @JacksonAnnotationsInside // 表示这是一个Jackson组合注解 @JsonSerialize(using = SensitiveSerializer.class) // 指定序列化器 public @interface Sensitive { /** * 脱敏类型 */ SensitiveType type(); /** * 自定义脱敏时-前缀保留长度 */ int prefixLen() default 0; /** * 自定义脱敏时-后缀保留长度 */ int suffixLen() default 0; /** * 替换符号 */ String maskChar() default "*"; } ``` ###### 实现 Jackson 序列化器 ```java import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.ser.ContextualSerializer; import java.io.IOException; /** * 敏感数据序列化器 * 核心类 - 实现 ContextualSerializer 以获取字段上的注解信息 */ public class SensitiveSerializer extends JsonSerializer implements ContextualSerializer { private SensitiveType sensitiveType; private int prefixLen; private int suffixLen; private String maskChar; // 1. 必须保留无参构造,供 Jackson 反射初始化 public SensitiveSerializer() {} // 2. 内部构造,用于创建带有具体策略的新实例 public SensitiveSerializer(SensitiveType type, int prefixLen, int suffixLen, String maskChar) { this.sensitiveType = type; this.prefixLen = prefixLen; this.suffixLen = suffixLen; this.maskChar = maskChar; } /** * 创建上下文序列化器 * 每个字段都会调用此方法,用于读取该字段的注解配置 * 这个方法只会在初始化时调用一次,不会影响每次序列化的性能 */ @Override public JsonSerializer createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException { if (property == null) { return prov.findNullValueSerializer(property); } // 如果注解存在,且字段类型是 String if (property.getType().getRawClass() != String.class) { return prov.findValueSerializer(property.getType(), property); } // 获取字段上的 @Sensitive 注解 Sensitive sensitive = property.getAnnotation(Sensitive.class); if (sensitive == null) { sensitive = property.getContextAnnotation(Sensitive.class); } if (sensitive != null) { return new SensitiveSerializer( sensitive.type(), sensitive.prefixLen(), sensitive.suffixLen(), sensitive.maskChar() ); } // 如果不是 String 类型,或者没加注解,就用默认的序列化器 return prov.findValueSerializer(property.getType(), property); } /** * 实际的序列化逻辑 */ @Override public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException { if (value == null) { gen.writeNull(); return; } String result; if (sensitiveType == SensitiveType.CUSTOM) { // 自定义脱敏逻辑 result = customDesensitize(value); } else { // 使用枚举定义的脱敏规则 result = sensitiveType.desensitize(value); } gen.writeString(result); } /** * 自定义脱敏逻辑 * 根据前缀长度和后缀长度进行脱敏 */ private String customDesensitize(String value) { if (value == null || value.isEmpty()) { return value; } int length = value.length(); // 如果原字符串太短,无法保留指定的前后缀 if (length <= prefixLen + suffixLen) { return maskChar.repeat(length); } String prefix = value.substring(0, prefixLen); String suffix = value.substring(length - suffixLen); int maskLen = length - prefixLen - suffixLen; return prefix + maskChar.repeat(maskLen) + suffix; } } ``` ###### **在 VO 中使用** ```java import lombok.Data; /** * 用户信息 VO * 演示各种脱敏注解的使用 */ @Data public class UserVO { private Long id; @Sensitive(type = SensitiveType.CHINESE_NAME) private String name; @Sensitive(type = SensitiveType.MOBILE_PHONE) private String phone; @Sensitive(type = SensitiveType.ID_CARD) private String idCard; @Sensitive(type = SensitiveType.EMAIL) private String email; @Sensitive(type = SensitiveType.BANK_CARD) private String bankCard; @Sensitive(type = SensitiveType.ADDRESS) private String address; @Sensitive(type = SensitiveType.PASSWORD) private String password; // 自定义脱敏:保留前2位和后3位 @Sensitive(type = SensitiveType.CUSTOM, prefixLen = 2, suffixLen = 3) private String customField; } ``` ###### 测试效果 ```java @RestController @RequestMapping("/user") public class UserController { @GetMapping("/{id}") public UserVO getUser(@PathVariable Long id) { UserVO user = new UserVO(); user.setId(id); user.setName("张三丰"); user.setPhone("13812345678"); user.setIdCard("110101199003070312"); user.setEmail("test@example.com"); user.setBankCard("6222021234567890123"); user.setAddress("北京市朝阳区建国路88号"); user.setPassword("123456"); user.setCustomField("ABCDEFGHIJ"); return user; } } 输出结果: { "id": 1, "name": "张**", "phone": "138****5678", "idCard": "110***********0312", "email": "t***@example.com", "bankCard": "6222 **** **** 0123", "address": "北京市朝阳区****", "password": "******", "customField": "AB*****HIJ" } ``` ##### 进阶可设计成条件脱敏 ```typescript /** * 增强版注解 - 支持条件脱敏 */ @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @JacksonAnnotationsInside @JsonSerialize(using = ConditionalSensitiveSerializer.class) public @interface Sensitive { SensitiveType type(); /** * 是否启用脱敏(可配合SpEL表达式) */ String condition() default ""; /** * 需要的角色才能查看原值 */ String[] roles() default {}; } ``` ```java /** * 条件脱敏序列化器 * 根据当前用户角色决定是否脱敏 */ public class ConditionalSensitiveSerializer extends JsonSerializer implements ContextualSerializer { private SensitiveType sensitiveType; private String[] roles; @Override public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException { // 检查当前用户是否有权限查看原值 if (hasPermission(roles)) { gen.writeString(value); // 不脱敏 } else { gen.writeString(sensitiveType.desensitize(value)); // 脱敏 } } private boolean hasPermission(String[] roles) { // 从SecurityContext获取当前用户角色 // 判断是否包含指定角色 Authentication auth = SecurityContextHolder .getContext().getAuthentication(); if (auth == null) return false; return auth.getAuthorities().stream() .map(GrantedAuthority::getAuthority) .anyMatch(r -> Arrays.asList(roles).contains(r)); } // ... createContextual 方法 } ``` 使用示例: ```java @Data public class UserVO { // 只有ADMIN角色才能看到原始手机号 @Sensitive(type = SensitiveType.MOBILE_PHONE, roles = {"ADMIN"}) private String phone; } ``` #### 2.基于 AOP (面向切面编程) 在 Controller 层方法返回后,利用切面拦截返回值,通过反射遍历对象属性并修改。 - **实现原理**:`@Aspect` + `@AfterReturning` + 反射修改对象值。 - **优点**:逻辑与业务完全解耦。 - **缺点**: - **性能差**:反射遍历复杂对象(尤其是深层嵌套 List)非常消耗 CPU。 - **实现复杂**:需要处理基本类型、集合、数组等各种返回结构,容易写出 Bug。 - **组件化难**:依赖 Spring 环境,很难独立抽取成通用 Jar 包。 ![[7-Blog/后端与微服务/assets/AOP_方案的问题-db9217a3.jpg]] ##### **代码实现(仅供了解)** **核心痛点代码化:** 你会发现代码量大,且充满了反射操作。 ```java import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component; import java.lang.reflect.Field; @Aspect @Component public class SensitiveAspect { @AfterReturning(pointcut = "@annotation(com.example.annotation.SensitiveResult)", returning = "result") public void doAfterReturning(JoinPoint joinPoint, Object result) { if (result == null) return; // 核心痛点:必须递归处理(这里仅展示简单的一层处理,实际生产需要写递归逻辑处理List/Map) if (result instanceof Collection) { for (Object obj : (Collection) result) { handleDesensitization(obj); } } else { handleDesensitization(result); } } private void handleDesensitization(Object obj) { // 反射获取所有字段 Field[] fields = obj.getClass().getDeclaredFields(); for (Field field : fields) { if (field.isAnnotationPresent(SensitiveField.class)) { field.setAccessible(true); try { Object value = field.get(obj); if (value instanceof String) { // 修改原对象的值! String hidden = performMasking((String) value); field.set(obj, hidden); } } catch (IllegalAccessException e) { log.error("脱敏失败", e); } } } } private String performMasking(String original) { return original.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2"); } } ``` #### 3. 基于 Hibernate Validator 在数据校验阶段(Validating)修改数据。 - **实现原理**:实现 `ConstraintValidator` 并在校验通过后修改值。 - **优点**:复用校验逻辑。 - **缺点**:**场景极其有限**。Validator 通常用于**入参**校验,而脱敏通常是**出参**需求,方向相反。 **核心痛点代码化:** 这种写法非常怪异,通常用于清洗数据(如去除空格),而非脱敏输出。 ```java // 1. 定义注解 @Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = SensitiveValidator.class) // 指定校验器 public @interface SensitiveValid { String message() default "脱敏处理"; Class[] groups() default {}; Class[] payload() default {}; } // 2. 实现校验器 (兼职修改数据) public class SensitiveValidator implements ConstraintValidator { @Override public boolean isValid(String value, ConstraintValidatorContext context) { // 这里的本意是校验,但我们利用副作用修改数据(这是反模式!) // 注意:Java的String是不可变的,Hibernate Validator 只有在特定钩子下才能改变引用 // 所以实际上这个方案在很多版本中是行不通的,或者需要配合自定义的Payload处理流程 return true; } } ``` 注:由于 Hibernate Validator 主要用于验证,想通过它修改值比较困难且非标准,通常不推荐展示此代码作为生产方案。 #### 4.Getter 方法覆写 (朴实无华型) ![[7-Blog/后端与微服务/assets/Lombok_+_手动_Getter_原理-efb9447d.jpg]] 利用 Java Bean 的特性(以及 Lombok 的机制),手动接管字段的读取逻辑。 虽然实体类使用了 Lombok 的 `@Data` 或 `@Getter` 自动生成方法,但 Java 编译器遵循 **“手动优先”** 原则。 只要我们在类中**手动写了** `getXxx()`**方法**,Lombok 就不会生成该方法,而是直接使用我们写的。 由于 JSON 序列化工具(Jackson, FastJson, Gson)底层默认都是调用 `get` 方法来获取值,因此在这里做手脚,可以**一统所有序列化框架**。 ##### 代码实现 ```java import lombok.Data; @Data public class UserVO { private Long id; private String name; private String phone; private String idCard; private String email; // ===== 手动覆写需要脱敏的字段的Getter ===== public String getName() { return SensitiveUtil.maskName(this.name); } public String getPhone() { return SensitiveUtil.maskPhone(this.phone); } public String getIdCard() { return SensitiveUtil.maskIdCard(this.idCard); } public String getEmail() { return SensitiveUtil.maskEmail(this.email); } // ===== 如果需要获取原始值,可以提供额外方法 ===== public String getOriginalPhone() { return this.phone; } } ``` 脱敏工具类 ```java /** * 敏感数据脱敏工具类 * 核心工具类 - 可被所有方案复用 */ public final class SensitiveUtil { private SensitiveUtil() {} /** * 手机号脱敏 * 13812345678 → 138****5678 */ public static String maskPhone(String phone) { if (phone == null || phone.length() < 11) { return phone; } return phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2"); } /** * 身份证号脱敏 * 110101199003070312 → 110***********0312 */ public static String maskIdCard(String idCard) { if (idCard == null || idCard.length() < 15) { return idCard; } return idCard.replaceAll("(\\d{3})\\d+(\\d{4})", "$1***********$2"); } /** * 姓名脱敏 * 张三 → 张* * 张三丰 → 张** */ public static String maskName(String name) { if (name == null || name.isEmpty()) { return name; } if (name.length() == 1) { return name; } if (name.length() == 2) { return name.charAt(0) + "*"; } return name.charAt(0) + "*".repeat(name.length() - 1); } /** * 邮箱脱敏 * test@example.com → t***@example.com */ public static String maskEmail(String email) { if (email == null || !email.contains("@")) { return email; } int atIndex = email.indexOf("@"); if (atIndex <= 1) { return email; } return email.charAt(0) + "***" + email.substring(atIndex); } /** * 银行卡号脱敏 * 6222021234567890123 → 6222 **** **** 0123 */ public static String maskBankCard(String bankCard) { if (bankCard == null || bankCard.length() < 12) { return bankCard; } return bankCard.substring(0, 4) + " **** **** " + bankCard.substring(bankCard.length() - 4); } /** * 地址脱敏 * 北京市朝阳区建国路88号 → 北京市朝阳区**** */ public static String maskAddress(String address) { if (address == null || address.length() < 8) { return address; } return address.substring(0, 6) + "****"; } /** * 通用脱敏方法 * 保留前prefixLen位和后suffixLen位,中间用maskChar替换 */ public static String mask(String value, int prefixLen, int suffixLen, String maskChar) { if (value == null || value.isEmpty()) { return value; } int length = value.length(); if (length <= prefixLen + suffixLen) { return maskChar.repeat(length); } String prefix = value.substring(0, prefixLen); String suffix = value.substring(length - suffixLen); int maskLen = length - prefixLen - suffixLen; return prefix + maskChar.repeat(maskLen) + suffix; } } ``` #### 分析与对比 ![[7-Blog/后端与微服务/assets/image-cccaa9e5.png]] ![[7-Blog/后端与微服务/assets/展示脱敏方案对比-fffd5c19.jpg]] - **对于简单的、仅用于前端展示的 VO 对象**: 直接使用 **Getter 覆写**。它最简单、最稳健,不用担心框架升级或 Json 库替换带来的兼容性问题。 - **对于大型项目、标准化开发**: 建议采用 **Jackson 注解 (** `@Sensitive`**) +** `ContextualSerializer`。虽然依赖 Jackson,但它保留了对象在 Java 内部的原始值,只在输出 JSON 的最后一刻才改变形态,既安全又灵活。 - **如果你的项目可能替换 Json 库**: 可以将脱敏算法抽取为独立的 `Util` 工具类。无论是 Jackson 的 Serializer,还是 FastJson 的 Filter,亦或是手动写的 Getter,都统一调用这个工具类,实现逻辑复用。 ### **Step 3:实现方案** 大麦项目选择了**最简单直接的方案**:在 VO 层重写 get 方法。 ```java @Data @ApiModel(value="TicketUserVo", description ="购票人数据") public class TicketUserVo implements Serializable { private static final long serialVersionUID = 1L; @ApiModelProperty(name ="id", dataType ="Long", value ="购票人id") private Long id; @ApiModelProperty(name ="userId", dataType ="Long", value ="用户id") private Long userId; @ApiModelProperty(name ="relName", dataType ="String", value ="用户真实名字") private String relName; @ApiModelProperty(name ="idType", dataType ="Integer", value ="证件类型") private Integer idType; @ApiModelProperty(name ="idNumber", dataType ="String", value ="证件号码") private String idNumber; /** * 姓名脱敏:只显示第一个字后面的内容 * 张三 → *三 * 李小明 → *小明 */ public String getRelName() { if (StringUtil.isNotEmpty(relName)) { return StrUtil.hide(relName, 0, 1); } return relName; } /** * 证件号脱敏:保留前4位和后4位 * 110101199001011234 → 1101**********1234 */ public String getIdNumber() { if (StringUtil.isNotEmpty(idNumber)) { return DesensitizedUtil.idCardNum(idNumber, 4, 4); } else { return idNumber; } } } ``` #### **方案原理分析** ![[7-Blog/后端与微服务/assets/为什么重写_get_方法有效?-3b55f349.jpg]] #### **Hutool 脱敏工具类** ```java /** * 字符串隐藏(掩码) * * CharSequenceUtil.hide("jackduan@163.com", -1, 4) → ****duan@163.com * CharSequenceUtil.hide("jackduan@163.com", 2, 3) → ja*kduan@163.com * CharSequenceUtil.hide("张三", 0, 1) → *三 * CharSequenceUtil.hide("李小明", 0, 1) → *小明 */ public static String hide(CharSequence str, int startInclude, int endExclude) { return replace(str, startInclude, endExclude, '*'); } /** * 身份证号脱敏 * * idCardNum("110101199001011234", 4, 4) → 1101**********1234 * idCardNum("110101199001011234", 6, 4) → 110101********1234 */ public static String idCardNum(String idCardNum, int front, int end) { if (StrUtil.isBlank(idCardNum)) { return StrUtil.EMPTY; } if ((front + end) > idCardNum.length()) { return StrUtil.EMPTY; } if (front < 0 || end < 0) { return StrUtil.EMPTY; } return StrUtil.hide(idCardNum, front, idCardNum.length() - end); } ``` ## **第二部分:加密存储** ### **Step 1:业务理解** ![[7-Blog/后端与微服务/assets/image-1aca39c2.png]] ### **Step 2:技术选型** ![[7-Blog/后端与微服务/assets/image-9c03e1f8.png]] ![[7-Blog/后端与微服务/assets/image-a117ba51.png]] #### **方案一:对称加密(AES/SM4)** ##### **原理图解** ![[7-Blog/后端与微服务/assets/image-46c37b87.png]] ##### **AES 加密实现** ```java import javax.crypto.Cipher; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; import javax.crypto.spec.GCMParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.security.SecureRandom; import java.util.Base64; /** * AES 加密工具类 * 使用 AES-GCM 模式(推荐,提供认证加密) */ public class AESUtil { private static final String ALGORITHM = "AES"; private static final String TRANSFORMATION = "AES/GCM/NoPadding"; private static final int KEY_SIZE = 256; // 密钥长度 private static final int GCM_IV_LENGTH = 12; // IV长度 private static final int GCM_TAG_LENGTH = 128; // 认证标签长度 /** * 生成 AES 密钥 */ public static String generateKey() throws Exception { KeyGenerator keyGenerator = KeyGenerator.getInstance(ALGORITHM); keyGenerator.init(KEY_SIZE, new SecureRandom()); SecretKey secretKey = keyGenerator.generateKey(); return Base64.getEncoder().encodeToString(secretKey.getEncoded()); } /** * AES-GCM 加密 * @param plainText 明文 * @param base64Key Base64编码的密钥 * @return Base64编码的密文(包含IV) */ public static String encrypt(String plainText, String base64Key) throws Exception { if (plainText == null || plainText.isEmpty()) { return plainText; } byte[] keyBytes = Base64.getDecoder().decode(base64Key); SecretKeySpec keySpec = new SecretKeySpec(keyBytes, ALGORITHM); // 生成随机 IV byte[] iv = new byte[GCM_IV_LENGTH]; new SecureRandom().nextBytes(iv); GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv); Cipher cipher = Cipher.getInstance(TRANSFORMATION); cipher.init(Cipher.ENCRYPT_MODE, keySpec, gcmSpec); byte[] cipherText = cipher.doFinal(plainText.getBytes("UTF-8")); // 将 IV 和密文拼接在一起 byte[] combined = new byte[iv.length + cipherText.length]; System.arraycopy(iv, 0, combined, 0, iv.length); System.arraycopy(cipherText, 0, combined, iv.length, cipherText.length); return Base64.getEncoder().encodeToString(combined); } /** * AES-GCM 解密 * @param cipherText Base64编码的密文(包含IV) * @param base64Key Base64编码的密钥 * @return 明文 */ public static String decrypt(String cipherText, String base64Key) throws Exception { if (cipherText == null || cipherText.isEmpty()) { return cipherText; } byte[] keyBytes = Base64.getDecoder().decode(base64Key); SecretKeySpec keySpec = new SecretKeySpec(keyBytes, ALGORITHM); byte[] combined = Base64.getDecoder().decode(cipherText); // 分离 IV 和密文 byte[] iv = new byte[GCM_IV_LENGTH]; byte[] encrypted = new byte[combined.length - GCM_IV_LENGTH]; System.arraycopy(combined, 0, iv, 0, iv.length); System.arraycopy(combined, iv.length, encrypted, 0, encrypted.length); GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv); Cipher cipher = Cipher.getInstance(TRANSFORMATION); cipher.init(Cipher.DECRYPT_MODE, keySpec, gcmSpec); byte[] plainText = cipher.doFinal(encrypted); return new String(plainText, "UTF-8"); } } ``` ##### **SM4 国密算法实现** ```java import org.bouncycastle.jce.provider.BouncyCastleProvider; import javax.crypto.Cipher; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.security.SecureRandom; import java.security.Security; import java.util.Base64; /** * SM4 国密算法工具类 * 需要引入 BouncyCastle 依赖 * * * org.bouncycastle * bcprov-jdk15on * 1.70 * */ public class SM4Util { private static final String ALGORITHM = "SM4"; private static final String TRANSFORMATION = "SM4/CBC/PKCS5Padding"; private static final int KEY_SIZE = 128; private static final int IV_SIZE = 16; static { // 注册 BouncyCastle 提供者 Security.addProvider(new BouncyCastleProvider()); } /** * 生成 SM4 密钥 */ public static String generateKey() throws Exception { KeyGenerator keyGenerator = KeyGenerator.getInstance(ALGORITHM, "BC"); keyGenerator.init(KEY_SIZE, new SecureRandom()); SecretKey secretKey = keyGenerator.generateKey(); return Base64.getEncoder().encodeToString(secretKey.getEncoded()); } /** * SM4 加密 */ public static String encrypt(String plainText, String base64Key) throws Exception { if (plainText == null || plainText.isEmpty()) { return plainText; } byte[] keyBytes = Base64.getDecoder().decode(base64Key); SecretKeySpec keySpec = new SecretKeySpec(keyBytes, ALGORITHM); // 生成随机 IV byte[] iv = new byte[IV_SIZE]; new SecureRandom().nextBytes(iv); IvParameterSpec ivSpec = new IvParameterSpec(iv); Cipher cipher = Cipher.getInstance(TRANSFORMATION, "BC"); cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec); byte[] cipherText = cipher.doFinal(plainText.getBytes("UTF-8")); // IV + 密文 byte[] combined = new byte[iv.length + cipherText.length]; System.arraycopy(iv, 0, combined, 0, iv.length); System.arraycopy(cipherText, 0, combined, iv.length, cipherText.length); return Base64.getEncoder().encodeToString(combined); } /** * SM4 解密 */ public static String decrypt(String cipherText, String base64Key) throws Exception { if (cipherText == null || cipherText.isEmpty()) { return cipherText; } byte[] keyBytes = Base64.getDecoder().decode(base64Key); SecretKeySpec keySpec = new SecretKeySpec(keyBytes, ALGORITHM); byte[] combined = Base64.getDecoder().decode(cipherText); byte[] iv = new byte[IV_SIZE]; byte[] encrypted = new byte[combined.length - IV_SIZE]; System.arraycopy(combined, 0, iv, 0, iv.length); System.arraycopy(combined, iv.length, encrypted, 0, encrypted.length); IvParameterSpec ivSpec = new IvParameterSpec(iv); Cipher cipher = Cipher.getInstance(TRANSFORMATION, "BC"); cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec); byte[] plainText = cipher.doFinal(encrypted); return new String(plainText, "UTF-8"); } } ``` ##### **使用示例** ```java public class SymmetricEncryptDemo { public static void main(String[] args) throws Exception { // 1. 生成密钥(实际项目中应该安全存储) String aesKey = AESUtil.generateKey(); String sm4Key = SM4Util.generateKey(); String phone = "13812345678"; // 2. AES 加密解密 String aesEncrypted = AESUtil.encrypt(phone, aesKey); String aesDecrypted = AESUtil.decrypt(aesEncrypted, aesKey); System.out.println("=== AES ==="); System.out.println("原文: " + phone); System.out.println("密文: " + aesEncrypted); System.out.println("解密: " + aesDecrypted); // 3. SM4 加密解密 String sm4Encrypted = SM4Util.encrypt(phone, sm4Key); String sm4Decrypted = SM4Util.decrypt(sm4Encrypted, sm4Key); System.out.println("=== SM4 ==="); System.out.println("原文: " + phone); System.out.println("密文: " + sm4Encrypted); System.out.println("解密: " + sm4Decrypted); } } ``` #### **方案二:非对称加密(RSA/SM2)** ##### **原理图解** ![[7-Blog/后端与微服务/assets/image-d5bb4004.png]] ##### **RSA 加密实现** ```java import javax.crypto.Cipher; import java.security.*; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; import java.util.Base64; /** * RSA 非对称加密工具类 */ public class RSAUtil { private static final String ALGORITHM = "RSA"; private static final String TRANSFORMATION = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding"; private static final int KEY_SIZE = 2048; /** * 生成 RSA 密钥对 */ public static KeyPairResult generateKeyPair() throws Exception { KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(ALGORITHM); keyPairGenerator.initialize(KEY_SIZE, new SecureRandom()); KeyPair keyPair = keyPairGenerator.generateKeyPair(); String publicKey = Base64.getEncoder().encodeToString( keyPair.getPublic().getEncoded()); String privateKey = Base64.getEncoder().encodeToString( keyPair.getPrivate().getEncoded()); return new KeyPairResult(publicKey, privateKey); } /** * 公钥加密 */ public static String encrypt(String plainText, String base64PublicKey) throws Exception { if (plainText == null || plainText.isEmpty()) { return plainText; } byte[] keyBytes = Base64.getDecoder().decode(base64PublicKey); X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes); KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM); PublicKey publicKey = keyFactory.generatePublic(keySpec); Cipher cipher = Cipher.getInstance(TRANSFORMATION); cipher.init(Cipher.ENCRYPT_MODE, publicKey); byte[] cipherText = cipher.doFinal(plainText.getBytes("UTF-8")); return Base64.getEncoder().encodeToString(cipherText); } /** * 私钥解密 */ public static String decrypt(String cipherText, String base64PrivateKey) throws Exception { if (cipherText == null || cipherText.isEmpty()) { return cipherText; } byte[] keyBytes = Base64.getDecoder().decode(base64PrivateKey); PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes); KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM); PrivateKey privateKey = keyFactory.generatePrivate(keySpec); Cipher cipher = Cipher.getInstance(TRANSFORMATION); cipher.init(Cipher.DECRYPT_MODE, privateKey); byte[] plainText = cipher.doFinal(Base64.getDecoder().decode(cipherText)); return new String(plainText, "UTF-8"); } /** * 私钥签名 */ public static String sign(String data, String base64PrivateKey) throws Exception { byte[] keyBytes = Base64.getDecoder().decode(base64PrivateKey); PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes); KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM); PrivateKey privateKey = keyFactory.generatePrivate(keySpec); Signature signature = Signature.getInstance("SHA256withRSA"); signature.initSign(privateKey); signature.update(data.getBytes("UTF-8")); return Base64.getEncoder().encodeToString(signature.sign()); } /** * 公钥验签 */ public static boolean verify(String data, String sign, String base64PublicKey) throws Exception { byte[] keyBytes = Base64.getDecoder().decode(base64PublicKey); X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes); KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM); PublicKey publicKey = keyFactory.generatePublic(keySpec); Signature signature = Signature.getInstance("SHA256withRSA"); signature.initVerify(publicKey); signature.update(data.getBytes("UTF-8")); return signature.verify(Base64.getDecoder().decode(sign)); } /** * 密钥对结果 */ public static class KeyPairResult { private final String publicKey; private final String privateKey; public KeyPairResult(String publicKey, String privateKey) { this.publicKey = publicKey; this.privateKey = privateKey; } public String getPublicKey() { return publicKey; } public String getPrivateKey() { return privateKey; } } } ``` ##### **混合加密(推荐实践)** ```java /** * 混合加密方案 * RSA + AES 结合,兼顾安全性和性能 * * 原理: * 1. 用 AES 加密大量数据(速度快) * 2. 用 RSA 加密 AES 密钥(安全传输) */ public class HybridEncryptUtil { /** * 混合加密 * @return 格式: Base64(RSA加密的AES密钥) + "." + Base64(AES加密的数据) */ public static String encrypt(String plainText, String rsaPublicKey) throws Exception { // 1. 生成随机 AES 密钥 String aesKey = AESUtil.generateKey(); // 2. 用 AES 加密数据 String encryptedData = AESUtil.encrypt(plainText, aesKey); // 3. 用 RSA 公钥加密 AES 密钥 String encryptedKey = RSAUtil.encrypt(aesKey, rsaPublicKey); // 4. 组合返回 return encryptedKey + "." + encryptedData; } /** * 混合解密 */ public static String decrypt(String cipherText, String rsaPrivateKey) throws Exception { // 1. 分离密钥和数据 String[] parts = cipherText.split("\\."); if (parts.length != 2) { throw new IllegalArgumentException("Invalid cipher text format"); } String encryptedKey = parts[0]; String encryptedData = parts[1]; // 2. 用 RSA 私钥解密 AES 密钥 String aesKey = RSAUtil.decrypt(encryptedKey, rsaPrivateKey); // 3. 用 AES 解密数据 return AESUtil.decrypt(encryptedData, aesKey); } } ``` #### **方案三:哈希算法(密码存储)** ##### **原理图解** ![[7-Blog/后端与微服务/assets/image-afd953a2.png]] ##### **BCrypt 密码加密(推荐)** ```java import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; /** * 密码加密工具类 * 使用 BCrypt 算法 */ public class PasswordUtil { // BCrypt 强度 (4-31),越大越安全但越慢,推荐 10-12 private static final int STRENGTH = 12; private static final PasswordEncoder encoder = new BCryptPasswordEncoder(STRENGTH); /** * 加密密码 * @param rawPassword 原始密码 * @return BCrypt 哈希值(包含算法标识、强度、盐值) */ public static String encode(String rawPassword) { return encoder.encode(rawPassword); } /** * 验证密码 * @param rawPassword 用户输入的密码 * @param encodedPassword 数据库存储的哈希值 * @return 是否匹配 */ public static boolean matches(String rawPassword, String encodedPassword) { return encoder.matches(rawPassword, encodedPassword); } } ``` ##### **SHA256 + 盐值实现** ```java import java.security.MessageDigest; import java.security.SecureRandom; import java.util.Base64; /** * SHA256 + 盐值 密码工具类 * 适用于不想引入 Spring Security 的场景 */ public class SHA256PasswordUtil { private static final int SALT_LENGTH = 16; private static final int ITERATIONS = 10000; // 迭代次数 /** * 加密密码 * @return 格式: Base64(盐值) + ":" + Base64(哈希值) */ public static String encode(String rawPassword) throws Exception { // 生成随机盐值 byte[] salt = new byte[SALT_LENGTH]; new SecureRandom().nextBytes(salt); // 计算哈希 byte[] hash = hash(rawPassword, salt); // 组合返回 return Base64.getEncoder().encodeToString(salt) + ":" + Base64.getEncoder().encodeToString(hash); } /** * 验证密码 */ public static boolean matches(String rawPassword, String encodedPassword) throws Exception { String[] parts = encodedPassword.split(":"); if (parts.length != 2) { return false; } byte[] salt = Base64.getDecoder().decode(parts[0]); byte[] expectedHash = Base64.getDecoder().decode(parts[1]); byte[] actualHash = hash(rawPassword, salt); // 时间恒定比较,防止时序攻击 return MessageDigest.isEqual(expectedHash, actualHash); } private static byte[] hash(String password, byte[] salt) throws Exception { MessageDigest md = MessageDigest.getInstance("SHA-256"); md.update(salt); byte[] hash = md.digest(password.getBytes("UTF-8")); // 多次迭代增加破解难度 for (int i = 0; i < ITERATIONS; i++) { md.reset(); hash = md.digest(hash); } return hash; } } ``` ###### **使用示例** ```java public class PasswordDemo { public static void main(String[] args) throws Exception { String password = "MySecretPassword123"; // BCrypt String bcryptHash = PasswordUtil.encode(password); System.out.println("BCrypt哈希: " + bcryptHash); // 输出: $2a$12$xxxxx... (每次不同) System.out.println("验证正确密码: " + PasswordUtil.matches(password, bcryptHash)); // 输出: true System.out.println("验证错误密码: " + PasswordUtil.matches("wrong", bcryptHash)); // 输出: false // SHA256 + 盐值 String sha256Hash = SHA256PasswordUtil.encode(password); System.out.println("SHA256哈希: " + sha256Hash); System.out.println("验证: " + SHA256PasswordUtil.matches(password, sha256Hash)); } } ``` #### **方案四:ShardingSphere 透明加密(⭐推荐)** ##### **透明加密原理** ![[7-Blog/后端与微服务/assets/image-bf7ce05c.png]] ##### **数据库表结构** ```sql -- 原始表结构 CREATE TABLE t_user ( id BIGINT PRIMARY KEY, username VARCHAR(50), phone VARCHAR(20), -- 原字段 id_card VARCHAR(30) ); -- 改造后的表结构 CREATE TABLE t_user ( id BIGINT PRIMARY KEY, username VARCHAR(50), phone_cipher VARCHAR(200), -- 密文列(必需) phone_plain VARCHAR(20), -- 明文列(可选,用于模糊查询) id_card_cipher VARCHAR(200), id_card_plain VARCHAR(30) ); ``` ##### **配置文件方式(推荐)** ```yaml # application.yml spring: shardingsphere: datasource: names: ds ds: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://localhost:3306/test?useSSL=false username: root password: root rules: # 加密规则配置 encrypt: # 加密器定义 encryptors: # SM4 加密器 sm4_encryptor: type: SM4 props: sm4-key: 1234567890abcdef # 16字节密钥 sm4-mode: CBC sm4-iv: fedcba0987654321 # 16字节IV sm4-padding: PKCS5Padding # AES 加密器 aes_encryptor: type: AES props: aes-key-value: my_aes_secret_key_32bytes!!!! # 32字节密钥 # 表加密规则 tables: # t_user 表配置 t_user: columns: # phone 字段加密配置 phone: cipher-column: phone_cipher # 密文列名 plain-column: phone_plain # 明文列名(可选) encryptor-name: sm4_encryptor # 使用的加密器 # id_card 字段加密配置 id_card: cipher-column: id_card_cipher encryptor-name: sm4_encryptor # t_order 表配置 t_order: columns: buyer_phone: cipher-column: buyer_phone_cipher encryptor-name: aes_encryptor # 查询时使用密文列还是明文列 query-with-cipher-column: true props: sql-show: true # 打印SQL,调试用 ``` ##### **Java Config 方式** ```java import org.apache.shardingsphere.encrypt.api.config.EncryptRuleConfiguration; import org.apache.shardingsphere.encrypt.api.config.rule.EncryptColumnRuleConfiguration; import org.apache.shardingsphere.encrypt.api.config.rule.EncryptTableRuleConfiguration; import org.apache.shardingsphere.infra.config.algorithm.AlgorithmConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.*; @Configuration public class ShardingSphereEncryptConfig { @Bean public EncryptRuleConfiguration encryptRuleConfiguration() { // 1. 定义加密器 Map encryptors = new HashMap<>(); // SM4 加密器 Properties sm4Props = new Properties(); sm4Props.setProperty("sm4-key", "1234567890abcdef"); sm4Props.setProperty("sm4-mode", "CBC"); sm4Props.setProperty("sm4-iv", "fedcba0987654321"); sm4Props.setProperty("sm4-padding", "PKCS5Padding"); encryptors.put("sm4_encryptor", new AlgorithmConfiguration("SM4", sm4Props)); // AES 加密器 Properties aesProps = new Properties(); aesProps.setProperty("aes-key-value", "my_aes_secret_key_32bytes!!!!"); encryptors.put("aes_encryptor", new AlgorithmConfiguration("AES", aesProps)); // 2. 定义表的加密规则 List tables = new ArrayList<>(); // t_user 表 EncryptColumnRuleConfiguration phoneColumn = new EncryptColumnRuleConfiguration( "phone", // 逻辑列名 "phone_cipher", // 密文列 null, // 辅助查询列 "phone_plain", // 明文列 "sm4_encryptor", // 加密器名 null ); EncryptColumnRuleConfiguration idCardColumn = new EncryptColumnRuleConfiguration( "id_card", "id_card_cipher", null, null, // 不保留明文 "sm4_encryptor", null ); EncryptTableRuleConfiguration userTable = new EncryptTableRuleConfiguration( "t_user", Arrays.asList(phoneColumn, idCardColumn), true // 查询时使用密文列 ); tables.add(userTable); // 3. 创建加密规则配置 return new EncryptRuleConfiguration(tables, encryptors); } } ``` ##### **自定义加密器** ```java import org.apache.shardingsphere.encrypt.spi.EncryptAlgorithm; import org.apache.shardingsphere.encrypt.spi.context.EncryptContext; import java.util.Properties; /** * 自定义加密算法 * 需要在 META-INF/services/ 下注册 SPI */ public class CustomEncryptAlgorithm implements EncryptAlgorithm { private Properties props; private String secretKey; @Override public void init(Properties props) { this.props = props; this.secretKey = props.getProperty("secret-key"); } @Override public String encrypt(Object plainValue, EncryptContext encryptContext) { if (plainValue == null) { return null; } try { // 使用自定义加密逻辑 return SM4Util.encrypt(plainValue.toString(), secretKey); } catch (Exception e) { throw new RuntimeException("Encrypt failed", e); } } @Override public Object decrypt(String cipherValue, EncryptContext encryptContext) { if (cipherValue == null || cipherValue.isEmpty()) { return null; } try { return SM4Util.decrypt(cipherValue, secretKey); } catch (Exception e) { throw new RuntimeException("Decrypt failed", e); } } @Override public String getType() { return "CUSTOM_SM4"; // 配置文件中使用的类型名 } @Override public Properties getProps() { return props; } } ``` ###### **SPI 注册文件:** ``` # META-INF/services/org.apache.shardingsphere.encrypt.spi.EncryptAlgorithm com.example.encrypt.CustomEncryptAlgorithm ``` ##### **业务代码(完全无感知)** ```java @Data @TableName("t_user") public class User { @TableId(type = IdType.ASSIGN_ID) private Long id; private String username; // 正常写 phone,ShardingSphere 自动处理 private String phone; private String idCard; } @Mapper public interface UserMapper extends BaseMapper { // 正常的 MyBatis 操作 @Select("SELECT * FROM t_user WHERE phone = #{phone}") User selectByPhone(@Param("phone") String phone); } @Service public class UserService { @Autowired private UserMapper userMapper; public void createUser() { User user = new User(); user.setUsername("张三"); user.setPhone("13812345678"); // 明文传入 user.setIdCard("110101199003070312"); userMapper.insert(user); // 自动加密存储 } public User getUser(Long id) { // 自动解密返回 return userMapper.selectById(id); // phone = "13812345678" } public User getByPhone(String phone) { // 查询条件也会自动加密 return userMapper.selectByPhone(phone); } } ``` ##### **重要注意事项** ⚠️ 模糊查询限制 • 密文列无法直接 LIKE 查询 • 解决方案:保留明文列(plain-column) 用于模糊查询 • 或者使用辅助查询列(assisted-query-column) ⚠️ 存量数据迁移 • 需要编写迁移脚本,将明文加密后写入密文列 • 可以配置 query-with-cipher-column: false 过渡 ⚠️ 密钥管理 • 密钥不要硬编码在配置文件 • 推荐使用配置中心或密钥管理服务(KMS) ⚠️ 索引问题 • 密文列上的索引仍然有效(精确查询) • 范围查询、排序会失效 ⚠️ 性能影响 • 加解密有 CPU 开销 • 密文比明文长,增加存储和带宽开销 #### **分析** ![[7-Blog/后端与微服务/assets/image-2c701b93.png]] ![[7-Blog/后端与微服务/assets/image-eb5b0dfc.png]] 1️⃣ 密码存储(BCrypt) - 专为密码设计,自带盐值 - 故意设计得慢,抵抗暴力破解 2️⃣ 数据库字段加密 - 已使用 ShardingSphere → 提供透明加密(业务无感知) - 未使用 ShardingSphere → 推荐 AES / SM4 + TypeHandler 3️⃣ 国内合规项目(SM4 国密算法) - 金融、政务项目通常要求使用国密算法 4️⃣ 数据传输加密(混合加密:RSA + AES) - RSA 用于交换密钥 - AES 用于加密实际数据 5️⃣ 密钥管理是关键 - 不要将密钥硬编码在代码中 - 使用配置中心或 KMS(如 KMS、HashiCorp Vault 等) - 定期轮换密钥 #### **最佳实践** ```java /** * 统一加密服务接口 * 方便切换加密实现 */ public interface EncryptService { /** * 加密 */ String encrypt(String plainText); /** * 解密 */ String decrypt(String cipherText); } /** * SM4 实现 */ @Service @ConditionalOnProperty(name = "encrypt.type", havingValue = "SM4") public class SM4EncryptService implements EncryptService { @Value("${encrypt.sm4.key}") private String key; @Override public String encrypt(String plainText) { return SM4Util.encrypt(plainText, key); } @Override public String decrypt(String cipherText) { return SM4Util.decrypt(cipherText, key); } } /** * AES 实现 */ @Service @ConditionalOnProperty(name = "encrypt.type", havingValue = "AES") public class AESEncryptService implements EncryptService { @Value("${encrypt.aes.key}") private String key; @Override public String encrypt(String plainText) { return AESUtil.encrypt(plainText, key); } @Override public String decrypt(String cipherText) { return AESUtil.decrypt(cipherText, key); } } ``` ##### **密钥管理** ``` # ❌ 错误做法:硬编码密钥 encrypt: key: 1234567890abcdef # ✅ 正确做法:使用环境变量 encrypt: key: ${ENCRYPT_KEY} # ✅ 更好做法:使用配置中心 encrypt: key: ${nacos:encrypt.key} # ✅ 最佳做法:使用密钥管理服务 (KMS) encrypt: key-id: ${KMS_KEY_ID} ``` ### **Step 3:实现方案** ![[7-Blog/后端与微服务/assets/shardingSphere-6f95d7d2.jpg]] #### **依赖引入** ``` 5.3.2 org.apache.shardingsphere shardingsphere-jdbc-core ${shardingsphere.version} ``` #### **数据源配置** ```yaml spring: datasource: driver-class-name: org.apache.shardingsphere.driver.ShardingSphereDriver url: jdbc:shardingsphere:classpath:shardingsphere-user.yaml ``` #### **加密规则配置** ```yaml rules: # 加密规则 - !ENCRYPT tables: # ========== d_user 表 ========== d_user: columns: # 手机号加密 mobile: cipherColumn: mobile # 密文存储列 encryptorName: user_encryption_algorithm # 使用的加密算法 # 密码加密 password: cipherColumn: password encryptorName: user_encryption_algorithm # 身份证号加密 id_number: cipherColumn: id_number encryptorName: user_encryption_algorithm # ========== d_user_mobile 表 ========== d_user_mobile: columns: mobile: cipherColumn: mobile encryptorName: user_encryption_algorithm # ========== d_ticket_user 表 ========== d_ticket_user: columns: id_number: cipherColumn: id_number encryptorName: user_encryption_algorithm # ========== 加密算法配置 ========== encryptors: user_encryption_algorithm: type: SM4 # 国密算法 SM4 props: sm4-key: d3ecdaa11d6ab89e1987870186073eaa # 密钥(16字节) sm4-mode: CBC # 加密模式 sm4-iv: 1afc7fdce9ebc393f693cd3d23e35ed2 # 初始化向量 sm4-padding: PKCS7Padding # 填充方式 props: sql-show: true # 开发环境打印真实 SQL ``` #### **关键设计决策:确定性加密** ![[7-Blog/后端与微服务/assets/为什么固定IV-d2d02e3f.jpg]] #### **⚠️ ShardingSphere 加密的使用限制** ![[7-Blog/后端与微服务/assets/shardingsphere加密限制-7bcfe51c.jpg]] #### **SM4 算法参数说明** | **参数** | **类型** | **说明** | | --- | --- | --- | | `sm4-key` | String | SM4 密钥,必须是 16 字节(128位) | | `sm4-mode` | String | 加密模式:CBC 或 ECB | | `sm4-iv` | String | 初始化向量,CBC 模式必须指定,16 字节 | | `sm4-padding` | String | 填充方式:PKCS5Padding 或 PKCS7Padding | ## **完整数据流程图** ![[7-Blog/后端与微服务/assets/敏感数据完整生命周期-4085027a.jpg]] ## **核心总结** ### **两道防线对比** | **防线** | **目的** | **实现方式** | **作用时机** | | --- | --- | --- | --- | | **加密存储** | 防止数据泄露 | ShardingSphere + SM4 | 数据写入数据库时 | | **展示脱敏** | 防止信息暴露 | VO 层重写 get 方法 | 数据返回前端时 | ### **设计亮点** | **设计点** | **说明** | | --- | --- | | **透明加密** | 业务代码无感知,框架自动处理 | | **确定性加密** | 固定 IV,支持分片和索引 | | **简单脱敏** | 重写 get 方法,无需额外依赖 | | **分层处理** | 存储层加密,展示层脱敏,职责分离 | ### **需要注意的问题** 1.密钥管理 - 密钥不能硬编码在配置文件中(生产环境) - 使用密钥管理服务(KMS) - 定期轮换密钥 2. 存量数据迁移 - 启用加密前需要将存量数据加密 - 建议停机迁移或双写过渡 3.性能影响 - 加解密有一定性能开销 - 热点数据考虑缓存明文(内存中) 4.备份恢复 - 密钥丢失=数据永久丢失 - 密钥需要独立备份 --- > *💡 **设计思想**:敏感数据保护是**纵深防御**的典型应用。加密存储是第一道防线,即使数据库被攻破,攻击者也只能看到密文;展示脱敏是第二道防线,即使数据被授权访问,用户也只能看到部分信息。两道防线相互独立,任何一道被突破都不会导致全面失守。* ## 疑问与解答 但是shardingsphere貌似是分库分表的一个工具 适用于微服务项目 但这种对于业务透明的加密解密确实很方便 单体项目能用这个工具吗 或者有没有其他的类似的工具 在我之前的单体项目中 使用的是专为密码设计的BCrypt 为弥补其不足 加上了动态盐 存在用户表里面 实现动态盐+静态盐加密 我觉得还是挺好用的 另外我还察觉到一个问题 邮箱和电话都加密存储的话 索引还能生效吗?分片好像是做哈希确定的 这个shardingsphere里面应该是做好了 先加密 再哈希 从而确定存储位置的吧 不然先确定位置 加密一下 找不到了 失去了意义 模糊查询怎么办呢 框架也会帮忙做好处理吗 如果我自己想在单体项目中实现这样的使用无察觉的加密方式 好像很难处理这些问题 太多细节了 1. **ShardingSphere 是否适用于单体项目?** - ShardingSphere 确实主要用于分库分表,但它的加密功能是独立的 - 单体项目也可以只使用它的加密功能,不启用分片规则 - 但这样确实有点"杀鸡用牛刀"的感觉 2. **单体项目有没有类似的透明加密方案?** - MyBatis 拦截器/TypeHandler - JPA/Hibernate 的 AttributeConverter - Spring Data 的自定义 Converter - 自定义注解 + AOP 3. **用户提到的 BCrypt + 动态盐方案** - BCrypt 是专门为密码设计的哈希算法 - 它本身就有盐值机制 - 用户又加了动态盐,这是一个加强方案 - 但要注意:BCrypt 是单向哈希,不是加密,无法解密 - 适合密码场景(只需验证,不需要获取原文) - 不适合手机号、身份证等需要查看原文的场景 4. **加密后索引能否生效?** - 关键点:确定性加密(同一明文 → 同一密文) - 如果使用确定性加密,索引可以生效 - 因为查询时也会先加密查询条件,再去数据库匹配 - ShardingSphere 的处理流程: - 写入时:明文 → 加密 → 存储密文 - 查询时:WHERE mobile = '13800001234' → 先加密 '13800001234' → WHERE mobile = '加密后的值' - ![[7-Blog/后端与微服务/assets/加密后的索引问题-b0af746c.jpg]] 5. **分片 + 加密的处理顺序** - 用户的理解是对的 - 流程应该是:先加密,再根据密文(或原文)做分片 - 但实际上 ShardingSphere 可能是:先分片(用原文),再加密存储 - 查询时:加密查询条件后,再根据分片键路由 ![[7-Blog/后端与微服务/assets/ShardingSphere_的处理顺序-0454d69a.jpg]] 6. **模糊查询的问题** - 加密存储天然无法支持模糊匹配 - 可选方案包括:不加密模糊查询字段、采用可搜索加密技术、维护前缀hash辅助列,或依赖 ShardingSphere 的有限模糊查询支持 7. **单体项目如何实现透明加密?** - 这涉及不少实现细节。最实用的做法是通过 MyBatis TypeHandler 或 JPA AttributeConverter 来实现,这样能在数据层自动处理加密解密。 对于微服务架构 使用ShardingSphere确实是很方便 既解决了分库分表的问题 又能实现字段级加密解密 学习了解了这么多 我有设想过一套逻辑 因为不外乎是加密解密脱敏 实现自定义注解 封装加密工具类 对外预留接口 内部多种实现使用策略模式真实需求选择 编码时在需要加密的字段添加注解 使用枚举值指定加密算法 如密码使用PassWord标注 内部实现用**BCrypt**算法单向加密 手机、邮箱等字段用XX标准 内部实现用可逆的加密 结合**MyBatis TypeHandler**加密解密过程无感知 既优雅又复用性高 涉及到查询 通过拦截器 先加密 再查询 又保证了索引的生效 到了脱敏逻辑 也是同样的 使用枚举指定脱敏方式 使用权限校验加强脱敏 什么身份(如管理员)可以看到脱敏前的原信息 其他用户权限不足只能查看脱敏后的信息 既保证了安全 又提高了debug效率 我的想法是否健壮 能否实现这个思路 优秀的设计点 - 注解驱动,声明式使用,优雅简洁 - 策略模式,算法可扩展,符合开闭原则。 - TypeHandler +拦截器,透明处理。 - 权限控制脱敏,安全性和便利性兼顾 需要注意的细节 - BCrypt 是单向的,不能用常规 TypeHandler 解密·拦截器需要识别哪些参数需要加密 - 脱敏时需要获取当前用户上下文 - 动态盐需要特殊处理(从同一行其他字段获取) 完全可以实现,下面给出完整方案 [[04-加密脱敏最佳实践]]