Implementing Database Field Encryption with a MyBatis Plugin
The article demonstrates how to implement lightweight field-level encryption in MyBatis by adding encrypted columns, creating a custom interceptor that encrypts annotated parameters on write and decrypts them on read, handling executor, statement, parameter, and result set processing, and addressing pagination count queries.
This article explains how to use a MyBatis plugin to achieve encryption and decryption of specific database fields, addressing security and compliance requirements.
Requirement background : The company needs to encrypt certain plaintext columns stored in the database to prevent unauthorized access and personal data leakage. Because the project is no longer iterating, a lightweight solution using a MyBatis plugin was chosen to encrypt data on write and decrypt on read.
Solution overview : New encrypted columns are added alongside the original plaintext columns. The database schema, mapper.xml , and DO objects are updated. A custom MyBatis interceptor handles the encryption of the plaintext fields before they are persisted and the decryption of the encrypted fields after they are retrieved.
System architecture : The plugin can intercept four core MyBatis components:
Executor : Executes the actual SQL statements.
StatementHandler : Creates and holds ParameterHandler and ResultSetHandler .
ParameterHandler : Sets method parameters into the prepared statement.
ResultSetHandler : Converts JDBC result sets into Java objects.
MyBatis plugin basics :
public interface Interceptor {
Object intercept(Invocation invocation) throws Throwable;
Object plugin(Object target);
void setProperties(Properties properties);
}The plugin is configured in mybatis-config.xml :
<plugins>
<plugin interceptor="com.xx.xx.xxxInterceptor">
<property name="param1" value="value1"/>
</plugin>
</plugins>Interceptable class selection : To encrypt input parameters, the Executor ’s update and query methods are intercepted because the dynamic SQL is parsed into static SQL before StatementHandler.parameterize() is called, making it impossible to inject encrypted values earlier.
Spring‑MyBatis execution flow (simplified) :
Spring creates SqlSessionFactory via SqlSessionFactoryBean , loading configuration and mapper files.
Configuration files are parsed into Configuration and MappedStatement objects.
Mapper interfaces are scanned and turned into MapperFactoryBean beans.
MapperFactoryBean creates a JDK proxy ( MapperProxy ) for each mapper.
Calling a mapper method invokes MapperProxy.invoke , which delegates to SqlSession methods (insert/update/delete/query).
SqlSession retrieves the corresponding MappedStatement and passes it to Executor .
Executor creates a StatementHandler , which in turn creates ParameterHandler and ResultSetHandler .
The StatementHandler prepares the JDBC PreparedStatement , sets parameters via ParameterHandler , and executes the statement.
ResultSetHandler processes the result set and returns Java objects.
Plugin creation timing : The proxy chain is built in Configuration.newStatementHandler , newParameterHandler , newResultSetHandler , and newExecutor . For example:
public StatementHandler newStatementHandler(Executor executor, MappedStatement ms, Object param, RowBounds rb, ResultHandler rh, BoundSql bs) {
StatementHandler sh = new RoutingStatementHandler(executor, ms, param, rb, rh, bs);
sh = (StatementHandler) interceptorChain.pluginAll(sh);
return sh;
}Custom annotation used to mark methods that require encryption:
@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface TEncrypt {
String[] srcKey() default {};
String[] destKey() default {};
}Example DAO method:
public interface UserMapper {
@TEncrypt(srcKey = {"secret"}, destKey = {"secretCiper"})
List
selectUserList(UserInfo userInfo);
}Corresponding mapper.xml snippet (adding encrypted column mapping):
<resultMap id="BaseResultMap" type="com.xxx.internet.demo.entity.UserInfo">
<id column="id" property="id" jdbcType="BIGINT"/>
<id column="phone" property="phone" jdbcType="VARCHAR"/>
<id column="secret" property="secret" jdbcType="VARCHAR"/>
<!-- encrypted column -->
<id column="secret_ciper" property="secretCiper" jdbcType="VARCHAR"/>
<id column="name" property="name" jdbcType="VARCHAR"/>
</resultMap>
<select id="selectUserList" resultMap="BaseResultMap" parameterType="com.xxx.internet.demo.entity.UserInfo">
SELECT * FROM `t_user_info`
<where>
<if test="phone != null">`phone` = #{phone}</if>
<if test="secret != null">AND `secret` = #{secret}</if>
<if test="secretCiper != null">AND `secret_ciper` = #{secretCiper}</if>
<if test="name">AND `name` = #{name}</if>
</where>
ORDER BY `update_time` DESC
</select>Executor encryption interceptor (core logic):
@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 })
})
public class ExecutorEncryptInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
Object paramObj = args[1];
String id = ms.getId();
String className = id.substring(0, id.lastIndexOf('.'));
String methodName = id.substring(id.lastIndexOf('.') + 1);
if (methodName.endsWith("_COUNT")) {
methodName = methodName.substring(0, methodName.lastIndexOf("_COUNT"));
}
Method[] methods = Class.forName(className).getMethods();
for (Method method : methods) {
if (method.getName().equalsIgnoreCase(methodName) && method.isAnnotationPresent(TEncrypt.class)) {
TEncrypt anno = method.getAnnotation(TEncrypt.class);
if (paramObj instanceof Map) {
// handle map parameters
} else {
// encrypt object fields
}
}
}
return invocation.proceed();
}
}Encryption workflow :
Check whether the invoked method is annotated with @TEncrypt .
Retrieve the annotation parameters ( srcKey and destKey ).
Iterate over the parameterObject (Map, List, or POJO) to locate fields listed in srcKey .
Encrypt the field value and store the result in the corresponding destKey field.
Proceed with the original MyBatis execution.
Challenges : When using pagination plugins such as PageHelper , an additional COUNT query is generated (method ID ends with _COUNT ). Because this generated query lacks the @TEncrypt annotation, it would use the plaintext column and produce an incorrect total count. The interceptor therefore strips the _COUNT suffix, checks the original method for the annotation, and applies encryption to the count query as well.
Conclusion : The MyBatis plugin approach provides a lightweight, minimally invasive way to encrypt/decrypt database fields. Although it introduces some coupling to the DAO layer, the impact is limited, and the implementation deepens understanding of MyBatis’s internal execution flow.
vivo Internet Technology
Sharing practical vivo Internet technology insights and salon events, plus the latest industry news and hot conferences.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.