50行代碼,搞定敏感數據讀寫!
一、介紹
在實際的軟件系統開發過程中,由于業務的需求,在代碼層面實現數據的脫敏還是遠遠不夠的,往往還需要在數據庫層面針對某些關鍵性的敏感信息,例如:身份證號、銀行卡號、手機號、工資等信息進行加密存儲,實現真正意義的數據混淆脫敏,以滿足信息安全的需要。
那在實際的研發過程中,我們如何實踐呢?
二、方案實踐
在此,提供三套方案以供大家選擇。
- 通過 SQL 函數實現加解密
- 對 SQL 進行解析攔截,實現數據加解密
- 自定義一套脫敏工具
1. 通過 SQL 函數實現加解密
最簡單的方法,莫過于直接在數據庫層面操作,通過函數對某個字段進行加、解密,例如如下這個案例!
- -- 對“你好,世界”進行加密
- select HEX(AES_ENCRYPT('你好,世界','ABC123456'));
- -- 解密,輸出:你好,世界
- select AES_DECRYPT(UNHEX('A174E3C13FE16AA0FD071A4BBD7CD7C5'),'ABC123456');
采用MySQL內置的AES協議加、解密函數,密鑰是ABC123456,可以很輕松的對某個字段實現加、解密。
如果是很小的需求,需要加密的數據就是指定的信息,此方法可行。
但是當需要加密的表字段非常多的時候,這個使用起來就比較雞肋了,例如我想更改加密算法或者不同的部署環境配置不同的密鑰,這個時候就不得不把所有的代碼進行更改一遍。
2. 對 SQL 進行解析攔截,實現數據加解密
通過上面的方案,我們發現最大的痛點就是加密算法和密鑰都寫死在SQL上了,因此我們可以將這塊的服務從抽出來,在JDBC層面,當sql執行的時候,對其進行攔截處理。
Apache ShardingSphere 框架下的數據脫敏模塊,它就可以幫助我們實現這一需求,如果你是SpringBoot項目,可以實現無縫集成,對原系統的改造會非常少。
下面以用戶表為例,我們來看看采用ShardingSphere如何實現!
(1) 創建用戶表
- CREATE TABLE user (
- id bigint(20) NOT NULL COMMENT '用戶ID',
- email varchar(255) NOT NULL DEFAULT '' COMMENT '郵件',
- nick_name varchar(255) DEFAULT NULL COMMENT '昵稱',
- pass_word varchar(255) NOT NULL DEFAULT '' COMMENT '二次密碼',
- reg_time varchar(255) NOT NULL DEFAULT '' COMMENT '注冊時間',
- user_name varchar(255) NOT NULL DEFAULT '' COMMENT '用戶名',
- salary varchar(255) DEFAULT NULL COMMENT '基本工資',
- PRIMARY KEY (id) USING BTREE
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
(2) 創建 springboot 項目并添加依賴包
- <dependencies>
- <!--spring boot核心-->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter</artifactId>
- </dependency>
- <!--spring boot 測試-->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-test</artifactId>
- <scope>test</scope>
- </dependency>
- <!--springmvc web-->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-web</artifactId>
- </dependency>
- <!--mysql 數據源-->
- <dependency>
- <groupId>mysql</groupId>
- <artifactId>mysql-connector-java</artifactId>
- </dependency>
- <!--mybatis 支持-->
- <dependency>
- <groupId>org.mybatis.spring.boot</groupId>
- <artifactId>mybatis-spring-boot-starter</artifactId>
- <version>2.0.0</version>
- </dependency>
- <!--shardingsphere數據分片、脫敏工具-->
- <dependency>
- <groupId>org.apache.shardingsphere</groupId>
- <artifactId>sharding-jdbc-spring-boot-starter</artifactId>
- <version>4.1.0</version>
- </dependency>
- <dependency>
- <groupId>org.apache.shardingsphere</groupId>
- <artifactId>sharding-jdbc-spring-namespace</artifactId>
- <version>4.1.0</version>
- </dependency>
- </dependencies>
(3) 添加脫敏配置
在application.properties文件中,添加shardingsphere相關配置,即可實現針對某個表進行脫敏。
- server.port=8080
- loglogging.path=log
- #shardingsphere數據源集成
- spring.shardingsphere.datasource.name=ds
- spring.shardingsphere.datasource.ds.type=com.zaxxer.hikari.HikariDataSource
- spring.shardingsphere.datasource.ds.driver-class-name=com.mysql.cj.jdbc.Driver
- spring.shardingsphere.datasource.ds.jdbc-url=jdbc:mysql://127.0.0.1:3306/test
- spring.shardingsphere.datasource.ds.username=xxxx
- spring.shardingsphere.datasource.ds.password=xxxx
- #加密方式、密鑰配置
- spring.shardingsphere.encrypt.encryptors.encryptor_aes.type=aes
- spring.shardingsphere.encrypt.encryptors.encryptor_aes.props.aes.key.value=hkiqAXU6Ur5fixGHaO4Lb2V2ggausYwW
- #plainColumn表示明文列,cipherColumn表示脫敏列
- springspring.shardingsphere.encrypt.tables.user.columns.salary.plainColumn=
- spring.shardingsphere.encrypt.tables.user.columns.salary.cipherColumn=salary
- #springspring.shardingsphere.encrypt.tables.user.columns.pass_word.assistedQueryColumn=
- spring.shardingsphere.encrypt.tables.user.columns.salary.encryptor=encryptor_aes
- #sql打印
- spring.shardingsphere.props.sql.show=true
- spring.shardingsphere.props.query.with.cipher.column=true
- #基于xml方法的配置
- mybatis.mapper-locations=classpath:mapper/*.xml
其中下面的配置信息是關鍵的一部,spring.shardingsphere.encrypt.tables是指要脫敏的表,user是表名,salary表示user表中的真實列,其中plainColumn指的是明文列,cipherColumn指的是脫敏列,如果是新工程,只需要配置脫敏列即可!
- springspring.shardingsphere.encrypt.tables.user.columns.salary.plainColumn=
- spring.shardingsphere.encrypt.tables.user.columns.salary.cipherColumn=salary
- #springspring.shardingsphere.encrypt.tables.user.columns.pass_word.assistedQueryColumn=
- spring.shardingsphere.encrypt.tables.user.columns.salary.encryptor=encryptor_aes
(4) 編寫數據持久層
- <mapper namespace="com.example.shardingsphere.mapper.UserMapperXml" >
- <resultMap id="BaseResultMap" type="com.example.shardingsphere.entity.UserEntity" >
- <id column="id" property="id" jdbcType="BIGINT" />
- <result column="email" property="email" jdbcType="VARCHAR" />
- <result column="nick_name" property="nickName" jdbcType="VARCHAR" />
- <result column="pass_word" property="passWord" jdbcType="VARCHAR" />
- <result column="reg_time" property="regTime" jdbcType="VARCHAR" />
- <result column="user_name" property="userName" jdbcType="VARCHAR" />
- <result column="salary" property="salary" jdbcType="VARCHAR" />
- </resultMap>
- <select id="findAll" resultMap="BaseResultMap">
- SELECT * FROM user
- </select>
- <insert id="insert" parameterType="com.example.shardingsphere.entity.UserEntity">
- INSERT INTO user(id,email,nick_name,pass_word,reg_time,user_name, salary)
- VALUES(#{id},#{email},#{nickName},#{passWord},#{regTime},#{userName}, #{salary})
- </insert>
- </mapper>
- public interface UserMapperXml {
- /**
- * 查詢所有的信息
- * @return
- */
- List<UserEntity> findAll();
- /**
- * 新增數據
- * @param user
- */
- void insert(UserEntity user);
- }
- public class UserEntity {
- private Long id;
- private String email;
- private String nickName;
- private String passWord;
- private String regTime;
- private String userName;
- private String salary;
- //省略set、get...
- }
(5) 最后我們來測試一下程序運行情況
編寫啟用服務程序:
- @SpringBootApplication
- @MapperScan("com.example.shardingsphere.mapper")
- public class ShardingSphereApplication {
- public static void main(String[] args) {
- SpringApplication.run(ShardingSphereApplication.class, args);
- }
- }
編寫單元測試:
- @RunWith(SpringJUnit4ClassRunner.class)
- @SpringBootTest(classes = ShardingSphereApplication.class)
- public class UserTest {
- @Autowired
- private UserMapperXml userMapperXml;
- @Test
- public void insert() throws Exception {
- UserEntity entity = new UserEntity();
- entity.setId(3l);
- entity.setEmail("123@123.com");
- entity.setNickName("阿三");
- entity.setPassWord("123");
- entity.setRegTime("2021-10-10 00:00:00");
- entity.setUserName("張三");
- entity.setSalary("2500");
- userMapperXml.insert(entity);
- }
- @Test
- public void query() throws Exception {
- List<UserEntity> dataList = userMapperXml.findAll();
- System.out.println(JSON.toJSONString(dataList));
- }
- }
插入數據后,如下圖,數據庫存儲的數據已被加密!
我們繼續來看看,運行查詢服務,結果如下圖,數據被成功解密!
采用配置方式,最大的好處就是直接通過配置脫敏列就可以完成對某些數據表字段的脫敏,非常方便。
3. 自定義一套脫敏工具
當然,有的同學可能會覺得shardingsphere配置雖然簡單,但是還是不放心,里面的很多規則自己無法掌控,想自己開發一套數據庫的脫敏工具。
方案也是有的,例如如下這套實踐方案,以Mybatis為例:
- 首先編寫一套加解密的算法工具類
- 通過Mybatis的typeHandler插件,實現特定字段的加解密
實踐過程如下:
(1) 加解密工具類
- public class AESCryptoUtil {
- private static final Logger log = LoggerFactory.getLogger(AESCryptoUtil.class);
- private static final String DEFAULT_ENCODING = "UTF-8";
- private static final String AES = "AES";
- /**
- * 加密
- *
- * @param content 需要加密內容
- * @param key 任意字符串
- * @return
- * @throws Exception
- */
- public static String encryptByRandomKey(String content, String key) {
- try {
- //構造密鑰生成器,生成一個128位的隨機源,產生原始對稱密鑰
- KeyGenerator keygen = KeyGenerator.getInstance(AES);
- SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
- random.setSeed(key.getBytes());
- keygen.init(128, random);
- byte[] raw = keygen.generateKey().getEncoded();
- SecretKey secretKey = new SecretKeySpec(raw, AES);
- Cipher cipher = Cipher.getInstance(AES);
- cipher.init(Cipher.ENCRYPT_MODE, secretKey);
- byte[] encrypted = cipher.doFinal(content.getBytes("utf-8"));
- return Base64.getEncoder().encodeToString(encrypted);
- } catch (Exception e) {
- log.warn("AES加密失敗,參數:{},錯誤信息:{}", content, e);
- return "";
- }
- }
- public static String decryptByRandomKey(String content, String key) {
- try {
- //構造密鑰生成器,生成一個128位的隨機源,產生原始對稱密鑰
- KeyGenerator generator = KeyGenerator.getInstance(AES);
- SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
- random.setSeed(key.getBytes());
- generator.init(128, random);
- SecretKey secretKey = new SecretKeySpec(generator.generateKey().getEncoded(), AES);
- Cipher cipher = Cipher.getInstance(AES);
- cipher.init(Cipher.DECRYPT_MODE, secretKey);
- byte[] encrypted = Base64.getDecoder().decode(content);
- byte[] original = cipher.doFinal(encrypted);
- return new String(original, DEFAULT_ENCODING);
- } catch (Exception e) {
- log.warn("AES解密失敗,參數:{},錯誤信息:{}", content, e);
- return "";
- }
- }
- public static void main(String[] args) {
- String encryptResult = encryptByRandomKey("Hello World", "123456");
- System.out.println(encryptResult);
- String decryptResult = decryptByRandomKey(encryptResult, "123456");
- System.out.println(decryptResult);
- }
- }
2.3.2、針對 salary 字段進行單獨解析
- <mapper namespace="com.example.shardingsphere.mapper.UserMapperXml" >
- <resultMap id="BaseResultMap" type="com.example.shardingsphere.entity.UserEntity" >
- <id column="id" property="id" jdbcType="BIGINT" />
- <result column="email" property="email" jdbcType="VARCHAR" />
- <result column="nick_name" property="nickName" jdbcType="VARCHAR" />
- <result column="pass_word" property="passWord" jdbcType="VARCHAR" />
- <result column="reg_time" property="regTime" jdbcType="VARCHAR" />
- <result column="user_name" property="userName" jdbcType="VARCHAR" />
- <result column="salary" property="salary" jdbcType="VARCHAR"
- typeHandler="com.example.shardingsphere.handle.EncryptDataRuleTypeHandler"/>
- </resultMap>
- <select id="findAll" resultMap="BaseResultMap">
- select * from user
- </select>
- <insert id="insert" parameterType="com.example.shardingsphere.entity.UserEntity">
- INSERT INTO user(id,email,nick_name,pass_word,reg_time,user_name, salary)
- VALUES(
- #{id},
- #{email},
- #{nickName},
- #{passWord},
- #{regTime},
- #{userName},
- #{salary,jdbcType=INTEGER,typeHandler=com.example.shardingsphere.handle.EncryptDataRuleTypeHandler})
- </insert>
- </mapper>
EncryptDataRuleTypeHandler解析器,內容如下:
- public class EncryptDataRuleTypeHandler implements TypeHandler<String> {
- private static final String EMPTY = "";
- /**
- * 寫入數據
- * @param preparedStatement
- * @param i
- * @param data
- * @param jdbcType
- * @throws SQLException
- */
- @Override
- public void setParameter(PreparedStatement preparedStatement, int i, String data, JdbcType jdbcType) throws SQLException {
- if (StringUtils.isEmpty(data)) {
- preparedStatement.setString(i, EMPTY);
- } else {
- preparedStatement.setString(i, AESCryptoUtil.encryptByRandomKey(data, "123456"));
- }
- }
- /**
- * 讀取數據
- * @param resultSet
- * @param columnName
- * @return
- * @throws SQLException
- */
- @Override
- public String getResult(ResultSet resultSet, String columnName) throws SQLException {
- return decrypt(resultSet.getString(columnName));
- }
- /**
- * 讀取數據
- * @param resultSet
- * @param columnIndex
- * @return
- * @throws SQLException
- */
- @Override
- public String getResult(ResultSet resultSet, int columnIndex) throws SQLException {
- return decrypt(resultSet.getString(columnIndex));
- }
- /**
- * 讀取數據
- * @param callableStatement
- * @param columnIndex
- * @return
- * @throws SQLException
- */
- @Override
- public String getResult(CallableStatement callableStatement, int columnIndex) throws SQLException {
- return decrypt(callableStatement.getString(columnIndex));
- }
- /**
- * 對數據進行解密
- * @param data
- * @return
- */
- private String decrypt(String data) {
- return AESCryptoUtil.decryptByRandomKey(data, "123456");
- }
- }
(3) 單元測試
再次運行單元測試,程序讀寫正常!
通過如下的方式,也可以實現對數據表中某個特定字段進行數據脫敏處理!
三、小結
因業務的需求,當需要對某些數據表字段進行脫敏處理的時候,有個細節很容易遺漏,那就是字典類型,例如salary字段,根據常規,很容易想到使用數字類型,但是卻不是,要知道加密之后的數據都是一串亂碼,數字類型肯定是無法存儲字符串的,因此在定義的時候,這個要留心一下。
其次,很多同學可能會覺得,這個也不能防范比人竊取數據啊!
如果加密使用的密鑰和數據都在一個項目里面,答案是肯定的,你可以隨便解析任何人的數據。因此在實際的處理上,這個更多的是在流程上做變化。例如如下方式:
- 首先,加密采用的密鑰會在另外一個單獨的服務來存儲管理,保證密鑰不輕易泄露出去,最重要的是加密的數據不輕易被別人解密。
- 其次,例如某些人想要訪問誰的工資條數據,那么就需要做二次密碼確認,也就是輸入自己的密碼才能獲取,可以進一步防止研發人員隨意通過接口方式讀取數據。
- 最后就是,杜絕代碼留漏洞。
以上三套方案,都可以幫助大家實現數據庫字段數據的脫敏,希望能幫助到大家,謝謝欣賞!