# sensitive-data-encryption **Repository Path**: pengmqqq/sensitive-data-encryption ## Basic Information - **Project Name**: sensitive-data-encryption - **Description**: 后端敏感数据加密的一些解决方案,包括: 1. 配置文件敏感数据加解密 2. 前端传输敏感数据加解密 3. 数据库获取的敏感数据加解密 - **Primary Language**: Java - **License**: MulanPSL-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2024-08-27 - **Last Updated**: 2024-08-27 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # sensitive-data-encryption #### 介绍 后端敏感数据加密的一些解决方案,包括: 1. 配置文件敏感数据加解密 2. 前端传输敏感数据加解密 3. 数据库获取的敏感数据加解密 #### 软件架构 配置文件数据脱敏: Jasypt + AES 前后端传输以及数据库存储数据脱敏:AOP + AES #### 使用说明 1. 配置文件数据脱敏 将需要脱敏的数据进行加密之后再放入配置文件(注意要使用解密算法配套的加密算法)例如: ~~~yaml test: password: Enc(tKWTx+XSlLlJFdLOIQPKYQ==) ~~~ 2. 前后端传输以及数据库存储数据脱敏: 在需要加密/解密的属性/参数上增加注解 @EncryptField ~~~java @Data @Accessors(chain = true) public class User { private Integer id; private String name; @EncryptField private String phone; @EncryptField private String email; private Integer age; } ~~~ 在需要对参数加密的方法上增加注解 @NeedEncrypt ~~~java @NeedEncrypt public void addAll(List user) { users.addAll(user); System.out.println(""); } ~~~ 在需要对返回值解密的方法上增加注解 @NeedDecrypt 例如某些需要访问第三方平台的操作,从数据库取到的是加密的数据,代码中需要进行解密再发送给第三方平台进行认证 ~~~java @NeedDecrypt public List findAll() { ArrayList list = new ArrayList<>(users); return list; } #### 实现方案 ##### 配置文件数据脱敏: - pom文件引入依赖: ~~~xml org.springframework.boot spring-boot-starter-aop org.projectlombok lombok true com.github.ulisesbocchio jasypt-spring-boot-starter 3.0.4 ~~~ - 配置文件 application.yml 新增jasypt相关配置: ~~~yaml jasypt: encryptor: property: # 算法识别的前后缀,默认ENC(),包含在前后缀的加密信息,会使用指定算法解密 prefix: Enc( suffix: ) bean: desencrypt # 指定自定义加密算法的bean test: password: Enc(tKWTx+XSlLlJFdLOIQPKYQ==) # 加密数据 ~~~ - 新增自定义算法类: ~~~java @Component("desencrypt") public class JasyptAlgorithmConfig implements StringEncryptor { @Override public String encrypt(String message) { return AESUtils.encrypt(message,AESUtils.getKey()); } @Override public String decrypt(String encryptedMessage) { return AESUtils.decrypt(encryptedMessage,AESUtils.getKey()); } ~~~ - 新增加密工具类: ~~~java public class AESUtils { private static final String USER_PWD_KEY = "A39DSSSDFGS4OaHr"; private static final Charset CHARSET = Charset.forName("UTF-8"); // 加密算法名称 private static final String AES = "AES"; // 偏移量-AES 128位数据块对应偏移量为16位字符串 private static final String IV = "70w5zbOds3DSFA5C"; // AES-加密方式, CBC-工作模式,PKCS5Padding-填充模式 private static final String AES_CBC_PKCS5PADDING = "AES/CBC/PKCS5Padding"; /** * AES 加密操作 * * @param content 待加密内容 * @param key 加密密钥 * @return 返回Base64转码后的加密数据 */ public static String encrypt(String content, String key) { if (StringUtils.isEmpty(content)) { return content; } try { /* * 新建一个密码编译器的实例,由三部分构成,用"/"分隔,分别代表如下 * 1. 加密的类型(如AES,DES,RC2等) * 2. 模式(AES中包含ECB,CBC,CFB,CTR,CTS等) * 3. 补码方式(包含nopadding/PKCS5Padding等等) * 依据这三个参数可以创建很多种加密方式 */ Cipher cipher = Cipher.getInstance(AES_CBC_PKCS5PADDING); //偏移量 IvParameterSpec zeroIv = new IvParameterSpec(IV.getBytes(CHARSET)); byte[] byteContent = content.getBytes(CHARSET); //使用加密秘钥 SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes(CHARSET), AES); //SecretKeySpec skeySpec = getSecretKey(key); cipher.init(Cipher.ENCRYPT_MODE, skeySpec, zeroIv); // 初始化为加密模式的密码器 byte[] result = cipher.doFinal(byteContent); // 加密 return Base64.encodeBase64String(result); //通过Base64转码返回 } catch (Exception ex) { throw new RuntimeException(ex); } } /** * AES 解密操作 * * @param content * @param key * @return */ public static String decrypt(String content, String key) { if (StringUtils.isEmpty(content)) { return content; } try { Cipher cipher = Cipher.getInstance(AES_CBC_PKCS5PADDING); IvParameterSpec zeroIv = new IvParameterSpec(IV.getBytes(CHARSET)); SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes(CHARSET), AES); //SecretKeySpec skeySpec = getSecretKey(key); cipher.init(Cipher.DECRYPT_MODE, skeySpec, zeroIv); byte[] result = cipher.doFinal(Base64.decodeBase64(content)); return new String(result, CHARSET); } catch (Exception ex) { throw new RuntimeException(ex); } } public static String getKey(){ return USER_PWD_KEY; } } ~~~ ##### 前后端接口以及数据库存储数据加密: - 新增三个注解 - @EncryptField ~~~java /** * 安全字段注解 * 加在需要加密/解密的属性/参数上 * 实现自动加密解密 */ @Target({ElementType.FIELD,ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) public @interface EncryptField { String[] value() default ""; } ~~~ - @NeedDecrypt ~~~java /** * 安全字段注解 * 加在需要解密的方法参数上 * 实现自动解密 */ @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface NeedDecrypt { } ~~~ - @NeedEncrypt ~~~java /** * 安全字段注解 * 加在需要加密的方法参数上 * 实现自动加密 */ @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface NeedEncrypt { } ~~~ - 新增两个切面类 - EncryptAspect ~~~java /** * @author 17540 * 对加了@NeedEncrypt注释的方法的参数进行扫描,参数中存在@EncryptFild修饰的加密字段,则进行加密 * 当前只适配非嵌套对象参数,List参数,普通String参数 */ @Slf4j @Aspect @Component public class EncryptAspect { @Pointcut("@annotation(com.example.encryption.common.anno.NeedEncrypt)") public void pointCut() { } @Around("pointCut()") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { //加密 return joinPoint.proceed(encrypt(joinPoint)); } public Object[] encrypt(ProceedingJoinPoint joinPoint) { Object[] objects=null; try { objects = joinPoint.getArgs(); if (objects.length != 0) { for (int i = 0; i < objects.length; i++) { //抛砖引玉 ,可自行扩展其他类型字段的判断 if (objects[i] instanceof String) { String value = encryptValue(objects[i]); objects[i] = value; } else { encryptData(objects[i]); } } } } catch (Exception e) { e.printStackTrace(); } return objects; } private void encryptData(Object obj) throws IllegalAccessException { if (Objects.isNull(obj)) { return; } if (obj instanceof ArrayList) { encryptList(obj); } else { encryptObj(obj); } } /** * 加密对象 * @param obj * @throws IllegalAccessException */ private void encryptObj(Object obj) throws IllegalAccessException { if (Objects.isNull(obj)) { log.info("当前需要加密的object为null"); return; } Field[] fields = obj.getClass().getDeclaredFields(); for (Field field : fields) { boolean containEncryptField = field.isAnnotationPresent(EncryptField.class); if (containEncryptField) { //获取访问权 field.setAccessible(true); if (field.get(obj) == null) { continue; } String value = AESUtils.encrypt(String.valueOf(field.get(obj)), AESUtils.getKey()); field.set(obj, value); } } } /** * 针对list<实体来> 进行反射、解密 * @param obj * @throws IllegalAccessException */ private void encryptList(Object obj) throws IllegalAccessException { List result = new ArrayList<>(); if (obj instanceof ArrayList) { result.addAll((List) obj); } for (Object object : result) { encryptObj(object); } obj = result; } /** * 加密单个值 * @param realValue * @return */ public String encryptValue(Object realValue) { if (Objects.isNull(realValue)) { return null; } try { realValue = AESUtils.encrypt(String.valueOf(realValue), AESUtils.getKey()); } catch (Exception e) { log.info("加密异常={}",e.getMessage()); } return String.valueOf(realValue); } } - DecryptAspect ~~~java /** * @author 17540 * 对加了@NeedEncrypt注释的方法的参数进行扫描,参数中存在@EncryptFild修饰的加密字段,则进行加密 * 当前只适配非嵌套对象参数,List参数,普通String参数 */ @Slf4j @Aspect @Component public class DecryptAspect { @Pointcut("@annotation(com.example.encryption.common.anno.NeedDecrypt)") public void pointCut() { } @Around("pointCut()") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { //解密 Object result = decrypt(joinPoint); return result; } public Object decrypt(ProceedingJoinPoint joinPoint) { Object result = null; try { Object obj = joinPoint.proceed(); if (obj != null) { //抛砖引玉 ,可自行扩展其他类型字段的判断 if (obj instanceof String) { result = decryptValue(obj); } else { result = decryptData(obj); } } } catch (Throwable e) { e.printStackTrace(); } 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; } /** * 针对单个实体类进行 解密 * @param obj * @throws IllegalAccessException */ private void decryptObj(Object obj) throws IllegalAccessException { Field[] fields = obj.getClass().getDeclaredFields(); for (Field field : fields) { boolean hasSecureField = field.isAnnotationPresent(EncryptField.class); if (hasSecureField) { field.setAccessible(true); String realValue = (String) field.get(obj); if (Objects.isNull(realValue)) { continue; } try{ String value = AESUtils.decrypt(realValue, AESUtils.getKey()); field.set(obj, value); log.info("解密后={}", value); }catch (Exception e){ log.error("解密{}异常=,{}",realValue, e.getMessage()); } } } } /** * 针对list<实体来> 进行反射、解密 * @param obj * @throws IllegalAccessException */ private void decryptList(Object obj) throws IllegalAccessException { List result = new ArrayList<>(); if (obj instanceof ArrayList) { result.addAll((List) obj); } for (Object object : result) { decryptObj(object); } obj = result; } public String decryptValue(Object realValue) { try { realValue = AESUtils.decrypt(String.valueOf(realValue), AESUtils.getKey()); } catch (Exception e) { log.info("解密{}异常={}",realValue, e.getMessage()); } return String.valueOf(realValue); } } ~~~