Java SpringBoot

一、介绍

在实际的软件系统开发过程中,由于业务的需求,在代码层面实现数据的脱敏还是远远不够的,往往还需要在数据库层面针对某些关键性的敏感信息,例如:身份证号、银行卡号、手机号、工资等信息进行加密存储,实现真正意义的数据混淆脱敏,以满足信息安全的需要。
那在实际的研发过程中,如何实践呢?

二、方案实践

在此,提供三套方案以供大家选择。

  • 通过 SQL 函数实现加解密
  • 对 SQL 进行解析拦截,实现数据加解密
  • 自定义一套脱敏工具

    2.1、通过 SQL 函数实现加解密

    最简单的方法,莫过于直接在数据库层面操作,通过函数对某个字段进行加、解密,例如如下这个案例! ```sql — 对“你好,世界”进行加密 select HEX(AES_ENCRYPT(‘你好,世界’,’ABC123456’));

— 解密,输出:你好,世界 select AES_DECRYPT(UNHEX(‘A174E3C13FE16AA0FD071A4BBD7CD7C5’),’ABC123456’);

  1. 采用Mysql内置的AES协议加、解密函数,密钥是ABC123456,可以很轻松的对某个字段实现加、解密。<br />如果是很小的需求,需要加密的数据就是指定的信息,此方法可行。<br />但是当需要加密的表字段非常多的时候,这个使用起来就比较鸡肋了,例如更改加密算法或者不同的部署环境配置不同的密钥,这个时候就不得不把所有的代码进行更改一遍。
  2. <a name="Pvh3F"></a>
  3. ### 2.2、对 SQL 进行解析拦截,实现数据加解密
  4. 通过上面的方案,可以发现最大的痛点就是加密算法和密钥都写死在SQL上了,因此可以将这块的服务从抽出来,在JDBC层面,当sql执行的时候,对其进行拦截处理。<br />Apache ShardingSphere 框架下的数据脱敏模块,它就可以实现这一需求,如果是SpringBoot项目,可以实现无缝集成,对原系统的改造会非常少。<br />下面以用户表为例,来看看采用ShardingSphere如何实现!
  5. <a name="MCuSu"></a>
  6. #### 2.2.1、创建用户表
  7. ```sql
  8. CREATE TABLE user (
  9. id bigint(20) NOT NULL COMMENT '用户ID',
  10. email varchar(255) NOT NULL DEFAULT '' COMMENT '邮件',
  11. nick_name varchar(255) DEFAULT NULL COMMENT '昵称',
  12. pass_word varchar(255) NOT NULL DEFAULT '' COMMENT '二次密码',
  13. reg_time varchar(255) NOT NULL DEFAULT '' COMMENT '注册时间',
  14. user_name varchar(255) NOT NULL DEFAULT '' COMMENT '用户名',
  15. salary varchar(255) DEFAULT NULL COMMENT '基本工资',
  16. PRIMARY KEY (id) USING BTREE
  17. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

2.2.2、创建 springboot 项目并添加依赖包

  1. <dependencies>
  2. <!--spring boot核心-->
  3. <dependency>
  4. <groupId>org.springframework.boot</groupId>
  5. <artifactId>spring-boot-starter</artifactId>
  6. </dependency>
  7. <!--spring boot 测试-->
  8. <dependency>
  9. <groupId>org.springframework.boot</groupId>
  10. <artifactId>spring-boot-starter-test</artifactId>
  11. <scope>test</scope>
  12. </dependency>
  13. <!--springmvc web-->
  14. <dependency>
  15. <groupId>org.springframework.boot</groupId>
  16. <artifactId>spring-boot-starter-web</artifactId>
  17. </dependency>
  18. <!--mysql 数据源-->
  19. <dependency>
  20. <groupId>mysql</groupId>
  21. <artifactId>mysql-connector-java</artifactId>
  22. </dependency>
  23. <!--mybatis 支持-->
  24. <dependency>
  25. <groupId>org.mybatis.spring.boot</groupId>
  26. <artifactId>mybatis-spring-boot-starter</artifactId>
  27. <version>2.0.0</version>
  28. </dependency>
  29. <!--shardingsphere数据分片、脱敏工具-->
  30. <dependency>
  31. <groupId>org.apache.shardingsphere</groupId>
  32. <artifactId>sharding-jdbc-spring-boot-starter</artifactId>
  33. <version>4.1.0</version>
  34. </dependency>
  35. <dependency>
  36. <groupId>org.apache.shardingsphere</groupId>
  37. <artifactId>sharding-jdbc-spring-namespace</artifactId>
  38. <version>4.1.0</version>
  39. </dependency>
  40. </dependencies>

2.2.3、添加脱敏配置

在application.properties文件中,添加shardingsphere相关配置,即可实现针对某个表进行脱敏

  1. server.port=8080
  2. logging.path=log
  3. #shardingsphere数据源集成
  4. spring.shardingsphere.datasource.name=ds
  5. spring.shardingsphere.datasource.ds.type=com.zaxxer.hikari.HikariDataSource
  6. spring.shardingsphere.datasource.ds.driver-class-name=com.mysql.cj.jdbc.Driver
  7. spring.shardingsphere.datasource.ds.jdbc-url=jdbc:mysql://127.0.0.1:3306/test
  8. spring.shardingsphere.datasource.ds.username=xxxx
  9. spring.shardingsphere.datasource.ds.password=xxxx
  10. #加密方式、密钥配置
  11. spring.shardingsphere.encrypt.encryptors.encryptor_aes.type=aes
  12. spring.shardingsphere.encrypt.encryptors.encryptor_aes.props.aes.key.value=hkiqAXU6Ur5fixGHaO4Lb2V2ggausYwW
  13. #plainColumn表示明文列,cipherColumn表示脱敏列
  14. spring.shardingsphere.encrypt.tables.user.columns.salary.plainColumn=
  15. spring.shardingsphere.encrypt.tables.user.columns.salary.cipherColumn=salary
  16. #spring.shardingsphere.encrypt.tables.user.columns.pass_word.assistedQueryColumn=
  17. spring.shardingsphere.encrypt.tables.user.columns.salary.encryptor=encryptor_aes
  18. #sql打印
  19. spring.shardingsphere.props.sql.show=true
  20. spring.shardingsphere.props.query.with.cipher.column=true
  21. #基于xml方法的配置
  22. mybatis.mapper-locations=classpath:mapper/*.xml

其中下面的配置信息是关键的一部,spring.shardingsphere.encrypt.tables是指要脱敏的表,user是表名,salary表示user表中的真实列,其中plainColumn指的是明文列,cipherColumn指的是脱敏列,如果是新工程,只需要配置脱敏列即可!

  1. spring.shardingsphere.encrypt.tables.user.columns.salary.plainColumn=
  2. spring.shardingsphere.encrypt.tables.user.columns.salary.cipherColumn=salary
  3. #spring.shardingsphere.encrypt.tables.user.columns.pass_word.assistedQueryColumn=
  4. spring.shardingsphere.encrypt.tables.user.columns.salary.encryptor=encryptor_aes

2.2.4、编写数据持久层

  1. <mapper namespace="com.example.shardingsphere.mapper.UserMapperXml" >
  2. <resultMap id="BaseResultMap" type="com.example.shardingsphere.entity.UserEntity" >
  3. <id column="id" property="id" jdbcType="BIGINT" />
  4. <result column="email" property="email" jdbcType="VARCHAR" />
  5. <result column="nick_name" property="nickName" jdbcType="VARCHAR" />
  6. <result column="pass_word" property="passWord" jdbcType="VARCHAR" />
  7. <result column="reg_time" property="regTime" jdbcType="VARCHAR" />
  8. <result column="user_name" property="userName" jdbcType="VARCHAR" />
  9. <result column="salary" property="salary" jdbcType="VARCHAR" />
  10. </resultMap>
  11. <select id="findAll" resultMap="BaseResultMap">
  12. SELECT * FROM user
  13. </select>
  14. <insert id="insert" parameterType="com.example.shardingsphere.entity.UserEntity">
  15. INSERT INTO user(id,email,nick_name,pass_word,reg_time,user_name, salary)
  16. VALUES(#{id},#{email},#{nickName},#{passWord},#{regTime},#{userName}, #{salary})
  17. </insert>
  18. </mapper>
  1. public interface UserMapperXml {
  2. /**
  3. * 查询所有的信息
  4. * @return
  5. */
  6. List<UserEntity> findAll();
  7. /**
  8. * 新增数据
  9. * @param user
  10. */
  11. void insert(UserEntity user);
  12. }
  13. public class UserEntity {
  14. private Long id;
  15. private String email;
  16. private String nickName;
  17. private String passWord;
  18. private String regTime;
  19. private String userName;
  20. private String salary;
  21. //省略set、get...
  22. }

