赞
踩
封装了一个前后端传参敏感数据加解密小工具,直接通过AOP+注解完成,在项目中亲测有效,特点包括:
import org.apache.commons.codec.binary.Base64; import java.nio.charset.StandardCharsets; import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import cn.hutool.core.text.CharSequenceUtil; import lombok.CustomLog; import static com.uih.uplus.mti.tm.biz.utils.errorcode.ServiceErrorCode.TM_ILLEGAL_PARAMETER; /** * @author * @description AES/CBC/PKCS5Padding加解密工具类 * @date 2023/5/25 14:02 */ @CustomLog public class AesUtils { /** * 算法/模式/补码方式 */ private static final String MODE_METHOD = "AES/CBC/PKCS5Padding"; /** * 算法 */ private static final String ALGORITHM_NAME = "AES"; /** * AES算法加密明文 * * @date: 2023/5/25 14:01 * @author: */ public static String encryptAES(String data, String key, String iv) { //若字段为 null/"" 直接返回 if (CharSequenceUtil.isEmpty(data)) { return data; } //加密 try { SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), ALGORITHM_NAME); Cipher cipher = Cipher.getInstance(MODE_METHOD); //使用CBC模式,需要一个向量iv,可增加加密算法的强度 IvParameterSpec ivParameterSpec = new IvParameterSpec(iv.getBytes(StandardCharsets.UTF_8)); cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivParameterSpec); byte[] encrypted = cipher.doFinal(data.getBytes()); return encode(encrypted); } catch (Exception e) { throw new AppException(TM_ILLEGAL_PARAMETER); } } /** * AES算法解密密文 * * @date: 2023/5/25 13:55 * @author: */ public static String decryptAES(String data, String key, String iv) { //若字段为 null/"" 直接返回 if (CharSequenceUtil.isEmpty(data)) { return data; } //解密 try { Cipher cipher = Cipher.getInstance(MODE_METHOD); SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), ALGORITHM_NAME); IvParameterSpec ivParameterSpec = new IvParameterSpec(iv.getBytes(StandardCharsets.UTF_8)); cipher.init(Cipher.DECRYPT_MODE, keySpec, ivParameterSpec); byte[] original = cipher.doFinal(decode(data)); String originalString = new String(original); return originalString.trim(); } catch (Exception e) { throw new AppException(TM_ILLEGAL_PARAMETER); } } /** * BASE64编码 * * @date: 2023/5/25 13:54 * @author: */ public static String encode(byte[] byteArray) { return Base64.encodeBase64String(byteArray); } /** * BASE64解码 * * @date: 2023/5/25 13:54 * @author: */ public static byte[] decode(String base64EncodedString) { return Base64.decodeBase64(base64EncodedString); } }
import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * @description: 针对方法是否开启加解密注解 * 该注解是一个普通的自定义注解,不需要和其他任何注解组合使用,也不受其他任何注解的干扰 * 该注解的主要功能是针对入参所有字段中,对与字符串数组decryptParams中同名的字段,尝试对该其进行解密操作 * 针对出参的所有字段,对与字符串数组encryptParams中同名的字段,尝试对其进行加密操作 * 注意: * 无论待加解密的对象被封装成自定义对象、分页类型、列表类型,AOP方法都会将其一层层剥离,最终仅针对具体的字符串字段做加解密操作 * 待解密的字段前后端参数名称必须保持一致(即不能使用@RequestParam注解去处理前后端参数名称不一致的情况) * 若形参包括自定义类型对象,则该对象名称不能与类型中的成员变量重名 * 若待解密/加密字段为空值,会直接忽略该字段不做解密/加密处理,后端不会抛出任何异常 * 若在后端本因被解密的字段,前端传递过来却没有加密,后端不会抛异常,只会在控制台输出错误日志,并将其作为明文处理与使用(增加弹性) * * @date: 2023/4/1 14:52 * @author: */ @Retention(RetentionPolicy.RUNTIME) @Target(value = {ElementType.METHOD}) public @interface EncryptMethod { /** * 从前端传递的参数中,需要解密的字段列表 */ String[] decryptParams() default {}; /** * 向前端传递的参数中,需要加密的字段列表 */ String[] encryptParams() default {}; }
import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.uih.uplus.common.utils.result.Result; import com.uih.uplus.mti.tm.biz.utils.AesUtils; import com.uih.uplus.mti.tm.biz.utils.AppException; import com.uih.uplus.mti.tm.biz.utils.TMPage; import com.uih.uplus.mti.tm.biz.utils.annotation.encrypt.EncryptMethod; import com.uih.uplus.mti.tm.biz.utils.uap.utils.TokenUtil; 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.aspectj.lang.reflect.MethodSignature; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; import javax.servlet.http.HttpServletRequest; import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.convert.Convert; import cn.hutool.crypto.digest.MD5; import lombok.CustomLog; import static com.uih.uplus.mti.tm.biz.utils.errorcode.ServiceErrorCode.DECRYPT_FAILED; import static com.uih.uplus.mti.tm.biz.utils.errorcode.ServiceErrorCode.ENCRYPT_FAILED; import static com.uih.uplus.mti.tm.biz.utils.errorcode.ServiceErrorCode.NO_SUPPORT_DECRYPT_TYPE; /** * @description: 入参解密/出参加密 * 支持形参中的待解密对象为:自定义实体(实际解密实体中的指定字符串成员变量)、普通字符串、普通字符串列表 * 支持返回值的待加密对象为:单个自定义实体、自定义实体列表、自定义实体分页,实际加密的是每个自定义实体中的指定字段 * @date: 2023/5/16 14:42 * @author: */ @Component @Aspect @CustomLog public class EncryptAop { //基于前端传递的token动态生成密钥和偏移量 @Autowired private TokenUtil tokenUtil; /** * 切入点 * * @date: 2023/5/17 20:13 * @author: */ @Pointcut("@annotation(com.uih.uplus.mti.tm.biz.utils.annotation.encrypt.EncryptMethod)") public void pointCut() { } /** * 环绕通知 * * @date: 2023/5/16 16:37 * @author: */ @Around("pointCut()") public Object aroundEncrypt(ProceedingJoinPoint pjp) throws Throwable { //第一步:获取当前被@EncryptMethod注解的方法对象、注解对象 MethodSignature signature = (MethodSignature)pjp.getSignature(); Method method = pjp.getTarget().getClass().getMethod(signature.getName(), signature.getParameterTypes()); EncryptMethod annotation = method.getAnnotation(EncryptMethod.class); //第二步:对入参敏感字段解密 decrypt(pjp, method, annotation); //第三步:执行请求处理接口 Object response = pjp.proceed(pjp.getArgs()); //第四步:对出参敏感字段加密,并返回 encrypt(annotation, response); return response; } /** * 对入参做解密 * 支持对入参的自定义实体的字符串类型的成员变量、普通字符串形参的解密 * * @date: 2023/5/17 20:13 * @author: */ private void decrypt(ProceedingJoinPoint pjp, Method method, EncryptMethod annotation) { String key = MD5.create().digestHex(tokenUtil.getToken().jwtString); String iv = MD5.create().digestHex16(tokenUtil.getToken().jwtString); //获取注解的参数值 List<String> decryptParam = Lists.newArrayList(annotation.decryptParams()); ServletRequestAttributes sra = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes(); if (sra != null) { HttpServletRequest request = sra.getRequest(); //获取请求参数 try { Object[] args = pjp.getArgs(); Parameter[] params = method.getParameters(); for (int i = 0; i < args.length; i++) { //判断当前形参是否是自定义实体类型,判断依据: //针对自定义实体类型,接口参数的名称一般与Params或Body中的变量名称不一致 //而非自定义实体类型,一般接口参数的名称与Params或Body中的变量名称是一致的 //若该形参是自定义实体类 if (Boolean.FALSE.equals(getKey(request, params[i].getName()))) { Field[] declaredFields = args[i].getClass().getDeclaredFields(); for (Field field : declaredFields) { if (decryptParam.contains(field.getName())) { field.setAccessible(true); field.set(args[i], AesUtils.decryptAES((String)field.get(args[i]), key, iv)); } } } else { //若该形参是非自定义实体 if (decryptParam.contains(params[i].getName())) { //若该参数是字符串列表 if (args[i] instanceof List && Objects.nonNull(args[i]) && (CollUtil.isNotEmpty((List<?>)args[i])) && (((List<?>)args[i]).get(0) instanceof String)) { args[i] = Convert.toList(String.class, args[i]).stream().map(x -> AesUtils.decryptAES(x, key, iv)) .collect(Collectors.toList()); } else if (args[i] instanceof String) { //若该参数是普通字符串 args[i] = AesUtils.decryptAES(pjp.getArgs()[i].toString(), key, iv); } else { //暂不支持其他类型形参的解密(如Map、Set、Long等) throw new AppException(NO_SUPPORT_DECRYPT_TYPE); } } } } } catch (Exception e) { logger.error(e.getMessage()); throw new AppException(DECRYPT_FAILED); } } } /** * 对出参对象的指定字段加密(仅支持分页、列表、自定义实体类型) * * @date: 2023/5/17 14:48 * @author: */ private void encrypt(EncryptMethod annotation, Object response) { if (!Objects.isNull(response) && (response instanceof Result<?>)) { Result<?> result = (Result<?>)response; if (result.getData() instanceof TMPage<?>) { //若返回值为分页类型(TMPage是当前项目的分页类型,其他项目可能有自己的分页类型) TMPage<?> page = (TMPage<?>)result.getData(); page.getRecords().forEach(x -> doEncryptObject(x, Lists.newArrayList(annotation.encryptParams()))); } else if (result.getData() instanceof List<?>) { //若返回值为列表类型 List<?> list = (List<?>)result.getData(); list.forEach(x -> doEncryptObject(x, Lists.newArrayList(annotation.encryptParams()))); } else { //其他(默认为自定义实体对象) doEncryptObject(result.getData(), Lists.newArrayList(annotation.encryptParams())); } } } /** * 仅针对自定义实体中的敏感字段加密 * * @date: 2023/5/17 14:19 * @author: */ private void doEncryptObject(Object object, List<String> encryptParam) { //若加密字段数组为空,直接返回,杜绝了返回值位Boolean的情况 if (Objects.isNull(object) || CollUtil.isEmpty(encryptParam)) { return; } String key = MD5.create().digestHex(tokenUtil.getToken().jwtString); String iv = MD5.create().digestHex16(tokenUtil.getToken().jwtString); Field[] declaredFields = object.getClass().getDeclaredFields(); HashMap<Object, Object> cipherMap = Maps.newHashMap(); try { for (Field field : declaredFields) { if (encryptParam.contains(field.getName())) { field.setAccessible(true); String cipherText = (String)field.get(object); cipherText = AesUtils.encryptAES(cipherText, key, iv); cipherMap.put(field.getName(), cipherText); } } BeanUtil.copyProperties(cipherMap, object); } catch (Exception e) { logger.error(e.getMessage()); throw new AppException(ENCRYPT_FAILED); } } /** * 根据前后端参数名称是否一致判断参数类型 * 若名称一致则为非自定义类型 * 若名称不一致则为自定义类型 * * @date: 2023/5/16 15:34 * @author: */ public Boolean getKey(HttpServletRequest request, String name) { Map<String, String[]> map = request.getParameterMap(); for (Map.Entry<String, String[]> element : map.entrySet()) { if (element.getKey().equals(name)) { return true; } } return false; } }
@RestController @RequestMapping(value = "/app/patient") public class AppPatientController { @Autowired private AppPatientService appPatientService; /** * 分页查询/患者列表信息 * * @date: 2023/4/10 13:45 * @author: */ @GetMapping("/page") @EncryptMethod(decryptParams = {"keyword"}, encryptParams = {"name", "patientIdNo"}) public Result<TMPage<InpatientVO>> patientList(@Valid InpatientListDTO inpatientListDTO) { return RestResult.success(appPatientService.patientList(inpatientListDTO)); } }
入参:
/**
* @Description 患者列表分页查询请求参数
* @Date 2022/10/19 10:53
* @Author
*/
@Data
public class InpatientListDTO {
//查询类型:0最近访问 1本院急诊科 2全域患者
private Integer queryType;
//搜索关键字(App端使用): 支持身份证号码右模糊、门诊号右模糊、患者姓名全模糊
private String keyword;
......
}
出参:
/**
* @Description 查询患者住院信息返回对象
* @Date 2022/10/19 10:53
* @Author
*/
@Data
public class InpatientVO {
//患者治疗周期信息表ID
private String patientPeriodId;
//患者姓名
private String name;
//身份证号
private String patientIdNo;
......
}
其实上面的整个加解密方法是有缺陷的,比如若存在类嵌套,敏感字段藏得比较深的话,上面的方法就不适用了。当然也有补救办法,就是如果嵌套的类对象中有敏感字段,就将该对象整体加解密,本文没有这样做的原因是项目中并不存在这种情况。
实质上AOP+注解实现前后端传参加解密的方式有很多种思路,根据不同的需求和考量,可以从以下几个方面着手:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。