在实际生产项目中,经常需要对如身份证信息、手机号、银行卡号等的敏感数据进行加密数据库存储,但在业务代码中对敏感信息进行手动加解密则十分不优雅,甚至会存在错加密、漏加密、业务人员需要知道实际的加密规则等的情况。本文将介绍使mybatis拦截器+自定义注解的形式对敏感数据进行存储前拦截加密的详细过程。
1
Mybatis
Plugin 介绍
MyBatis 允许你在已映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:
//语句执行拦截
Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
// 参数获取、设置时进行拦截
ParameterHandler (getParameterObject, setParameters)
// 对返回结果进行拦截
ResultSetHandler (handleResultSets, handleOutputParameters)
//sql语句拦截
StatementHandler (prepare, parameterize, batch, update, query)
简而言之,即在执行sql的整个周期中,我们可以任意切入到某一点对sql的参数、sql执行结果集、sql语句本身等进行切面处理。基于这个特性,我们便可以使用其对我们需要进行加密的数据进行切面统一加密处理了(分页插件 pageHelper 就是这样实现数据库分页查询的)。
2 基于注解方式对敏感信息加解密拦截器
2.1 实现思路
对应数据的加密我们用ParameterHandler,解密用ResultSetHandler
目前字段需要灵活变更,所以需要用注解的方式判断哪些字段需要加密
mybatis的interceptor接口有以下方法需要实现
public interface Interceptor {
//主要参数拦截方法
Object intercept(Invocation invocation) throws Throwable;
//mybatis插件链 必须实现加入到拦截中
default Object plugin(Object target) {return Plugin.wrap(target, this);}
//自定义插件配置文件方法 可为空
default void setProperties(Properties properties) {}
}
2.2 定义需要加解密的敏感字段注解
定义类的注解
/**
* 注解敏感信息类的注解
*/
@Inherited
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveData {
}
定义字段的注解
/**
* 注解敏感信息类中敏感字段的注解
*/
@Inherited
@Target({ ElementType.Field })
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveField {
}
2.3 定义加解密管理类
/**
* 利用注解对字段进行加密管理类
*/
@Component
public class AESManager {
@Value("${sensitive.data.encryption}")
private String encryptionKey;
/**
* 解密
* @param result
* @param <T>
* @return
* @throws Exception
*/
public <T> T decrypt(T result) throws Exception {
//取出resultType的类
Class<?> resultClass = result.getClass();
Field[] declaredFields = resultClass.getDeclaredFields();
for (Field field : declaredFields) {
//取出所有被EncryptDecryptField注解的字段
SensitiveField sensitiveField = field.getAnnotation(SensitiveField.class);
if (!Objects.isNull(sensitiveField)) {
field.setAccessible(true);
Object object = field.get(result);
//只支持String的解密
if (object instanceof String) {
String value = (String) object;
//对注解的字段进行逐一解密
//对于纯数字字符不解密
if(!StringUtils.isNumeric(value)){
field.set(result, AESUtils.decryptAES(value,encryptionKey));
}
}
}
}
return result;
}
/**
* 加密
* @param declaredFields
* @param paramsObject
* @param <T>
* @return
* @throws Exception
*/
public <T> T encrypt(Field[] declaredFields, T paramsObject) throws Exception {
for (Field field : declaredFields) {
//取出所有被EncryptDecryptField注解的字段
SensitiveField sensitiveField = field.getAnnotation(SensitiveField.class);
if (!Objects.isNull(sensitiveField)) {
field.setAccessible(true);
Object object = field.get(paramsObject);
//暂时只实现String类型的加密
if (object instanceof String) {
String value = (String) object;
//加密 这里我使用自定义的AES加密工具
field.set(paramsObject, AESUtils.encryptAES(value, encryptionKey));
}
}
}
return paramsObject;
}
}
/**
* AES加密解密工具类
*/
public class AESUtils {
/**
* 解密
* @param content 加密内容
* @param key
* @return
*/
public static String decryptAES(String content, String key) {
if(StringUtils.isBlank(content)){
return null;
}
byte[] byteRresult = new byte[content.length() / 2];
for (int i = 0; i < content.length() / 2; i++) {
int high = Integer.parseInt(content.substring(i * 2, i * 2 + 1), 16);
int low = Integer.parseInt(content.substring(i * 2 + 1, i * 2 + 2), 16);
byteRresult[i] = (byte) (high * 16 + low);
}
try {
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");//修改后
random.setSeed(key.getBytes());
KeyGenerator kgen = KeyGenerator.getInstance("AES");
kgen.init(128, random);
SecretKey secretKey = kgen.generateKey();
byte[] enCodeFormat = secretKey.getEncoded();
SecretKeySpec secretKeySpec = new SecretKeySpec(enCodeFormat, "AES");
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec);
byte[] result = cipher.doFinal(byteRresult);
return new String(result);
} catch (Exception e) {
log.error("decrypt aes error key:{}",content);
log.error("decrypt aes error message :{}",e.getMessage());
}
return null;
}
/**
* 加密
*/
public static String encryptAES(String content, String key) {
try {
if(StringUtils.isBlank(content)){
return null;
}
KeyGenerator kgen = KeyGenerator.getInstance("AES");
kgen.init(128, new SecureRandom(key.getBytes()));
SecretKey secretKey = kgen.generateKey();
SecretKeySpec secretKeySpec = getSecretKey(key);
Cipher cipher = Cipher.getInstance("AES");
byte[] byteContent = content.getBytes("utf-8");
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
byte[] byteRresult = cipher.doFinal(byteContent);
StringBuffer sb = new StringBuffer();
for (int i = 0; i < byteRresult.length; i++) {
String hex = Integer.toHexString(byteRresult[i] & 0xFF);
if (hex.length() == 1) {
hex = '0' + hex;
}
sb.append(hex.toUpperCase());
}
return sb.toString();
} catch (Exception e) {
log.error("encrypt aes error ",e);
}
return null;
}
private static SecretKeySpec getSecretKey(String password) {
KeyGenerator kg = null;
try {
kg = KeyGenerator.getInstance("AES");
SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
secureRandom.setSeed(password.getBytes());
kg.init(128, secureRandom);
SecretKey secretKey = kg.generateKey();
return new SecretKeySpec(secretKey.getEncoded(), "AES");
} catch (NoSuchAlgorithmException ex) {
ex.printStackTrace();
}
return null;
}
}
2.4 实现入参加密拦截器
@Component
@Intercepts({
@Signature(type = ParameterHandler.class, method = "setParameters", args = PreparedStatement.class),
})
public class EncryptInterceptor implements Interceptor {
@Autowired
private AESManager aesManager;
public Object intercept(Invocation invocation) throws Throwable {
//@Signature 指定了 type= parameterHandler 后,这里的 invocation.getTarget() 便是parameterHandler
//若指定ResultSetHandler ,这里则能强转为ResultSetHandler
ParameterHandler parameterHandler = (ParameterHandler) invocation.getTarget();
// 获取参数对像,即 mapper 中 paramsType 的实例
Field parameterField = parameterHandler.getClass().getDeclaredField("parameterObject");
parameterField.setAccessible(true);
//取出实例,当parameterObject 是单独一个对象时
Object parameterObject = parameterField.get(parameterHandler);
if (parameterObject != null) {
Class<?> parameterObjectClass = parameterObject.getClass();
//校验该实例的类是否被@SensitiveData所注解
SensitiveData sensitiveData = AnnotationUtils.findAnnotation(parameterObjectClass, SensitiveData.class);
if (Objects.nonNull(sensitiveData)) {
//取出当前当前类所有字段,传入加密方法
Field[] declaredFields = parameterObjectClass.getDeclaredFields();
aesManager.encrypt(declaredFields, parameterObject);
}
}
if (parameterObject != null) {
try {
Map<String,Object> jsonObject = (Map)parameterObject;
Object param1 = jsonObject.get("param1");
Class<?> parameterObjectClass = param1.getClass();
//校验该实例的类是否被@SensitiveData所注解
SensitiveData sensitiveData = AnnotationUtils.findAnnotation(parameterObjectClass, SensitiveData.class);
if (Objects.nonNull(sensitiveData)) {
//取出当前当前类所有字段,传入加密方法
Field[] declaredFields = parameterObjectClass.getDeclaredFields();
aesManager.encrypt(declaredFields, param1);
}
}catch (Exception e){
}
}
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}
2.5 实现出参解密拦截器
@Component
@Intercepts(
@Signature(type = ResultSetHandler.class,method = "handleResultSets",args = {Statement.class})
)
public class DecryptInterceptor implements Interceptor {
@Autowired
private AESManager aesManager;
@Override
public Object intercept(Invocation invocation) throws Throwable {
//取出查询的结果
Object resultObject = invocation.proceed();
if (Objects.isNull(resultObject)) {
return null;
}
//基于selectList
if (resultObject instanceof ArrayList) {
ArrayList resultList = (ArrayList) resultObject;
if (!CollectionUtils.isEmpty(resultList) && needToDecrypt(resultList.get(0))) {
for (Object result : resultList) {
//逐一解密
aesManager.decrypt(result);
}
}
//基于selectOne
} else {
if (needToDecrypt(resultObject)) {
aesManager.decrypt(resultObject);
}
}
return resultObject;
}
private boolean needToDecrypt(Object object) {
Class<?> objectClass = object.getClass();
SensitiveData sensitiveData = AnnotationUtils.findAnnotation(objectClass, SensitiveData.class);
return Objects.nonNull(sensitiveData);
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}
3 注解类中需要加的字段
@SensitiveData
public class UserDO {
/**
* 账户名
*/
private String username;
/**
* 手机号
*/
@SensitiveField
private String mobile;
}
4 注意实现
1 、添加注解的类一定是和数据库直接打交道的类,也就是mapping文件中映射的类
2、在同一个方法中,同一个对象不能连续更新和保存两次以上,因为更新和保存会对敏感字段进行加密,会导致加密两次。
3
该方法要实现,否则拦截器不生效