1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

MyBatisでデータを暗号化/複合化する

Last updated at Posted at 2022-02-27

SpringBoot+MyBatisでデータを暗号化/複合化したので備忘録として残します。

目的

  • エンティティの特定データを暗号化してDBに登録する。
  • 暗号化/複合化の実行はMyBatisの機能で行う。
  • MyBatisのMapperはXMLを使用する。

ライブラリ等

  • spring-boot-starter: 2.6.4
  • mybatis-spring-boot-starter: 2.2.2
  • lombok: 1.18.22

実装

  • 以下、パッケージ名は {package-name}と表記する。

データベースを作成

  • ID、名前、Eメールアドレスを持つユーザ情報。
  • 名前とEメールアドレスを暗号化する。
/src/main/resource/schema.sql
DROP TABLE IF EXISTS user;

CREATE TABLE `user` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(100) DEFAULT NULL,
  `email` varchar(256) DEFAULT NULL,
  PRIMARY KEY (`id`)
);
/src/main/java/{package-name}/entity/User.java
@Data
public class User {
    private int id;
    private String name;
    private String email;

    public User(String name, String email) {
        this.name = name;
        this.email = email;
    }
}

暗号化ロジックを作成

  • サンプルとしてシーザー暗号を採用した。
  • シーザー暗号は、アルファベットを決まった値だけシフトして暗号化する。
  • 例えば、シフト数2で Qiita を暗号化すると Tllwd になる。
/src/main/java/{package-name}/cipher/CaesarCipher.java
@Component
public class CaesarCipher {

    // シフトする値
    @Value("${caesar-cipher.shift:10}")
    private int shiftValue;

    // シフト
    private String shift(String str, int s) {
        if (s < 0) {
            s += 26;
        }

        StringBuilder sb = new StringBuilder();
        for (char ch : str.toCharArray()) {
            if (!Character.isAlphabetic(ch)) {
                sb.append(ch);
                continue;
            }

            int base = Character.isUpperCase(ch) ? 'A' : 'a';
            char newCh = (char) ((ch + s - base) % 26 + base);
            sb.append(newCh);
        }
        return sb.toString();
    }

    // 暗号化
    public String encipher(String text) {
        if (text == null) {
            return null;
        }

        return shift(text, shiftValue);
    }

    // 複合化
    public String decipher(String text) {
        if (text == null) {
            return null;
        }

        return shift(text, -shiftValue);
    }
}

TypeHandlerを作成

  • MyBatisのMapperでカラムに紐づけるためのTypeHandlerを作成する。
  • DBからデータを取得してエンティティに格納する際に実行される。
  • 暗号化するデータ型ごとに作成する必要がある。
/src/main/java/{package-name}/typehandler/CaesarCipherStringType.java
// DBに格納する際のDataTypeを定義
// AliasはMapper内で参照する名前
@Alias("CaesarCipherString")
public class CaesarCipherStringType {
}
/src/main/java/{package-name}/typehandler/CaesarCipherStringType.java
// 定義したDataTypeに紐づくTypeHandler
@MappedTypes(CaesarCipherStringType.class)
public class CaesarCipherStringTypeHandler extends BaseTypeHandler<String> {

    private final CaesarCipher caesarCipher;

    public CaesarCipherStringTypeHandler(CaesarCipher caesarCipher) {
        this.caesarCipher = caesarCipher;
    }

    // null以外の値をDBに保存(暗号化)
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType)
            throws SQLException {
        String str = caesarCipher.encipher(parameter);
        ps.setString(i, str);
    }

    // カラム名指定でDBから取得(複合化)
    @Override
    public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
        try {
            String str = rs.getString(columnName);
            if (str == null) {
                return null;
            }
            return caesarCipher.decipher(str);
        } catch (Exception e) {
            throw new SQLException(e);
        }
    }

    // カラム番号指定でDBから取得(複合化)
    @Override
    public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        try {
            String str = rs.getString(columnIndex);
            if (str == null) {
                return null;
            }
            return caesarCipher.decipher(str);
        } catch (Exception e) {
            throw new SQLException(e);
        }
    }

    // カラム番号指定でDBから取得(複合化)
    @Override
    public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        try {
            String str = cs.getString(columnIndex);
            if (str == null) {
                return null;
            }
            return caesarCipher.decipher(str);
        } catch (Exception e) {
            throw new SQLException(e);
        }
    }
}
/src/main/java/{package-name}/config/MyBatisConfig.java
@Configuration
@MapperScan("{package-name}.repository")
@RequiredArgsConstructor
public class MyBatisConfig {