2.2.5、最后来测试一下程序运行情况

编写启用服务程序

  1. @SpringBootApplication
  2. @MapperScan("com.example.shardingsphere.mapper")
  3. public class ShardingSphereApplication {
  4. public static void main(String[] args) {
  5. SpringApplication.run(ShardingSphereApplication.class, args);
  6. }
  7. }

编写单元测试

  1. @RunWith(SpringJUnit4ClassRunner.class)
  2. @SpringBootTest(classes = ShardingSphereApplication.class)
  3. public class UserTest {
  4. @Autowired
  5. private UserMapperXml userMapperXml;
  6. @Test
  7. public void insert() throws Exception {
  8. UserEntity entity = new UserEntity();
  9. entity.setId(3l);
  10. entity.setEmail("123@123.com");
  11. entity.setNickName("阿三");
  12. entity.setPassWord("123");
  13. entity.setRegTime("2021-10-10 00:00:00");
  14. entity.setUserName("张三");
  15. entity.setSalary("2500");
  16. userMapperXml.insert(entity);
  17. }
  18. @Test
  19. public void query() throws Exception {
  20. List<UserEntity> dataList = userMapperXml.findAll();
  21. System.out.println(JSON.toJSONString(dataList));
  22. }
  23. }

插入数据后,如下图,数据库存储的数据已被加密!
SpringBoot项目中敏感数据读写 - 图1
继续来看看,运行查询服务,结果如下图,数据被成功解密!
SpringBoot项目中敏感数据读写 - 图2
采用配置方式,最大的好处就是直接通过配置脱敏列就可以完成对某些数据表字段的脱敏,非常方便。

