引言:自从有公司项目前2年做了三级等保,每年一度例行公事,昨天继续配合做等保测试。这2天比较忙,这里整理之前写的一篇等保技术文章。
正文:
现在公司项目基本用mybatis实现,但由于项目跨度年份比较久, 技术实现分为2种,早期项目是用mybatis-generator实现, 新项目是用mybatis-plus实现, 这里讲一下分别用不同的方式实现数据库敏感字段加解密。
敏感字段加解密,比如手机号、姓名、身份证、住址、邮箱,原理比较简单,在数据插入前对数据进行加密,数据查询出来的时候再解密。
1、先说加密算法, 已有的老项目,可以用AES,毕竟修复数据可以通过mysql的sql语法直接搞定。直接上sql。新项目,推荐用国密SM4
java
select hex(AES_ENCRYPT('hello world! 张三-123', '1234567890123456'));
select AES_DECRYPT(UNHEX('42A8DAF413119F35A746CD231A955E7501C1E3E3D785CD892795EAE387B379BF'),'1234567890123456') ;
2、mybatis-generator项目,实现mybatis的typeHandler
java
/**
* 注意不能把 BaseTypeHandler 改为 BaseTypeHandler<String> ,如果改为 BaseTypeHandler<String> 的话,
* 所有 String 类型的字段都会给加密了。改为 BaseTypeHandler 在需要加密的地方加上 typeHandler 就好了
*/
public class EncryptTypeHandler extends BaseTypeHandler {
@SneakyThrows
@Override
public void setNonNullParameter(PreparedStatement preparedStatement, int i, Object o, JdbcType jdbcType) {
preparedStatement.setString(i, BizUtil.encryptData_ECB((String) o));
}
/**
* 用于在Mybatis获取数据结果集时如何把数据库类型转换为对应的Java类型
*
* @param rs 当前的结果集
* @param columnName 当前的字段名称
* @return 转换后的Java对象
* @throws SQLException
*/
@SneakyThrows
@Override
public String getNullableResult(ResultSet rs, String columnName) {
String r = rs.getString(columnName);
return r == null ? null : BizUtil.decryptData_ECB(r);
}
/**
* 用于在Mybatis通过字段位置获取字段数据时把数据库类型转换为对应的Java类型
*
* @param rs 当前的结果集
* @param columnIndex 当前字段的位置
* @return 转换后的Java对象
* @throws SQLException
*/
@SneakyThrows
@Override
public String getNullableResult(ResultSet rs, int columnIndex) {
String r = rs.getString(columnIndex);
return r == null ? null : BizUtil.decryptData_ECB(r);
}
/**
* 用于Mybatis在调用存储过程后把数据库类型的数据转换为对应的Java类型
*
* @param cs 当前的CallableStatement执行后的CallableStatement
* @param columnIndex 当前输出参数的位置
* @return
* @throws SQLException
*/
@SneakyThrows
@Override
public String getNullableResult(CallableStatement cs, int columnIndex) {
String r = cs.getString(columnIndex);
// 兼容待修复的数据
return r == null ? null : BizUtil.decryptData_ECB(r);
}
}
mapper文件对应加密字段属性实现
java
<result column="user_mobile" jdbcType="VARCHAR"
property="userMobile"
typeHandler="com.company.group.project.util.mybatis.
EncryptTypeHandler"/>
插入、更新对应属性加上
java
#{userMobile,jdbcType=VARCHAR, typeHandler=
com.company.group.project.util.mybatis.EncryptTypeHandler},
老项目埋坑点:
a、手写的mapper 人肉处理,通过建resultMap设置属性
b、敏感字段作为参数搜索,必须加密后传递查询。 正常来说,在baseService做 selectByExample封装, 不少同学之前在extendService甚至更高层级操作了mapper, 改的苦笑不得
3、mybatis-plus项目, 之前mybatis版本之前用的低版本3.1,为了用上mybatis-plus 3.4的JsqlParserSupport等,把springboot1.5.x 升级到了springboot2.x, 这个过程走了一些弯路,所幸成功了。
mybatis-plus主要通过对象属性注解反射实现的,参照了网上的一些代码,代码改动量相对比较少。 敏感字段作为参数搜索,必须加密传递查询。
加密插件:
java
public class EncryptInterceptor extends JsqlParserSupport implements InnerInterceptor {
/**
* 变量占位符正则
*/
private static final Pattern PARAM_PAIRS_RE = Pattern.compile("#\\{ew\\.paramNameValuePairs\\.(" + Constants.WRAPPER_PARAM + "\\d+)\\}");
@Override
public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) {
InnerInterceptor.super.beforePrepare(sh, connection, transactionTimeout);
//System.out.println("================================================================================beforePrepare");
}
/**
* 如果查询条件是加密数据列,那么要将查询条件进行数据加密。
* 例如,手机号加密存储后,按手机号查询时,先把要查询的手机号进行加密,再和数据库存储的加密数据进行匹配
*/
@Override
public void beforeQuery(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
//System.out.println("================================================================================beforeQuery");
if (Objects.isNull(parameterObject)) {
return;
}
if (!(parameterObject instanceof Map)) {
return;
}
Map paramMap = (Map) parameterObject;
// 参数去重,否则多次加密会导致查询失败
Set set = (Set) paramMap.values().stream().collect(Collectors.toSet());
for (Object param : set) {
/**
* 仅支持类型是自定义Entity的参数,不支持mapper的参数是QueryWrapper、String等,例如:
*
* 支持:findList(@Param(value = "query") UserEntity query);
* 支持:findPage(@Param(value = "query") UserEntity query, Page<UserEntity> page);
*
* 不支持:findList(@Param(value = "mobile") String mobile);
* 不支持:findList(QueryWrapper wrapper);
*/
if (param instanceof AbstractWrapper || param instanceof String) {
// Wrapper、String类型查询参数,无法获取参数变量上的注解,无法确认是否需要加密,因此不做判断
continue;
}
if (needToDecrypt(param.getClass())) {
encryptEntity(param);
}
}
}
@Override
public void beforeUpdate(Executor executor, MappedStatement mappedStatement, Object parameterObject) throws SQLException {
if (Objects.isNull(parameterObject)) {
return;
}
// 通过MybatisPlus自带API(save、insert等)新增数据库时
if (!(parameterObject instanceof Map)) {
if (needToDecrypt(parameterObject.getClass())) {
encryptEntity(parameterObject);
}
return;
}
Map paramMap = (Map) parameterObject;
Object param;
// 通过MybatisPlus自带API(update、updateById等)修改数据库时
if (paramMap.containsKey(Constants.ENTITY) && null != (param = paramMap.get(Constants.ENTITY))) {
if (needToDecrypt(param.getClass())) {
encryptEntity(param);
}
return;
}
// 通过在mapper.xml中自定义API修改数据库时
if (paramMap.containsKey("entity") && null != (param = paramMap.get("entity"))) {
if (needToDecrypt(param.getClass())) {
encryptEntity(param);
}
return;
}
// 通过UpdateWrapper、LambdaUpdateWrapper修改数据库时
if (paramMap.containsKey(Constants.WRAPPER) && null != (param = paramMap.get(Constants.WRAPPER))) {
if (param instanceof Update && param instanceof AbstractWrapper) {
Class<?> entityClass = mappedStatement.getParameterMap().getType();
if (needToDecrypt(entityClass)) {
encryptWrapper(entityClass, param);
}
}
return;
}
}
/**
* 校验该实例的类是否被@EncryptedTable所注解
*/
private boolean needToDecrypt(Class<?> objectClass) {
EncryptedTable sensitiveData = AnnotationUtils.findAnnotation(objectClass, EncryptedTable.class);
return Objects.nonNull(sensitiveData);
}
/**
* 通过API(save、updateById等)修改数据库时
*
* @param parameter
*/
private void encryptEntity(Object parameter) {
//取出parameterType的类
Class<?> resultClass = parameter.getClass();
Field[] declaredFields = resultClass.getDeclaredFields();
for (Field field : declaredFields) {
//取出所有被EncryptedColumn注解的字段
EncryptedColumn sensitiveField = field.getAnnotation(EncryptedColumn.class);
if (!Objects.isNull(sensitiveField)) {
field.setAccessible(true);
Object object = null;
try {
object = field.get(parameter);
} catch (IllegalAccessException e) {
continue;
}
//只支持String的解密
if (object instanceof String) {
String value = (String) object;
//对注解的字段进行逐一加密
try {
field.set(parameter, AESUtil.encrypt(value));
} catch (IllegalAccessException e) {
continue;
}
}
}
}
}
/**
* 通过UpdateWrapper、LambdaUpdateWrapper修改数据库时
*
* @param entityClass
* @param ewParam
*/
private void encryptWrapper(Class<?> entityClass, Object ewParam) {
AbstractWrapper updateWrapper = (AbstractWrapper) ewParam;
String sqlSet = updateWrapper.getSqlSet();
if (StringUtils.isBlank(sqlSet)) {
return;
}
String[] elArr = sqlSet.split(",");
Map<String, String> propMap = new HashMap<>(elArr.length);
Arrays.stream(elArr).forEach(el -> {
String[] elPart = el.split("=");
propMap.put(elPart[0], elPart[1]);
});
//取出parameterType的类
Field[] declaredFields = entityClass.getDeclaredFields();
for (Field field : declaredFields) {
//取出所有被EncryptedColumn注解的字段
EncryptedColumn sensitiveField = field.getAnnotation(EncryptedColumn.class);
if (Objects.isNull(sensitiveField)) {
continue;
}
String el = propMap.get(field.getName());
try {
Matcher matcher = PARAM_PAIRS_RE.matcher(el);
if (matcher.matches()) {
String valueKey = matcher.group(1);
Object value = updateWrapper.getParamNameValuePairs().get(valueKey);
updateWrapper.getParamNameValuePairs().put(valueKey, AESUtil.encrypt(value.toString()));
}
}catch (Exception e){
logger.error("{}", e);
}
}
Method[] declaredMethods = entityClass.getDeclaredMethods();
for (Method method : declaredMethods) {
//取出所有被EncryptedColumn注解的字段
EncryptedColumn sensitiveField = method.getAnnotation(EncryptedColumn.class);
if (Objects.isNull(sensitiveField)) {
continue;
}
String el = propMap.get(method.getName());
try {
Matcher matcher = PARAM_PAIRS_RE.matcher(el);
if (matcher.matches()) {
String valueKey = matcher.group(1);
Object value = updateWrapper.getParamNameValuePairs().get(valueKey);
updateWrapper.getParamNameValuePairs().put(valueKey, AESUtil.encrypt(value.toString()));
}
}catch (Exception e){
logger.error("{}",e);
}
}
}
}
解密插件:
java
@Intercepts({
@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
})
@Component
public class DecryptInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object resultObject = invocation.proceed();
if (Objects.isNull(resultObject)) {
return null;
}
if (resultObject instanceof ArrayList) {
//基于selectList
ArrayList resultList = (ArrayList) resultObject;
if (!resultList.isEmpty() && needToDecrypt(resultList.get(0))) {
for (Object result : resultList) {
//逐一解密
decrypt(result);
}
}
} else if (needToDecrypt(resultObject)) {
//基于selectOne
decrypt(resultObject);
}
return resultObject;
}
/**
* 校验该实例的类是否被@EncryptedTable所注解
*/
private boolean needToDecrypt(Object object) {
Class<?> objectClass = object.getClass();
EncryptedTable sensitiveData = AnnotationUtils.findAnnotation(objectClass, EncryptedTable.class);
return Objects.nonNull(sensitiveData);
}
@Override
public Object plugin(Object o) {
return Plugin.wrap(o, this);
}
private <T> T decrypt(T result) throws Exception {
//取出resultType的类
Class<?> resultClass = result.getClass();
Field[] declaredFields = resultClass.getDeclaredFields();
for (Field field : declaredFields) {
//取出所有被EncryptedColumn注解的字段
EncryptedColumn sensitiveField = field.getAnnotation(EncryptedColumn.class);
if (!Objects.isNull(sensitiveField)) {
field.setAccessible(true);
Object object = field.get(result);
//只支持String的解密
if (object instanceof String) {
String value = (String) object;
//对注解的字段进行逐一解密
field.set(result, AESUtil.decrypt(value));
}
}
}
return result;
}
}
mybatis配置
java
@Configuration(proxyBeanMethods = false)
public class MybatisPlusConfig {
/**
* 单页分页条数限制(默认无限制,参见 插件#handlerLimit 方法)
*/
private static final Long MAX_LIMIT = 1000L;
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 加解密拦截器
interceptor.addInnerInterceptor(new EncryptInterceptor());
// 分页拦截
PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
paginationInterceptor.setMaxLimit(MAX_LIMIT);
interceptor.addInnerInterceptor(paginationInterceptor);
// 乐观锁拦截
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
}
model对象demo
java
@EncryptedTable
@TableEventListen({SqlCommandType.INSERT, SqlCommandType.UPDATE})
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
@TableName("db_demo")
public class Demo extends BaseEntity<Demo> {
private static final long serialVersionUID = 1L;
@EncryptedColumn
private String mobile;
@Override
protected Serializable pkVal() {
return null;
}
}
遇到的坑:
1、mybatis-plus的 service 低版本和高版本默认值不一样getOne(queryWrapper, false); 高版本默认是抛异常
2、针对对象反射,比如更新后查询这种,最好是更新 和查询分别用一个对象。