    private final CaesarCipher caesarCipher;

    // MyBatisのTypeHandlerにCaesarCipherStringTypeHandlerを登録
    @Bean
    ConfigurationCustomizer mybatisConfigurationCustomizer() {
        return configuration -> {
            configuration.getTypeHandlerRegistry().register(new CaesarCipherStringTypeHandler(caesarCipher));
        };
    }
}
/src/main/resources/application.yml
mybatis:
  mapper-locations: classpath*:/mapper/*.xml
  type-aliases-package: {package-name}.typehandler

MapperでTypeHandlerを紐付け

  • 先ほど作成したTypeHandlerをMapperに紐づけて、暗号化/複合化する
/src/main/java/{package-name}/repository/UserRepository.java
@Repository
public interface UserRepository {
    // 複合かして取得
    User findById(int id);

    // 複合化せずに取得
    User findByIdWithoutDecipher(int id);

    // 暗号化して登録
    void insert(User user);
}

/src/main/resources/mapper/UserRepository.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="{name-space}.repository.UserRepository">

  <resultMap type="{name-space}.model.User" id="user">
    <id column="id" property="id" />
    <result column="name" property="name" javaType="CaesarCipherString" />    <!-- javaTypeで複合化方式を指定 -->
    <result column="email" property="email" javaType="CaesarCipherString" />
  </resultMap>

  <resultMap type="{name-space}.model.User" id="userWithoutDeCipher">
    <id column="id" property="id" />
    <result column="name" property="name" />
    <result column="email" property="email" />
  </resultMap>

  <select id="findById" resultMap="user">
    select id,
           name,
           email
    from user
    where id = #{id}
  </select>

  <select id="findByIdWithoutDecipher" resultMap="userWithoutDeCipher">
    select id,
           name,
           email
    from user
    where id = #{id}
  </select>

  <insert id="insert" useGeneratedKeys="true" keyProperty="id">
    insert into user (id,
                      name,
                      email)
    values (#{id},
            #{name, javaType=CaesarCipherString},    <!-- javaTypeで暗号化方式を指定 -->
            #{email, javaType=CaesarCipherString})
  </insert>
</mapper>

テストで動作確認

/test/resources/db/user.xml
<?xml version="1.0" encoding="UTF-8" ?>
<dataset>
  <user
    id="1"
    name="QDPH"
    email="qdph@hadpsoh.frp" />
</dataset>
/src/test/java/{name-space}/UserRepositoryTest.java
@SpringBootTest
@TestExecutionListeners({
        DependencyInjectionTestExecutionListener.class,
        DirtiesContextTestExecutionListener.class,
        TransactionalTestExecutionListener.class,
        DbUnitTestExecutionListener.class
})
public class UserRepositoryTest {

    @Autowired
    private UserRepository userRepository;

    // 複合化して取得
    @Test
    @DatabaseSetup("/db/user.xml")
    public void findById() {
        User user = userRepository.findById(1);
        assertThat(user.getId()).isEqualTo(1);
        assertThat(user.getName()).isEqualTo("NAME");
        assertThat(user.getEmail()).isEqualTo("name@example.com");
    }

    // 複合化せずに取得
    @Test
    @DatabaseSetup("/db/user.xml")
    public void findByIdWithoutDecipher() {
        User user = userRepository.findByIdWithoutDecipher(1);
        assertThat(user.getId()).isEqualTo(1);
        assertThat(user.getName()).isEqualTo("QDPH");
        assertThat(user.getEmail()).isEqualTo("qdph@hadpsoh.frp");
    }

    // 暗号化して登録
    @Test
    @DatabaseSetup("/db/user.xml")
    public void insert() {
        User user1 = new User("NAME", "name@example.com");
        userRepository.insert(user1);

        User user2 = userRepository.findById(user1.getId());
        assertThat(user2.getName()).isEqualTo("QDPH");
        assertThat(user2.getEmail()).isEqualTo("qdph@hadpsoh.frp");
    }
}

GitHub

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?