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