[Mybatis] Implementing sensitive data encryption based on TypeHandler

1. Introduction

In business scenarios, we often encounter information such as user mobile phone numbers, ID numbers, bank card numbers, email addresses, passwords, etc., which are sensitive information and need to be stored in the database. Many companies will require encrypted storage of such data in the database.

There are two issues that need to be addressed when desensitizing sensitive data:

  1. Query operation requires encrypting the query keywords and decrypting the data found from the database.
  2. Insertion and update operations require the inserted or updated data to be encrypted and then saved to the database.

2. Solution ideas

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

Typehandler is an interface provided by mybatis. By implementing this interface, the conversion of jdbc type data and java type data can be realized. We often see varchar to string and bigint to conversion. Long, etc. are handled by mybatis itself by implementing this interface.

Therefore, you can implement a Typehandler yourself to meet your own data processing needs.

Advantages: It is simple to implement and easy to use. The entire use process only requires modification of the xml code

3. Implementation

1. Encryption and decryption method: The encryption and decryption method here directly uses des encryption of hutool.
 <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
  
  <!-- hutool-all includes encryption tools -->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.15</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        
        <!-- mybatis -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.2</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.33</version>
        </dependency>
2. Write a custom TypeHandler, inherited from BaseTypeHandler
package com.zsx.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;

/**
 * @Description typeHandler encryption and decryption processor Encrypts or decrypts String type fields
 * @author zhousx
 * @Data 2023-10-15 13:02
 */
@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);
 
 
    /*
     * Processed into 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 based on the column name, and the return value can be processed 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;
    }
 
    /*
     * Obtain the return result based on the column subscript, and the return value can be processed 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) based on 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;
    }
 
}
3. Register custom typeHandler to mybatis

application.yml

server:
  port: 8082

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/test?serverTimezone=GMT+8 & characterEncoding=utf8 & useSSL=true
    username: root
    password: 123456


mybatis:
  # The written TypeHandler needs to be registered in mybatis
  type-handlers-package: com.zsx.cryptotypehandler.common
  mapper-locations: classpath:mapper/*.xml
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
4. Use

Entity class User.java

package com.zsx.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {<!-- -->


    private int id;

    private String name;

    private String phone;
}

dao layer IUserDao.java

package com.zsx.dao;
 
import com.zsx.entity.User;
import org.apache.ibatis.annotations.Mapper;
 
import java.util.List;

/**
 * userMapper
 */
@Mapper
public interface IUserDao {<!-- -->
 
    int insertEncrypt(User user);
 
    List<User> findByName(User user);

    List<User> findByPhone(User user);

    List<User> findByPhone2(String phone);

}

xml file UserMapper.xml

<?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.zsx.dao.IUserDao">

    <!-- General query mapping results -->
    <resultMap id="BaseResultMap" type="com.zsx.entity.User">
        <id column="id" property="id" />
        <result column="name" property="name"/>
        <!-- Fields that need to be decrypted in the result set, plus typeHandler -->
        <result column="phone" property="phone" typeHandler="com.zsx.common.CryptoTypeHandler" />
    </resultMap>

 <!-- The fields that need to be encrypted in the sql parameters are added with typeHandler -->
    <insert id="insertEncrypt">
        insert into user (id, name, phone)
        values (#{id}, #{name}, #{phone,typeHandler=com.zsx.common.CryptoTypeHandler})
    </insert>

    <select id="findByName" resultMap="BaseResultMap">
        select id, name, phone
        from user
        where name = #{name}
    </select>

<!-- The fields that need to be encrypted in the sql parameters are added with typeHandler -->
    <select id="findByPhone" resultMap="BaseResultMap">
        select id, name, phone
        from user
        where phone = #{phone,typeHandler=com.zsx.common.CryptoTypeHandler}
    </select>

<!-- The fields that need to be encrypted in the sql parameters are added with typeHandler -->
    <select id="findByPhone2" resultMap="BaseResultMap">
        select id, name, phone
        from user
        where phone = #{phone,typeHandler=com.zsx.common.CryptoTypeHandler}
    </select>

</mapper>

The final test code EncryptTypeHandlerTest.java

package com.zsx;

import com.zsx.dao.IUserDao;
import com.zsx.entity.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.List;

@SpringBootTest
public class EncryptTypeHandlerTest {<!-- -->

    @Autowired
    private IUserDao userDao;

    @Test
    public void testInsert(){<!-- -->
        User user = new User();
        user.setPhone("11112222333");
        user.setName("zsx");
        int result = userDao.insertEncrypt(user);
        System.out.println(result);
    }

    @Test
    public void testSelectByName(){<!-- -->
        User user = new User();
        user.setName("zsx");
        List<User> userList = userDao.findByName(user);
        System.out.println(userList.toString());
    }

    @Test
    public void testSelectByPhone(){<!-- -->
        User user = new User();
        user.setPhone("11112222333");
        List<User> userList = userDao.findByPhone(user);
        System.out.println(userList.toString());
    }

    @Test
    public void testSelectByPhone2(){<!-- -->
        List<User> userList = userDao.findByPhone2("11112222333");
        System.out.println(userList.toString());
    }

}

The project structure is as follows

Note:
The results of sql query must be mapped using resultMap, otherwise the automatic decryption of the result field cannot be completed