2.3、自定义一套脱敏工具

可能会觉得shardingsphere配置虽然简单,但是还是不放心,里面的很多规则自己无法掌控,想自己开发一套数据库的脱敏工具。
方案也是有的,例如如下这套实践方案,以Mybatis为例:

  • 首先编写一套加解密的算法工具类
  • 通过Mybatis的typeHandler插件,实现特定字段的加解密

实践过程如下:

2.3.1、加解密工具类

  1. public class AESCryptoUtil {
  2. private static final Logger log = LoggerFactory.getLogger(AESCryptoUtil.class);
  3. private static final String DEFAULT_ENCODING = "UTF-8";
  4. private static final String AES = "AES";
  5. /**
  6. * 加密
  7. *
  8. * @param content 需要加密内容
  9. * @param key 任意字符串
  10. * @return
  11. * @throws Exception
  12. */
  13. public static String encryptByRandomKey(String content, String key) {
  14. try {
  15. //构造密钥生成器,生成一个128位的随机源,产生原始对称密钥
  16. KeyGenerator keygen = KeyGenerator.getInstance(AES);
  17. SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
  18. random.setSeed(key.getBytes());
  19. keygen.init(128, random);
  20. byte[] raw = keygen.generateKey().getEncoded();
  21. SecretKey secretKey = new SecretKeySpec(raw, AES);
  22. Cipher cipher = Cipher.getInstance(AES);
  23. cipher.init(Cipher.ENCRYPT_MODE, secretKey);
  24. byte[] encrypted = cipher.doFinal(content.getBytes("utf-8"));
  25. return Base64.getEncoder().encodeToString(encrypted);
  26. } catch (Exception e) {
  27. log.warn("AES加密失败,参数:{},错误信息:{}", content, e);
  28. return "";
  29. }
  30. }
  31. public static String decryptByRandomKey(String content, String key) {
  32. try {
  33. //构造密钥生成器,生成一个128位的随机源,产生原始对称密钥
  34. KeyGenerator generator = KeyGenerator.getInstance(AES);
  35. SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
  36. random.setSeed(key.getBytes());
  37. generator.init(128, random);
  38. SecretKey secretKey = new SecretKeySpec(generator.generateKey().getEncoded(), AES);
  39. Cipher cipher = Cipher.getInstance(AES);
  40. cipher.init(Cipher.DECRYPT_MODE, secretKey);
  41. byte[] encrypted = Base64.getDecoder().decode(content);
  42. byte[] original = cipher.doFinal(encrypted);
  43. return new String(original, DEFAULT_ENCODING);
  44. } catch (Exception e) {
  45. log.warn("AES解密失败,参数:{},错误信息:{}", content, e);
  46. return "";
  47. }
  48. }
  49. public static void main(String[] args) {
  50. String encryptResult = encryptByRandomKey("Hello World", "123456");
  51. System.out.println(encryptResult);
  52. String decryptResult = decryptByRandomKey(encryptResult, "123456");
  53. System.out.println(decryptResult);
  54. }
  55. }

