最近经手一个项目,不允许明文存储敏感数据(例如车架号、车牌等),可以通过自定义注解和spring-boot切面来实现敏感字段加解密。
用法大致如下:
1、自定义注解
package com.zzz.yyy.annotation;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import java.lang.annotation.*;
/**
* @author: zxy
* @desc: 加在敏感字段字段上,实现自动解密/加密
* @date: 2020/4/29 9:30
*/
@Documented
@Target({
ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Order(Ordered.HIGHEST_PRECEDENCE)
public @interface SensitiveField {
}
2、加密解密工具类
package com.zzz.yyy.util
import com.jn.ssr.oms.order.annotation.SensitiveField;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.AnnotationUtils;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.security.SecureRandom;
import java.util.List;
/**
* AES加密解密工具类
*/
@Slf4j
public class AESUtil {
private static final String defaultCharset = "UTF-8";
private static final String KEY_AES = "AES";
public static final String KEY = "something4u";
/**
* 加密
*
* @param data 需要加密的内容
* @param key 加密密码
* @return
*/
public static String encrypt(String data, String key) {
return doAES(data, key, Cipher.ENCRYPT_MODE);
}
/**
* 解密
*
* @param data 待解密内容
* @param key 解密密钥
* @return
*/
public static String decrypt(String data, String key) {
return doAES(data, key, Cipher.DECRYPT_MODE);
}
/**
* 加解密
*
* @param data 待处理数据
* @param mode 加解密mode
* @return
*/
private static String doAES(String data, String key, int mode) {
try {
if (data == null || "".equals(data) || key == null || "".equals(key)) {
return null;
}
boolean encrypt = mode == Cipher.ENCRYPT_MODE;
byte[] content;
if (encrypt) {
content = data.getBytes(defaultCharset);
} else {
content = parseHexStr2Byte(data);
}
KeyGenerator kgen = KeyGenerator.getInstance(KEY_AES);
kgen.init(128, new SecureRandom(key.getBytes()));
SecretKey secretKey = kgen.generateKey();
byte[] enCodeFormat = secretKey.getEncoded();
SecretKeySpec keySpec = new SecretKeySpec(enCodeFormat, KEY_AES);
// 创建密码器
Cipher cipher = Cipher.getInstance(KEY_AES);
// 初始化
cipher.init(mode, keySpec);
byte[] result = cipher.doFinal(content);
if (encrypt) {
return parseByte2HexStr(result);
} else {
return new String(result, defaultCharset);
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 将二进制转换成16进制
*
* @param buf
* @return
*/
private static String parseByte2HexStr(byte buf[]) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < buf.length; i++) {
String hex = Integer.toHexString(buf[i] & 0xFF);
if (hex.length() == 1) {
hex = '0' + hex;
}
sb.append(hex.toUpperCase());
}
return sb.toString();
}
/**
* 将16进制转换为二进制
*
* @param hexStr
* @return
*/
private static byte[] parseHexStr2Byte(String hexStr) {
if (hexStr.length() < 1) {
return null;
}
byte[] result = new byte[hexStr.length() / 2];
for (int i = 0; i < hexStr.length() / 2; i++) {
int high = Integer.parseInt(hexStr.substring(i * 2, i * 2 + 1), 16);
int low = Integer.parseInt(hexStr.substring(i * 2 + 1, i * 2 + 2), 16);
result[i] = (byte) (high * 16 + low);
}
return result;
}
/**
* 进行敏感字段解密/加密处理
*/
public static void handleItem(Object item, String secretKey, boolean isEncrypted) throws IllegalAccessException {
// 捕获类中的所有字段
Field[] fields = item.getClass().getDeclaredFields();
// 遍历所有字段
for (Field field : fields) {
// 若该字段被SecurityParameter注解,则进行解密/加密
Class<?> fieldType = field.getType();
if (fieldType == String.class) {
if (null != AnnotationUtils.findAnnotation(field, SensitiveField.class)) {
// 设置private类型允许访问
field.setAccessible(Boolean.TRUE);
handleField(item, field, secretKey, isEncrypted);
field.setAccessible(Boolean.FALSE);
}
} else if (List.class.isAssignableFrom(field.getType())) {
Type genericType = field.getGenericType();
// 是否参数化类型
if (genericType instanceof ParameterizedType) {
ParameterizedType pt = (ParameterizedType) genericType;
Class genericClazz = (Class) pt.getActualTypeArguments()[0];
if (genericClazz == String.class && null != AnnotationUtils.findAnnotation(field, SensitiveField.class)) {
field.setAccessible(Boolean.TRUE);
List list = (List) field.get(item);
if(list != null && !list.isEmpty()){
for (int i=0; i < list.size(); i++) {
if (isEncrypted) {
list.set(i, AESUtil.decrypt((String) list.get(i), secretKey));
} else {
list.set(i, AESUtil.encrypt((String) list.get(i), secretKey));
}
}
}
field.setAccessible(Boolean.FALSE);
} else {
Field[] subFields = genericClazz.getDeclaredFields();
for (Field subField : subFields) {
if (subField.getType() == String.class && null != AnnotationUtils.findAnnotation(subField, SensitiveField.class)) {
field.setAccessible(Boolean.TRUE);
List list = (List) field.get(item);
if(list != null && !list.isEmpty()) {
for (Object subObj : list) {
subField.setAccessible(Boolean.TRUE);
handleField(subObj, subField, secretKey, isEncrypted);
subField.setAccessible(Boolean.FALSE);
}
field.setAccessible(Boolean.FALSE);
}
}
}
}
}
} else if(fieldType.getName().startsWith("com.jn.ssr")){
Field[] subFields = fieldType.getDeclaredFields();
for (Field subField : subFields) {
if (subField.getType() == String.class && null != AnnotationUtils.findAnnotation(subField, SensitiveField.class)) {
field.setAccessible(Boolean.TRUE);
Object obj = field.get(item);
subField.setAccessible(Boolean.TRUE);
handleField(obj, subField, secretKey, isEncrypted);
subField.setAccessible(Boolean.FALSE);
field.setAccessible(Boolean.FALSE);
}
}
}
// T responseData由于泛型擦除,通过反射无法直接获取到实际类型
else if(fieldType == Object.class){
field.setAccessible(Boolean.TRUE);
handleItem(field.get(item), secretKey, isEncrypted);
field.setAccessible(Boolean.FALSE);
}
}
}
private static void handleField(Object item, Field field, String secretKey, boolean isEncrypted) throws IllegalAccessException {
if(null == item){
return;
}
if (isEncrypted) {
field.set(item, AESUtil.decrypt((String) field.get(item), secretKey));
} else {
field.set(item, AESUtil.encrypt((String) field.get(item), secretKey));
}
}
}
3、自定义切面
package com.zzz.yyy.advice;
import com.google.common.collect.Lists;
import com.jn.ssr.oms.order.tools.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.springframework.stereotype.Component;
import java.util.List;
/**
* 敏感字段解密/加密切面
*
* @Author: zxy
* @date: 2020/4/29 9:30
*/
@Component
@Aspect
@Slf4j
public class SensitiveFieldAspect {
@Around("execution(* com.zzz.yyy.controller.RtmController.*(..))")
public Object doProcess(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
// 捕获方法参数列表
List<Object> methodArgs = this.getMethodArgs(proceedingJoinPoint);
// 循环所有参数项
for (Object item : methodArgs) {
// 对参数项进行敏感字段解密处理
AESUtil.handleItem(item, AESUtil.KEY,true);
}
Object result = proceedingJoinPoint.proceed();
// 对返回值进行敏感字段加密处理
AESUtil.handleItem(result, AESUtil.KEY, false);
return result;
}
/**
* 获取方法的请求参数
*/
private List<Object> getMethodArgs(ProceedingJoinPoint proceedingJoinPoint) {
List<Object> methodArgs = Lists.newArrayList();
for (Object arg : proceedingJoinPoint.getArgs()) {
if (null != arg) {
methodArgs.add(arg);
}
}
return methodArgs;
}
}
5、使用该切面:加在敏感字段字段上,实现自动解密/加密
package com.zzz.yyy.rtm.entity;
import com.jn.ssr.oms.order.annotation.SecurityParameter;
import lombok.Data;
@Data
public class Vehicle {
/**
* 车架号
*/
@SecurityParameter
String vin;
/**
* 生产日期 格式:YYYY-MM-DD
*/
String mfgDate;
/**
* 车牌
*/
@SecurityParameter
String plateNo;
/**
* 车型
*/
String model;
/**
* 汽车经销商
*/
String sellingDealer;
/**
* 销售类型
*/
String salesType;
}