使用拦截器进行数据加解密

  • 使用拦截器进行数据加解密
    • 1. 加解密工具
    • 3. 加解密字段注解
      • 3.1 加密注解
      • 3.2 解密注解
    • 4. 封装加解密工具
    • 5. 拦截器
    • 6. 不同框架配置说明
      • 6.1 springboot下的配置
      • 6.2 spring的xml配置
    • 7. 说明

使用拦截器进行数据加解密

本文并非详细探讨AES加解密内容,而是在Spring+Mybatis的项目基础上,以sql拦截器的形式,实现了对数据存取加解密的方案。文章项目示例采用springboot框架,对需要加解密的字段添加注解,sql执行过程中,拦截器进行拦截。可通过配置加解密开关决定是否对字段进行加解密。加密方式AES。

文章并未列出所有源码,依赖包等详细配置,在源码中有具体的sql脚本等文件,点击访问项目源码。

  • 源码框架

    java8 springboot mybatis gradle


1. 加解密工具

方法generateAESKey()生成128位秘钥,以16进制字符串保存,从配置文件读取,以单例模式初始化加解密工具,保证项目运行过程中对象不会被重新创建,避免多次初始化Cipher。加解密方法详见代码如下。

/**
 * @decription ADESUtils
 * 

字段加解密,使用MySql AES算法

* @author Yampery * @date 2018/4/4 13:10 */
@Component public class ADESUtils { private static final String ENCRYPT_TYPE = "AES"; private static final String ENCODING = "UTF-8"; // 密盐 private static String aesSalt; private static ADESUtils adesUtils; private static Cipher encryptCipher; // 加密cipher private static Cipher decryptChipher; // 解密chipher // 加解密开关,从配置获取 private static String CRYPTIC_SWITCH; /** * 从配置中获取秘钥 * :默认值填写自己生成的秘钥 * @param key */ @Value("${sys.aes.salt:0}") public void setAESSalt(String key){ ADESUtils.aesSalt = key; } /** * 获取开关 * 默认为不加密 * @param val */ @Value("${sys.aes.switch:0}") public void setCrypticSwitch(String val) { ADESUtils.CRYPTIC_SWITCH = val; } /** * encryptCipher、decryptChipher初始化 */ public static void init(){ try { encryptCipher = Cipher.getInstance(ENCRYPT_TYPE); decryptChipher = Cipher.getInstance(ENCRYPT_TYPE); encryptCipher.init(Cipher.ENCRYPT_MODE, generateMySQLAESKey(aesSalt)); decryptChipher.init(Cipher.DECRYPT_MODE, generateMySQLAESKey(aesSalt)); } catch (InvalidKeyException e) { throw new RuntimeException(e); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } catch (NoSuchPaddingException e) { throw new RuntimeException(e); } } private ADESUtils() { } /** * 获取单例 * @return */ public static ADESUtils getInstance(){ if(adesUtils == null){ // 当需要创建的时候在加锁 synchronized(ADESUtils.class) { if (adesUtils == null) { adesUtils = new ADESUtils(); init(); } } } return adesUtils; } /** * 对明文加密 * @param pString * @return */ public String encrypt(String pString) { if (StringUtils.isBlank(pString) || StringUtils.equals("0", CRYPTIC_SWITCH)) return StringUtils.trimToEmpty(pString); try{ return new String(Hex.encodeHex(encryptCipher.doFinal(pString.getBytes(ENCODING)))).toUpperCase(); } catch (Exception e) { e.printStackTrace(); return pString; } } /** * 对密文解密 * @param eString * @return */ public String decrypt(String eString) { if (StringUtils.isBlank(eString) || StringUtils.equals("0", CRYPTIC_SWITCH)) return StringUtils.trimToEmpty(eString); try { return new String(decryptChipher.doFinal(Hex.decodeHex(eString.toCharArray()))); } catch (Exception e) { e.printStackTrace(); return eString; } } /** * 产生mysql-aes_encrypt * @param key 加密的密盐 * @return */ public static SecretKeySpec generateMySQLAESKey(final String key) { try { final byte[] finalKey = new byte[16]; int i = 0; for(byte b : Hex.decodeHex(key.toCharArray())) finalKey[i++ % 16] ^= b; return new SecretKeySpec(finalKey, "AES"); } catch(Exception e) { throw new RuntimeException(e); } } /** * 生成秘钥(128位) * @return * @throws Exception */ public static String generateAESKey() throws Exception{ //实例化 KeyGenerator kgen = KeyGenerator.getInstance("AES"); //设置密钥长度 kgen.init(128); //生成密钥 SecretKey skey = kgen.generateKey(); // 转为16进制字串 String key = new String(Hex.encodeHex(skey.getEncoded())); //返回密钥的16进制字串 return key.toUpperCase(); } }

3. 加解密字段注解

注解标识字段是否需要加密或者解密,用于通过反射获取需要进行加解密的字段,防止需求变动,将加密和解密注解分开。

3.1 加密注解

/**
 * @decription EncryptField
 * 

字段加密注解

* @author Yampery * @date 2017/10/24 13:01 */
@Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface EncryptField { String value() default ""; }

3.2 解密注解

/**
 * @decription DecryptField
 * 

字段解密注解

* @author Yampery * @date 2017/10/24 13:05 */
@Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface DecryptField { String value() default ""; }

4. 封装加解密工具

为了在项目中方便使用,将上节中的加解密工具进行封装,封装后的工具可以作用于对象,通过反射获取注解,而对原对象进行改变。另外,项目中也实现了对象的自加解密CrypticPojo,原理是CrypticPojo实现clone方法,并在内部实现加解密方法,需要进行字段加解密的业务对象只需要继承CrypticPojo,每次返回调用一次克隆并加密方法即可,具体见源码。

/**
 * @decription CryptPojoUtils
 * 

对象加解密工具 * 其子类可以通过调用encrypt(T t)方法实现自加密,返回参数类型; * 调用decrypt(T t)实现自解密,返回参数类型; * encrypt对注解{@link EncryptField}字段有效; * decrypt对注解{@link DecryptField}字段有效。

* @author Yampery * @date 2017/10/24 13:36 */
public class CryptPojoUtils { /** * 对对象t加密 * @param t * @param * @return */ public static T encrypt(T t) { Field[] declaredFields = t.getClass().getDeclaredFields(); try { if (declaredFields != null && declaredFields.length > 0) { for (Field field : declaredFields) { if (field.isAnnotationPresent(EncryptField.class) && field.getType().toString().endsWith("String")) { field.setAccessible(true); String fieldValue = (String) field.get(t); if (StringUtils.isNotEmpty(fieldValue)) { field.set(t, ADESUtils.getInstance().encrypt(fieldValue)); } field.setAccessible(false); } } } } catch (IllegalAccessException e) { throw new RuntimeException(e); } return t; } /** * 对象解密 * @param t * @param * @return */ public static T decrypt(T t) { Field[] declaredFields = t.getClass().getDeclaredFields(); try { if (declaredFields != null && declaredFields.length > 0) { for (Field field : declaredFields) { if (field.isAnnotationPresent(DecryptField.class) && field.getType().toString().endsWith("String")) { field.setAccessible(true); String fieldValue = (String)field.get(t); if(StringUtils.isNotEmpty(fieldValue)) { field.set(t, ADESUtils.getInstance().decrypt(fieldValue)); } } } } } catch (IllegalAccessException e) { throw new RuntimeException(e); } return t; } /** * 对含注解字段解密 * @param t * @param */ public static void decryptField(T t) { Field[] declaredFields = t.getClass().getDeclaredFields(); try { if (declaredFields != null && declaredFields.length > 0) { for (Field field : declaredFields) { if (field.isAnnotationPresent(DecryptField.class) && field.getType().toString().endsWith("String")) { field.setAccessible(true); String fieldValue = (String)field.get(t); if(StringUtils.isNotEmpty(fieldValue)) { field.set(t, ADESUtils.getInstance().decrypt(fieldValue)); } } } } } catch (IllegalAccessException e) { throw new RuntimeException(e); } // return t; } /** * 对含注解字段加密 * @param t * @param */ public static void encryptField(T t) { Field[] declaredFields = t.getClass().getDeclaredFields(); try { if (declaredFields != null && declaredFields.length > 0) { for (Field field : declaredFields) { if (field.isAnnotationPresent(EncryptField.class) && field.getType().toString().endsWith("String")) { field.setAccessible(true); String fieldValue = (String)field.get(t); if(StringUtils.isNotEmpty(fieldValue)) { field.set(t, ADESUtils.getInstance().encrypt(fieldValue)); } } } } } catch (IllegalAccessException e) { throw new RuntimeException(e); } } /** * 隐藏号码中间4位 * @param t * @param */ public static void hidePhone(T t) { Field[] declaredFields = t.getClass().getDeclaredFields(); try { if (declaredFields != null && declaredFields.length > 0) { for (Field field : declaredFields) { if (field.isAnnotationPresent(DecryptField.class) && field.getType().toString().endsWith("String")) { field.setAccessible(true); String fieldValue = (String)field.get(t); if(StringUtils.isNotEmpty(fieldValue)) { // 暂时与解密注解共用一个注解,该注解隐藏手机号中间四位 field.set(t, StringUtils.overlay(fieldValue, "****", 3, 7)); } } } } } catch (IllegalAccessException e) { throw new RuntimeException(e); } } }

5. 拦截器

使用sql拦截器处理加解密基本是对项目影响比较小的。该拦截器通过拦截sql,对写入数据和查询结果进行重写,然后再放行从而更改对象。
关于sql语句参数,文章中并没有在拦截器处理,而是使用一个LinkedMap封装了查询参数,在封装的过程中会对字段进行加密。
关于springboot中和spring中拦截器使用的区别下文将会介绍。

/**
 * @decription DBInterceptor
 * 

实现Mybatis拦截器,用于拦截修改,插入和返回需要加密或者解密的对象

* @author Yampery * @date 2018/4/4 14:17 */
@Intercepts({ @Signature(type=Executor.class,method="update",args={MappedStatement.class,Object.class}), @Signature(type=Executor.class,method="query",args={MappedStatement.class,Object.class,RowBounds.class,ResultHandler.class}) }) @Component public class DBInterceptor implements Interceptor { private final Logger logger = LoggerFactory.getLogger(DBInterceptor.class); @Value("${sys.aes.switch}") private String CRYPTIC_SWITCH; @Override public Object intercept(Invocation invocation) throws Throwable { MappedStatement statement = (MappedStatement) invocation.getArgs()[0]; String methodName = invocation.getMethod().getName(); Object parameter = invocation.getArgs()[1]; BoundSql sql = statement.getBoundSql(parameter); logger.info("sql is: {}", sql.getSql()); /** * @TODO 处理查询 */ if (StringUtils.equalsIgnoreCase("query", methodName)) { /** * 在这里可以处理查询参数,如传递的参数为明文,要按照密文查询 * 本文选择使用同一参数封装处理方案{@link git.yampery.cryptic.common.QueryParams} */ } /** * 拦截批量插入操作不仅繁琐,而且为了通用逐一通过反射加密不妥 * 如果有批量操作,最好在传递参数之前,向list中添加之前就加密 */ if (!"0".equals(CRYPTIC_SWITCH)) { if (StringUtils.equalsIgnoreCase("update", methodName) || StringUtils.equalsIgnoreCase("insert", methodName)) { CryptPojoUtils.encryptField(parameter); } } Object returnValue = invocation.proceed(); try { if (!"0".equals(CRYPTIC_SWITCH)) { if (returnValue instanceof ArrayList) { List list = (ArrayList) returnValue; if (null == list || 1 > list.size()) return returnValue; Object obj = list.get(0); if (null == obj) // 这里虽然list不是空,但是返回字符串等有可能为空 return returnValue; // 判断第一个对象是否有DecryptField注解 Field[] fields = obj.getClass().getDeclaredFields(); int len; if (null != fields && 0 < (len = fields.length)) { // 标记是否有解密注解 boolean isD = false; for (int i = 0; i < len; i++) { /** * 由于返回的是同一种类型列表,因此这里判断出来之后可以保存field的名称 * 之后处理所有对象直接按照field名称查找Field从而改之即可 * 有可能该类存在多个注解字段,所以需要保存到数组(项目中目前最多是2个) * @TODO 保存带DecryptField注解的字段名称到数组,按照名称获取字段并解密 * */ if (fields[i].isAnnotationPresent(DecryptField.class)) { isD = true; break; } } /// for end ~ if (isD) // 将含有DecryptField注解的字段解密 list.forEach(l -> CryptPojoUtils.decryptField(l)); } /// if end ~ } /// if end ~ } } catch (Exception e) { // 打印异常,由于拦截器本身抛出异常,比如拦截到很奇葩的返回,应算正常 // 直接返回原结果即可 logger.info("抛出异常,正常返回==> " + e.getMessage()); e.printStackTrace(); return returnValue; } return returnValue; } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { // TODO Auto-generated method stub } }

6. 不同框架配置说明

6.1 springboot下的配置

  • 启动主类需要添加mapper扫描注解
@SpringBootApplication
@MapperScan("git.yampery.cryptic.dao")
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
  • 配置文件application.properties需要添加映射
# Mybatis
mybatis.config-location             =classpath:mybatis/mybatis-config.xml
mybatis.mapper-locations            =classpath:mybatis/mapper/*.xml
mybatis.type-aliases-package        =git.yampery.cryptic.pojo

# 开启debug模式可以在控制台查看springboot加载流程
# debug                               =true

# 密盐(使用工具ADESUtils生成)
sys.aes.salt                        =4BB90812C2B9B0882A6FA7C203E4717F
# 加解密开关(1:开启加解密;0:关闭加解密)
sys.aes.switch                      =1
  • 拦截器会自动扫描,注意@Component注解

6.2 spring的xml配置

  • mybatis当然和传统配置一致,在spring上下文配置中添加
  • 拦截器配置,在spring上下文配置文件sqlsessionfactory中
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="dataSource" />
    <property name="configLocation" value="classpath:mybatis.xml" />
    
    <property name="mapperLocations">
        <array>
            <value>classpath:mybatis/mapper/*.xmlvalue>
        array>
    property>
    <property name="plugins">
        <array>
            <bean class="git.yampery.cryptic.interceptor.DBInterceptor">
                <property name="properties" value="property-value"/>
            bean>
        array>
    property>
bean>
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    <property name="basePackage" value="git.yampery.cryptic.dao" />
    <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory">property>
bean>

7. 说明

注意:文章中的代码只是部分,源码包含完整的测试和说明,点击访问项目源码。

你可能感兴趣的:(spring)