2.3.2、针对 salary 字段进行单独解析

  1. <mapper namespace="com.example.shardingsphere.mapper.UserMapperXml" >
  2. <resultMap id="BaseResultMap" type="com.example.shardingsphere.entity.UserEntity" >
  3. <id column="id" property="id" jdbcType="BIGINT" />
  4. <result column="email" property="email" jdbcType="VARCHAR" />
  5. <result column="nick_name" property="nickName" jdbcType="VARCHAR" />
  6. <result column="pass_word" property="passWord" jdbcType="VARCHAR" />
  7. <result column="reg_time" property="regTime" jdbcType="VARCHAR" />
  8. <result column="user_name" property="userName" jdbcType="VARCHAR" />
  9. <result column="salary" property="salary" jdbcType="VARCHAR"
  10. typeHandler="com.example.shardingsphere.handle.EncryptDataRuleTypeHandler"/>
  11. </resultMap>
  12. <select id="findAll" resultMap="BaseResultMap">
  13. select * from user
  14. </select>
  15. <insert id="insert" parameterType="com.example.shardingsphere.entity.UserEntity">
  16. INSERT INTO user(id,email,nick_name,pass_word,reg_time,user_name, salary)
  17. VALUES(
  18. #{id},
  19. #{email},
  20. #{nickName},
  21. #{passWord},
  22. #{regTime},
  23. #{userName},
  24. #{salary,jdbcType=INTEGER,typeHandler=com.example.shardingsphere.handle.EncryptDataRuleTypeHandler})
  25. </insert>
  26. </mapper>

EncryptDataRuleTypeHandler解析器,内容如下:

  1. public class EncryptDataRuleTypeHandler implements TypeHandler<String> {
  2. private static final String EMPTY = "";
  3. /**
  4. * 写入数据
  5. * @param preparedStatement
  6. * @param i
  7. * @param data
  8. * @param jdbcType
  9. * @throws SQLException
  10. */
  11. @Override
  12. public void setParameter(PreparedStatement preparedStatement, int i, String data, JdbcType jdbcType) throws SQLException {
  13. if (StringUtils.isEmpty(data)) {
  14. preparedStatement.setString(i, EMPTY);
  15. } else {
  16. preparedStatement.setString(i, AESCryptoUtil.encryptByRandomKey(data, "123456"));
  17. }
  18. }
  19. /**
  20. * 读取数据
  21. * @param resultSet
  22. * @param columnName
  23. * @return
  24. * @throws SQLException
  25. */
  26. @Override
  27. public String getResult(ResultSet resultSet, String columnName) throws SQLException {
  28. return decrypt(resultSet.getString(columnName));
  29. }
  30. /**
  31. * 读取数据
  32. * @param resultSet
  33. * @param columnIndex
  34. * @return
  35. * @throws SQLException
  36. */
  37. @Override
  38. public String getResult(ResultSet resultSet, int columnIndex) throws SQLException {
  39. return decrypt(resultSet.getString(columnIndex));
  40. }
  41. /**
  42. * 读取数据
  43. * @param callableStatement
  44. * @param columnIndex
  45. * @return
  46. * @throws SQLException
  47. */
  48. @Override
  49. public String getResult(CallableStatement callableStatement, int columnIndex) throws SQLException {
  50. return decrypt(callableStatement.getString(columnIndex));
  51. }
  52. /**
  53. * 对数据进行解密
  54. * @param data
  55. * @return
  56. */
  57. private String decrypt(String data) {
  58. return AESCryptoUtil.decryptByRandomKey(data, "123456");
  59. }
  60. }

2.3.3、单元测试

再次运行单元测试,程序读写正常!
SpringBoot项目中敏感数据读写 - 图3
SpringBoot项目中敏感数据读写 - 图4
通过如下的方式,也可以实现对数据表中某个特定字段进行数据脱敏处理!