Mybatis自动加解密

一、背景

在开发过程中,我们经常要对敏感信息做加密。例如: 账号、密码、银行卡号等。在目前项目的开发过程中,我们使用手动显示的方式调用加解密。但是当某个表需要加密的字段变多时,需要手动调用加解密的方式太多,过于繁琐,遂想有没有办法来实现非手动调用的自动加解密。

二、思路

先决条件: 加解密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,并测试通过。

以上。