1.为什么要做?
业务数据库中存放有一部分用户的敏感数据,例如手机号。为了保证数据安全,避免用户隐私的泄露,需要将数据库中涉及个人隐私的敏感信息进行加密存储。
2.结果?
- 过渡期间,数据不丢失,不错乱。
- 保证系统平稳过渡,前端或其他业务无感。
- 数据库中的所有敏感字段加密存储。
3.技术路线
- 对于待加密的明文字段创建相应的密文字段。例如对于明文字段
addr,创建密文字段address_encrypt。 - 对于数据库新增数据或者修改数据,同时写入明文数据与密文数据,即双写;
- 对于数据库中的存量数据,可通过后台线程或接口,对数据库密文字段为空的数据进行更新。
- 上线前,对所有变更接口的返回数据进行对比。
- 过渡期间,观察系统的异常情况。如果存在异常,及时回退;如果不存在异常,则在系统稳定后,停止对新增数据或者修改数据时对明文数据的写入,并删除明文存储的敏感字段。
4.怎么样做?
4.1.耦合业务流程
在字段写入时,就存入加密的数据;同时,从实体中取出来的数据,也是加密的结果,需要解密后,传递给前端或被其他业务使用。
存储数据
1 | `String address = "武汉市xxx"; // 存储数据 String addressEncrypt = EncryptHelper.encryptString(address); Entity entity = new Entity(); entity.setAddressEncrypt(addressEncrypt); // ... // 持久化到数据库 ` |
读取数据
1 | `// 从数据库读取数据 为entity String address = EncryptHelper.decrypt(entity.getAddressEncrypt()); // ... // 数据处理 ` |
4.2.Mybatis TypeHandler
TypeHandler是Mybatis中的类型转换器,用于在Mybatis中用于实现Java类型和JDBC类型的相互映射转换。Mybatis本身自带大量内置的TypeHandler,用于实现默认的Java类型和JDBC类型的相互映射转换。相较于于拦截器,TypeHandler既能够做到全局生效,也能够实现在字段级别对字段进行加解密,比较灵活。
4.2.1.使用方法
1). 定义TypeHandler
1 | `package xxx; import org.apache.ibatis.type.JdbcType; import org.apache.ibatis.type.TypeHandler; import java.sql.CallableStatement; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; /** * TypeHandler:T为要进行类型转换的Java数据类型,此处即为加密字段的Java类型。 */ @MappedJdbcTypes(JdbcType.INTEGER) public class CustomHandler implements TypeHandler { /** * 实体向数据库转换 * @param preparedStatement * @param i * @param integer * @param jdbcType * @throws SQLException */ @Override public void setParameter(PreparedStatement preparedStatement, int i, Integer integer, JdbcType jdbcType) throws SQLException { int encryption; // 数据加密 try { preparedStatement.setInt(i, encryption); } catch (Exception e) { e.printStackTrace(); } } @Override public Integer getResult(ResultSet resultSet, String columnName) throws SQLException { int encryption = resultSet.getInt(columnName); int decryption; // 数据解密 return decryption; } @Override public Integer getResult(ResultSet resultSet, int columnIndex) throws SQLException { int encryption = resultSet.getInt(columnIndex); int decryption; // 数据解密 return decryption; } @Override public Integer getResult(CallableStatement callableStatement, int columnIndex) throws SQLException { int encryption = callableStatement.getInt(columnIndex); int decryption; // 数据解密 return decryption; } }` |
2). 注入TypeHandler
如果需要TypeHandler对全局所有类型生效,则编辑mybatis-config.xml,将自定义的handler注入到Mybatis中;否则可省略此步骤。
1 | `<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> ... <typeHandlers> <typeHandler jdbcType="INTEGER" javaType="Integer" handler="com.abc.typeHandler.CustomHandler"/> ... </typeHandlers> </configuration>` |
3). 为字段指定TypeHandler
编辑Mapper xml文件:
1 | `<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.abc.mapper.EntityMapper"> <resultMap type="com.abc.Entity" id="entityResultMap"> <!--在resultMap中为字段定义TypeHandler--> + <result property="addr" column="addr" typeHandler="com.abc.CustomHandler" /> </resultMap> <!--为select指定resultMap--> + <select id="findEntityById" resultMap="entityResultMap"> SELECT * FROM Entity WHERE id = #{id} </select> <insert id="insertUser"> <!--为insert语句定义字段插入时使用的TypeHandler--> + INSERT INTO Entity(addr) VALUES(#{addr,typeHandler=com.abc.CustomHandler}) </insert> </mapper>` |
或者,编辑Mapper接口:
1 | `@Mapper public interface EntityMapper { @Results(value = { // 仅指定自定义TypeHandler字段即可,price字段未加密,则不需要指定TypeHandler @Result(property = "addr", column = "addr", typeHandler = CustomHandler.class), }) @Select("SELECT * FROM Entity WHERE id = #{id}") User findEntityById(@Param("id") Integer id); @Insert("INSERT INTO Entity(addr, price) VALUES(#{addr,typeHandler=com.abc.CustomHandler}, #{price})") int insertUser(@Param("addr") Integer addr, @Param("price") Integer price); } ` |
4.3.Mybatis Interceptor(拦截器)
Mybatis的Interceptor(拦截器)虽然Mybatis 中被当作 Plugin(插件),但是可以用来拦截SQL的参数处理、SQL语句构建、SQL执行和返回结果处理等阶段。要实现字段的加解密,可以通过自定义两个拦截器:参数处理拦截器和结果集拦截器,在参数处理拦截器中对字段进行加密,在结果集拦截器中对字段进行解密。但是Mybatis的拦截器是全局生效的,要想针对特定的字段,需要额外定义注解。
4.3.1.使用方法
1). 定义拦截器
1 | `package xxx; import xxx.EncryptDecryptClass; import xxx.ApplicationUtils; import org.apache.ibatis.executor.resultset.ResultSetHandler; import org.apache.ibatis.executor.statement.StatementHandler; import org.apache.ibatis.plugin.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.cglib.core.CollectionUtils; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.stereotype.Component; import java.sql.Statement; import java.util.ArrayList; import java.util.Objects; import java.util.Properties; @Intercepts({ @Signature(type = ResultSetHandler.class, method = "handleResultSets", args={Statement.class}) }) @ConditionalOnProperty(value = "domain.decrypt", havingValue = "true") @Component public class ResultInterceptor implements Interceptor { @Autowired private IEncryptDecrypt encryptDecrypt; @Override public Object intercept(Invocation invocation) throws Throwable { Object result = invocation.proceed(); if (Objects.isNull(result)){ return null; } if (result instanceof ArrayList) { ArrayList resultList = (ArrayList) result; if (result != null && !((ArrayList) result).isEmpty() && needToDecrypt(resultList.get(0))){ if(encryptDecrypt==null) { encryptDecrypt= ApplicationUtils.getBean(EncryptDecryptImpl.class); } for (int i = 0; i < resultList.size(); i++) { encryptDecrypt.decrypt(resultList.get(i)); } } }else { if (needToDecrypt(result)){ encryptDecrypt.decrypt(result); } } return result; } public boolean needToDecrypt(Object object){ Class objectClass = object.getClass(); EncryptDecryptClass encryptDecryptClass = AnnotationUtils.findAnnotation(objectClass, EncryptDecryptClass.class); if (Objects.nonNull(encryptDecryptClass)){ return true; } return false; } @Override public Object plugin(Object target) { if (target instanceof ResultSetHandler) { return Plugin.wrap(target, this); } return target; //return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { } } ` |
2). 定义注解
1 | `package xxx.annotation; import java.lang.annotation.*; /** * 需要加解密的类注解 * * @author fraser * @date 2019-05-15 11:11 */ @Documented @Inherited @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface EncryptDecryptClass { } ================================================================ package xxx.annotation; import java.lang.annotation.*; /** * 加密字典注解 * * @author fraser * @date 2019-05-15 11:08 */ @Documented @Inherited @Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) public @interface EncryptDecryptField { } ` |
3). 注册插件
1 | `package xxx.config; import xxx.ParameterInterceptor; import xxx.ResultInterceptor; import org.apache.ibatis.session.SqlSessionFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class MybatisInterceptorConfig { @Bean public String parameterInterceptor(SqlSessionFactory sqlSessionFactory) { ParameterInterceptor executorInterceptor = new ParameterInterceptor(); ResultInterceptor resultExecutorInterceptor = new ResultInterceptor(); sqlSessionFactory.getConfiguration().addInterceptor(executorInterceptor); sqlSessionFactory.getConfiguration().addInterceptor(resultExecutorInterceptor); return "interceptor"; } } ` |
4.4.加密字段模糊搜索
敏感字段加密后,会导致原来通过通配符进行模糊搜索的方案失效,常见的两种解决方案:
- 将所有数据读取到内存中解密后匹配
- 对加密数据创建可模糊匹配的加密索引字段,例如
address_enctypr_idnex。
很显然,方案1在数据量大的时候适用。方案2比较通用,也是目前很多大公司也在采用的方案,比如阿里、京东、拼多多等。
方案2如何实现呢?首先,要保证同一个明文对应密文相同;其次将明文数据分组,按照一定的窗口大小进行滑动加密,比如以4位英文字符(半角),2个中文字符(全角)为一个检索条件。
例如,需要加密为字符串为“qwert”,分别对“qwer”和“wert”进行加密,假设其加密结果分别为“encrypt1”和“encrypt2”,则拼接 “encrypt1”和“encrypt2”——即“encrypt1encrypt2”,并储存进加密索引字段(例如address_enctypr_idnex)。搜索时,例如搜索“qwer”,则使用like "%encrypt1%"即可。
但是方案2缺点很明显:
- 加密索引字段的可能会很长
- 模糊搜索时,搜索内容长度必须要长于窗口长度
对此,对于手机号,则可以限制搜索的内容为手机号后4位、后6位等;对于姓名则要求必须使用完整的姓名进行搜索;对于地址,可以将省市县作为单独的字段,将精确到小区的地址进行加密存储。这些都能够在一定程度上缓解加密索引字段的过长的问题。