Use Mybatis' TypeHandler to process data encryption and decryption

1. Background

Some projects need to encrypt some information before storage, for data security or privacy compliance, but at the same time, it also makes data processing cumbersome, which will inevitably lead to repetitive and lengthy codes. If the data can be processed in the persistence layer and avoided in the business layer, this problem can be reasonably avoided.

Two, the program

Use the TypeHandler provided by the mybatis framework to process data in the persistence layer.

Introduction to TypeHandler

Typehandler is an interface provided by mybatis. By implementing this interface, the conversion between jdbc type data and java type data can be realized. The conversion of varchar to string and bigint to long that we often see are all handled by mybatis itself.

​​​​​​​

We can implement a Typehandler ourselves to meet our needs.

3. Detailed implementation

1. Implement the interface and define your own Typehandler

Generally, you can implement the BaseTypeHandler interface. The author's encryption and decryption use the des encryption provided by hutool, and the maven coordinates are as follows:

<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.10</version>
</dependency>

The code is as follows, and the method can be written according to the business requirements to realize the code:

package com.example.cryptotypehandler.common;

import cn.hutool.crypto.symmetric.SymmetricAlgorithm;
import cn.hutool.crypto.symmetric.SymmetricCrypto;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;

import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

@Slf4j
@MappedJdbcTypes(JdbcType.VARCHAR)
@MappedTypes(String.class)
public class CryptoTypeHandler extends BaseTypeHandler<String> {

    private final byte[] key = {-26, -70, -29, -99, 73, -82, 91, -50, 79, -77, 59, 104, 2, -36, 50, -22, -39, -15, -57, -89, 81, -99, 42, -89};

    private final SymmetricCrypto des = new SymmetricCrypto(SymmetricAlgorithm.DESede, key);


    /*
     * Processing ginseng
     */
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
        if (parameter != null) {
            //encryption
            String encryptHex = des.encryptHex(parameter);
            log.info("{} ---encrypted as---> {}", parameter, encryptHex);
            ps.setString(i, encryptHex);
        }
    }

    /*
     * Get the return result according to the column name, you can process the return value in this method
     */
    @Override
    public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
        String originRes = rs.getString(columnName);
        if (originRes != null) {
            String res = des.decryptStr(originRes);
            log.info("{} ---decrypted as---> {}", originRes, res);
            return res;
        }
        log.info("The result is empty, no need to decrypt");
        return null;
    }

    /*
     * Get the return result according to the subscript of the column, you can process the return value in this method
     */
    @Override
    public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        String originRes = rs.getString(columnIndex);
        if (originRes != null) {
            String res = des.decryptStr(originRes);
            log.info("{} ---decrypted as---> {}", originRes, res);
            return res;
        }
        log.info("The result is empty, no need to decrypt");
        return null;
    }

    /*
     * Obtain the return result (stored procedure) according to the column subscript, and the return value can be processed in this method
     */
    @Override
    public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        String originRes = cs.getString(columnIndex);
        if (originRes != null) {
            String res = des.decryptStr(originRes);
            log.info("{} ---decrypted as---> {}", originRes, res);
            return res;
        }
        log.info("The result is empty, no need to decrypt");
        return null;
    }

}

2. Register a custom TypeHandler

The written TypeHandler needs to be registered in mybatis, and the configuration is added in application.yml or application.properties:

properties file:

mybatis.type-handlers-package=com.example.cryptotypehandler.common

yml file

mybatis:
  type-handlers-package: com.example.cryptotypehandler.common

The author's package structure is as follows

3. Define mapper layer interface

entity object class

package com.example.cryptotypehandler.domain;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Getter
@Setter
@ToString
public class AccountDO {

    private Long id;

    /**
     * username
     */
    private String userName;

    /**
     * password
     */
    private String password;
    
}

It is no different from ordinary mapper:

package com.example.cryptotypehandler.mapper;

import com.example.cryptotypehandler.domain.AccountDO;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

/**
 * <p>
 *  Mapper interface
 * </p>
 *
 * @author shuai.mh
 * @since 2022-11-29
 */
@Mapper
public interface AccountMapper {

    int insertEncrypt(AccountDO accountDO);

    List<AccountDO> selectAccount(AccountDO accountDO);
}

4. Write mapper.xml

Here are a few points to note:

  1. First look at the insert statement, what we need to encrypt is the password field, so add typeHandler=com.example.cryptotypehandler.common.CryptoTypeHandler after password; in addition, in #{userName}, do not fill in jdbcType=varchar, because the custom typerHandler The following two annotations are added: @MappedJdbcTypes(JdbcType.VARCHAR) and @MappedTypes(String.class). These two annotations indicate that if the JdbcType is varchar, this handler will be used. If added, userName will also be encrypted.
  2. Looking at the select statement again, there is no difference between sql and ordinary ones, but the password mapping in the resultMap adds typeHandler="com.example.cryptotypehandler.common.CryptoTypeHandler", which means that this field will be processed by the handler when it is converted into an entity object, and other The mapping jdbcType of the field remains the default, and if it is varchar, it will also be processed.
<?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.example.cryptotypehandler.mapper.AccountMapper">

    <!-- Generic Query Mapping Results -->
    <resultMap id="BaseResultMap" type="com.example.cryptotypehandler.domain.AccountDO">
        <id column="id" property="id" />
        <result column="user_name" property="userName"/>
        <result column="password" property="password" typeHandler="com.example.cryptotypehandler.common.CryptoTypeHandler" />
    </resultMap>

    <!-- Common Query Result Columns -->
    <sql id="Base_Column_List">
        id, user_name, password
    </sql>

    <insert id="insertEncrypt">
        insert into account (id, user_name, password)
        values (#{id}, #{userName}, #{password, typeHandler=com.example.cryptotypehandler.common.CryptoTypeHandler})
    </insert>

    <select id="selectAccount" resultMap="BaseResultMap">
        select <include refid="Base_Column_List"></include>
        from account
        where user_name = #{userName}
    </select>

</mapper>

5. Call interface, simple test

①Add new user

② Check the database, the password has been encrypted

③Query the user, the query result has been decrypted

Thanks for reading, welcome to communicate! Ask for attention (〃'▽'〃)

Tags: Mybatis

Posted by somo on Sat, 03 Dec 2022 05:48:44 +0530