一、背景
在开发过程中,我们经常要对敏感信息做加密。例如: 账号、密码、银行卡号等。在目前项目的开发过程中,我们使用手动显示的方式调用加解密。但是当某个表需要加密的字段变多时,需要手动调用加解密的方式太多,过于繁琐,遂想有没有办法来实现非手动调用的自动加解密。
二、思路
先决条件: 加解密bean(此bean会被spring托管、需要读配置文件的密钥key);MybatisTypeHandler;使用mybatis-generator plugin生成Mapper、xml、Entity;自动完成加解密不用手动调用。
问题: 使用MybatisTypeHadler,并注入加解密;但是用generator生成xml与Entity时,类的属性会变成加密类,我需要类的属性还是String类,那能不能自定义generator plugin替换原生generator类属性加密类替换为String类。
思路: 使用MybatisTypeHadler实现自动加解密,并将bean注入到MybatisTypeHandler里,并集成到generator plugin里。使用自定义plugin替换类属性类型。
三、实现
1.新建自定义加密对象
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MybatisCryptNumber {
private String value;
}
2.新建自定义加解密TypeHandler
@Slf4j
@AllArgsConstructor
@MappedTypes(MybatisCryptNumber.class)
public class CryptNumberTypeHandler extends BaseTypeHandler<String> {
/**
* 加解密bean
*/
private final CryptNumber cryptNumber;
/**
* 用于定义在Mybatis设置参数时该如何把Java类型的参数转换为对应的数据库类型
*
* @param ps 当前的PreparedStatement对象
* @param i 当前参数的位置
* @param parameter 当前参数的Java对象
* @param jdbcType 当前参数的数据库类型
*
* @throws SQLException
*/
@Override
public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType)
throws SQLException {
// 只要 parameter 非空都进行加密
try {
ps.setObject(i, cryptNumber.encrypt(parameter));
} catch (Exception e) {
log.error("setNonNullParameter error", e);
throw new SQLException(e);
}
}
/**
* 用于在Mybatis获取数据结果集时如何把数据库类型转换为对应的Java类型
*
* @param rs 当前的结果集
* @param columnName 当前的字段名称
*
* @return 转换后的Java对象
*
* @throws SQLException
*/
@Override
public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
String r = rs.getString(columnName);
try {
return r == null ? null : cryptNumber.decrypt(r);
} catch (Exception e) {
log.error("getNullableResult error", e);
throw new SQLException(e);
}
}
/**
* 用于在Mybatis通过字段位置获取字段数据时把数据库类型转换为对应的Java类型
*
* @param rs 当前的结果集
* @param columnIndex 当前字段的位置
*
* @return 转换后的Java对象
*
* @throws SQLException
*/
@Override
public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
try {
String r = rs.getString(columnIndex);
return r == null ? null : cryptNumber.decrypt(r);
} catch (Exception e) {
log.error("getNullableResult error", e);
throw new SQLException(e);
}
}
/**
* 用于Mybatis在调用存储过程后把数据库类型的数据转换为对应的Java类型
*
* @param cs 当前的CallableStatement执行后的CallableStatement
* @param columnIndex 当前输出参数的位置
*
* @return
*
* @throws SQLException
*/
@Override
public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
try {
// 兼容待修复的数据
String r = cs.getString(columnIndex);
return r == null ? null : cryptNumber.decrypt(r);
} catch (Exception e) {
log.error("getNullableResult error", e);
throw new SQLException(e);
}
}
}
3.注册自定义TypeHandler
@Bean
public CryptNumber cryptNumber(){
return new CryptNumber();
}
@Bean
public CryptNumberTypeHandler cryptNumberTypeHandler(CryptNumber cryptNumber) {
return new CryptNumberTypeHandler(cryptNumber);
}
@Bean
@Primary
public SqlSessionFactory customSqlSessionFactory(@Qualifier("customDataSource") DataSource dataSource, CryptNumberTypeHandler cryptNumberTypeHandler)
throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
bean.setTypeHandlers(cryptNumberTypeHandler,);
bean.setMapperLocations(
new PathMatchingResourcePatternResolver().getResources("classpath*:mybatis/**/*.xml"));
return bean.getObject();
}
4.新增自定义generator plugin
public class CryptReplacePlugin extends PluginAdapter {
private List<String> replaceTypes;
public CryptReplacePlugin() {
super();
replaceTypes = Lists
.newArrayList("MybatisCryptSimple", "MybatisCryptNumber", "MybatisCryptBase62", "MybatisCryptBase36");
}
@Override
public boolean validate(List<String> warnings) {
return true;
}
/**
* 拦截普通字段
*/
@Override
public boolean modelBaseRecordClassGenerated(TopLevelClass topLevelClass, IntrospectedTable introspectedTable) {
for (Field field : topLevelClass.getFields()) {
FullyQualifiedJavaType type = field.getType();
// 替换加密字段类型
if (replaceTypes.contains(type.getShortName())) {
field.setType(new FullyQualifiedJavaType("java.lang.String"));
}
}
return true;
}
@Override
public boolean modelExampleClassGenerated(TopLevelClass topLevelClass, IntrospectedTable introspectedTable) {
List<InnerClass> innerClasses = topLevelClass.getInnerClasses();
for (InnerClass innerClass : innerClasses) {
List<Method> methods = innerClass.getMethods();
for (Method method : methods) {
List<Parameter> parameters = method.getParameters();
List<Parameter> replaceParams = new LinkedList<>();
for (int i = 0; i < parameters.size(); i++) {
Parameter parameter = parameters.get(i);
FullyQualifiedJavaType type = parameter.getType();
if(replaceTypes.contains(type.getShortName())){
// 将生成的加密属性参数替换为String类型参数
Parameter nParam = new Parameter(new FullyQualifiedJavaType("java.lang.String"),
parameter.getName());
System.out.println(111);
replaceParams.add(nParam);
}else{
replaceParams.add(parameter);
}
}
if(CollectionUtil.isNotEmpty(replaceParams)){
ReflectUtil.setFieldValue(method, "parameters", replaceParams);
}
}
}
return true;
}
/**
* 拦截主键字段
*/
@Override
public boolean modelPrimaryKeyClassGenerated(TopLevelClass topLevelClass, IntrospectedTable introspectedTable) {
return true;
}
}
完成编码,使用一下。 1.在原先的generator.xml里添加自定义的generator plugin。
<plugin type="CryptReplacePlugin" />
2.修改需要加密的字段javaType并指定typeHandler。
<table tableName="Account" domainObjectName="AccountDO">
<generatedKey column="id" sqlStatement="mysql" identity="true"/>
<columnOverride column="PASSWORD" javaType="MybatisCryptNumber" typeHandler="CryptNumberTypeHandler"/>
</table>
执行一下generator plugin,并测试通过。
以上。