在业务系统中,用户敏感信息不能直接明文存储,也不应该直接明文展示。
常见的敏感字段包括:
姓名;
手机号;
身份证号;
地址;
邮箱;
银行卡号。
从数据安全角度看,通常需要做到两件事:
存储时加密:敏感数据进入数据库前进行加密;
展示时脱敏:敏感数据返回给前端前进行解密和脱敏展示。
如果每个接口都手动处理加密、解密和脱敏,代码会非常分散,也容易遗漏。
本文记录一种基于 注解 + 反射 + AOP 的实现方式,将加密和脱敏逻辑从业务代码中抽离出来。
一、需求场景
假设系统中有用户信息,包含姓名和地址等字段。
新增用户时,希望数据写入前自动加密:
张三 -> 加密后存储
湖北省武汉市 -> 加密后存储查询用户时,希望先解密,再做脱敏处理:
张三 -> 张*
湖北省武汉市 -> 湖北省***最终目标是:
业务代码只负责业务逻辑,加密、解密、脱敏由统一切面完成。
二、两种实现方式
1. 手动处理
最直接的方式是在每个业务方法中手动处理。
新增时:
user.setName(AesUtil.encrypt(user.getName()));
user.setAddress(AesUtil.encrypt(user.getAddress()));查询时:
user.setName(DesensitizationUtil.desensitization(
AesUtil.decrypt(user.getName()),
DesensitizationEnum.name
));
user.setAddress(DesensitizationUtil.desensitization(
AesUtil.decrypt(user.getAddress()),
DesensitizationEnum.address
));这种方式实现简单,但问题也很明显:
每个接口都要写重复逻辑;
很容易漏掉某个字段;
后续新增字段时需要修改很多地方;
加解密逻辑和业务逻辑混在一起;
维护成本较高。
2. 注解 + AOP 统一处理
更好的方式是:
在需要加密的字段上添加注解;
在需要解密脱敏的字段上添加注解;
在方法上添加切入点注解;
使用 AOP 拦截方法调用;
通过反射读取字段注解并统一处理。
整体流程如下:
请求进入
↓
AOP 拦截
↓
读取方法参数或返回值
↓
反射扫描字段注解
↓
执行加密 / 解密 / 脱敏
↓
继续执行原方法或返回结果这种方式可以减少业务代码侵入,让加密和脱敏逻辑集中管理。
三、引入依赖
示例项目中需要 Web、AOP、Hutool 和 Lombok 等依赖。
<dependencies>
<!-- Spring Boot 基础依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
</dependency>
<!-- Hutool 工具包 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.20</version>
</dependency>
<!-- AOP -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.7</version>
</dependency>
</dependencies>如果是 Spring Boot 项目,也可以直接使用:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>四、定义脱敏类型枚举
先定义一个脱敏类型枚举,用来区分不同字段的脱敏规则。
package com.weige.javaskillpoint.enums;
public enum DesensitizationEnum {
/**
* 姓名脱敏
*/
name,
/**
* 地址脱敏
*/
address,
/**
* 手机号脱敏
*/
phone;
}不同字段可以对应不同的脱敏策略。
例如:
name -> 保留首字,其余隐藏
address -> 保留前三位,其余隐藏
phone -> 保留前三位和后四位
五、定义加密相关注解
1. 加密字段注解
用于标记需要加密的字段。
package com.weige.javaskillpoint.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptField {
}
2. 加密方法注解
用于标记哪些方法需要执行加密切面。
package com.weige.javaskillpoint.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Encryption {
}
这里需要注意:
@Retention(RetentionPolicy.RUNTIME)
表示注解在运行时仍然保留,这样 AOP 和反射才能读取到注解信息。
六、定义解密脱敏相关注解
1. 解密字段注解
用于标记需要解密并脱敏的字段。
package com.weige.javaskillpoint.annotation;
import com.weige.javaskillpoint.enums.DesensitizationEnum;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DecryptField {
/**
* 脱敏类型
*/
DesensitizationEnum value();
}
这里注解带有一个枚举参数:
DesensitizationEnum value();
用于指定该字段使用哪种脱敏规则。
2. 解密方法注解
用于标记哪些方法返回值需要执行解密脱敏切面。
package com.weige.javaskillpoint.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Decryption {
}
七、定义请求对象和返回对象
1. 新增用户对象 UserBO
新增用户时,对字段进行加密处理。
package com.weige.javaskillpoint.entity;
import com.weige.javaskillpoint.annotation.EncryptField;
public class UserBO {
@EncryptField
private String name;
@EncryptField
private String address;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public UserBO(String name, String address) {
this.name = name;
this.address = address;
}
@Override
public String toString() {
return "UserBO{" +
"name='" + name + '\'' +
", address='" + address + '\'' +
'}';
}
}
字段上的:
@EncryptField表示该字段在方法执行前需要加密。
2. 用户返回对象 UserDO
查询用户时,对字段进行解密和脱敏。
package com.weige.javaskillpoint.entity;
import com.weige.javaskillpoint.annotation.DecryptField;
import com.weige.javaskillpoint.enums.DesensitizationEnum;
public class UserDO {
@DecryptField(DesensitizationEnum.name)
private String name;
@DecryptField(DesensitizationEnum.address)
private String address;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public UserDO(String name, String address) {
this.name = name;
this.address = address;
}
}字段上的:
@DecryptField(DesensitizationEnum.name)表示该字段需要解密,并按照姓名规则脱敏。
八、加密工具类
这里使用 Hutool 的 AES 工具实现加密和解密。
package com.weige.javaskillpoint.utils;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.symmetric.AES;
import com.weige.javaskillpoint.enums.DesensitizationEnum;
public class AesUtil {
/**
* 示例密钥。
* 生产环境不要硬编码,建议从配置中心、环境变量或密钥管理系统读取。
*/
public static String AES_KEY = "Wk#qerdfdshbd910";
public static AES aes = SecureUtil.aes(AES_KEY.getBytes());
public static Object encrypt(Object obj) {
return aes.encryptHex((String) obj);
}
public static Object decrypt(Object obj) {
return aes.decryptStr((String) obj, CharsetUtil.CHARSET_UTF_8);
}
public static Object decrypt(Object obj, DesensitizationEnum desensitizationEnum) {
Object decrypt = decrypt(obj);
return DesensitizationUtil.desensitization(decrypt, desensitizationEnum);
}
}
这里的流程是:
加密:明文 -> AES 加密 -> Hex 字符串
解密:Hex 字符串 -> AES 解密 -> 明文
解密脱敏:密文 -> 解密 -> 脱敏生产环境中不要直接把密钥写在代码里。
更推荐从配置中心、环境变量或密钥管理系统中读取。
九、脱敏工具类
定义一个简单的脱敏工具类。
package com.weige.javaskillpoint.utils;
import cn.hutool.core.util.StrUtil;
import com.weige.javaskillpoint.enums.DesensitizationEnum;
public class DesensitizationUtil {
public static Object desensitization(Object obj, DesensitizationEnum desensitizationEnum) {
Object result;
switch (desensitizationEnum) {
case name:
result = strUtilHide(obj, 1);
break;
case address:
result = strUtilHide(obj, 3);
break;
default:
result = "";
}
return result;
}
public static Object strUtilHide(String obj, int start, int end) {
return StrUtil.hide(obj, start, end);
}
public static Object strUtilHide(Object obj, int start) {
return strUtilHide((String) obj, start, ((String) obj).length());
}
}
示例效果:
姓名:张三 -> 张*
地址:湖北省武汉市 -> 湖北省****可以根据业务继续扩展手机号、身份证号、邮箱等脱敏规则。
十、加密切面
加密切面用于处理入参对象。
当方法标记了:
@Encryption切面会在方法执行前扫描参数对象中带有:
@EncryptField的字段,并进行加密。
package com.weige.javaskillpoint.aop;
import com.weige.javaskillpoint.annotation.EncryptField;
import com.weige.javaskillpoint.entity.UserBO;
import com.weige.javaskillpoint.utils.AesUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
@Slf4j
@Aspect
@Component
public class EncryptAspect {
@Pointcut("@annotation(com.weige.javaskillpoint.annotation.Encryption)")
public void point() {
}
@Around("point()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
encrypt(joinPoint);
return joinPoint.proceed();
}
public void encrypt(ProceedingJoinPoint joinPoint) {
try {
Object[] args = joinPoint.getArgs();
if (args.length == 0) {
return;
}
for (Object arg : args) {
if (arg instanceof UserBO) {
Field[] fields = arg.getClass().getDeclaredFields();
for (Field field : fields) {
if (!field.isAnnotationPresent(EncryptField.class)) {
continue;
}
field.setAccessible(true);
Object value = field.get(arg);
if (value != null) {
Object encrypt = AesUtil.encrypt(value);
field.set(arg, encrypt);
}
}
}
}
} catch (Exception e) {
log.error("参数加密失败", e);
}
}
}这个切面的核心逻辑是:
Object[] args = joinPoint.getArgs();拿到方法入参。
再通过反射扫描字段:
Field[] fields = arg.getClass().getDeclaredFields();判断字段上是否有:
@EncryptField如果有,就读取字段值并加密后重新写回对象。
十一、解密脱敏切面
解密脱敏切面用于处理方法返回值。
当方法标记了:
@Decryption切面会在方法返回后扫描返回对象中带有:
@DecryptField的字段,先解密,再根据注解配置执行脱敏。
package com.weige.javaskillpoint.aop;
import com.weige.javaskillpoint.annotation.DecryptField;
import com.weige.javaskillpoint.enums.DesensitizationEnum;
import com.weige.javaskillpoint.utils.AesUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
@Slf4j
@Aspect
@Component
public class DecryptAspect {
@Pointcut("@annotation(com.weige.javaskillpoint.annotation.Decryption)")
public void point() {
}
@Around("point()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
return decrypt(joinPoint);
}
public Object decrypt(ProceedingJoinPoint joinPoint) {
Object result = null;
try {
Object obj = joinPoint.proceed();
if (obj != null) {
if (obj instanceof String) {
decryptValue();
} else {
result = decryptData(obj);
}
}
} catch (Throwable e) {
log.error("返回值解密脱敏失败", e);
}
return result;
}
private Object decryptData(Object obj) throws IllegalAccessException {
if (Objects.isNull(obj)) {
return null;
}
if (obj instanceof ArrayList) {
decryptList(obj);
} else {
decryptObj(obj);
}
return obj;
}
private void decryptObj(Object obj) throws IllegalAccessException {
Field[] fields = obj.getClass().getDeclaredFields();
for (Field field : fields) {
if (!field.isAnnotationPresent(DecryptField.class)) {
continue;
}
field.setAccessible(true);
Object value = field.get(obj);
if (value != null) {
String realValue = (String) value;
DesensitizationEnum desensitizationEnum =
field.getAnnotation(DecryptField.class).value();
String result = (String) AesUtil.decrypt(realValue, desensitizationEnum);
field.set(obj, result);
}
}
}
private void decryptList(Object obj) throws IllegalAccessException {
List<Object> result = new ArrayList<>();
if (obj instanceof ArrayList) {
result.addAll((Collection<?>) obj);
}
for (Object item : result) {
decryptObj(item);
}
}
private void decryptValue() {
log.info("当前示例只处理对象返回值,单个 String 暂不处理");
}
}
这个切面支持两类返回值:
单个对象;
ArrayList集合。
如果返回值是集合,则遍历每个对象进行解密脱敏。
十二、Controller 使用示例
1. 加密接口
package com.weige.javaskillpoint.controller;
import com.weige.javaskillpoint.annotation.Encryption;
import com.weige.javaskillpoint.entity.UserBO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/encrypt")
@Slf4j
public class EncryptController {
@PostMapping("/v1")
@Encryption
public UserBO insert(@RequestBody UserBO user) {
log.info("加密后对象:{}", user);
return user;
}
}
请求进入后,切面会在方法执行前对 UserBO 中带有 @EncryptField 的字段进行加密。
2. 解密脱敏接口
package com.weige.javaskillpoint.controller;
import com.weige.javaskillpoint.annotation.Decryption;
import com.weige.javaskillpoint.entity.UserDO;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/decrypt")
public class DecryptController {
@GetMapping("/v1")
@Decryption
public UserDO decrypt() {
return new UserDO(
"7c29e296e92893476db5f9477480ba7f",
"b5c7ff86ac36c01dda45d9ffb0bf73194b083937349c3901f571d42acdaa7bae"
);
}
}
方法返回后,切面会对 UserDO 中带有 @DecryptField 的字段进行解密和脱敏处理。
十三、这种方案的优点
1. 业务代码更干净
Controller 或 Service 中不需要手动调用加密、解密和脱敏逻辑。
只需要在方法和字段上添加注解。
2. 逻辑集中管理
加密、解密、脱敏逻辑集中在切面和工具类中。
后续需要调整规则时,不需要修改大量接口代码。
3. 接入成本低
已有对象只需要增加字段注解。
已有接口只需要增加方法注解。
4. 可扩展性较好
可以继续扩展:
手机号脱敏;
身份证脱敏;
邮箱脱敏;
银行卡脱敏;
不同字段使用不同加密算法;
返回集合、分页对象、嵌套对象处理。
十四、需要注意的问题
1. 示例中只处理了 UserBO
加密切面中有一段判断:
if (arg instanceof UserBO)这意味着当前实现只处理 UserBO。
如果要做成通用能力,建议去掉具体类型判断,改成扫描任意对象中的 @EncryptField 字段。
例如:
if (arg != null) {
encryptObject(arg);
}2. 示例中集合判断只处理了 ArrayList
解密切面中判断的是:
if (obj instanceof ArrayList)实际项目中返回集合可能是:
List;Set;Collection;分页对象;
自定义响应对象。
如果要通用处理,应改成:
if (obj instanceof Collection)并考虑嵌套对象。
3. 不要在日志中打印明文敏感数据
解密后得到的是明文。
如果日志中直接打印对象,可能会泄露敏感信息。
建议对敏感字段统一做日志脱敏,或者避免直接打印完整对象。
4. 密钥不要硬编码
示例中的密钥是:
public static String AES_KEY = "Wk#qerdfdshbd910";生产环境不要这样写。
建议从以下位置读取:
环境变量;
配置中心;
KMS;
加密配置文件。
5. 加密字段不适合直接模糊查询
加密后的字段不再保留原始明文特征。
如果需要按手机号、姓名、地址进行模糊查询,需要单独设计方案,例如:
增加 Hash 字段用于精确查询;
增加脱敏字段用于展示;
建立搜索索引;
使用专门的可搜索加密方案。
6. AOP 方案更适合接口层处理
本文这种 AOP 方案适合处理请求入参和接口返回值。
如果目标是数据库字段级加密,更推荐在 MyBatis 拦截器、TypeHandler 或 ORM 层处理。
因为这样可以更靠近数据入库和出库过程,避免部分业务入口绕过 AOP。
十五、适用场景
这种方案适合:
接口入参加密前置处理;
接口返回值解密脱敏;
管理后台敏感信息展示;
用户信息查询结果脱敏;
希望通过注解减少重复代码的项目。
尤其适合这类需求:
数据入库前加密
数据返回前解密脱敏
业务代码尽量不写重复处理逻辑十六、不适合的场景
这种方案不太适合:
极高性能要求的接口;
需要复杂嵌套对象自动处理的场景;
所有数据库访问都必须强制加密的场景;
需要密钥轮换、审计、合规管控的强安全场景;
需要对加密字段做模糊查询、排序、分组的场景。
如果是强安全要求的数据存储,建议结合:
MyBatis 拦截器;
数据库字段级加密;
KMS 密钥管理;
日志脱敏;
访问审计;
权限控制。
结论
核心思路是:
使用
@EncryptField标记需要加密的字段;使用
@DecryptField标记需要解密脱敏的字段;使用
@Encryption标记需要加密处理的方法;使用
@Decryption标记需要解密脱敏处理的方法;在 AOP 切面中通过反射扫描字段注解;
对请求参数执行加密;
对返回结果执行解密和脱敏。
这种方式可以减少重复代码,让加密脱敏逻辑集中管理。
如果只是想在接口入参和返回值层面统一处理敏感数据,可以使用注解 + 反射 + AOP;
如果要做真正的数据入库和出库字段级加密,更建议把能力下沉到 MyBatis 拦截器或 ORM 层。