50行代碼,搞定敏感數(shù)據(jù)讀寫!
一、介紹
在實(shí)際的軟件系統(tǒng)開(kāi)發(fā)過(guò)程中,由于業(yè)務(wù)的需求,在代碼層面實(shí)現(xiàn)數(shù)據(jù)的脫敏還是遠(yuǎn)遠(yuǎn)不夠的,往往還需要在數(shù)據(jù)庫(kù)層面針對(duì)某些關(guān)鍵性的敏感信息,例如:身份證號(hào)、銀行卡號(hào)、手機(jī)號(hào)、工資等信息進(jìn)行加密存儲(chǔ),實(shí)現(xiàn)真正意義的數(shù)據(jù)混淆脫敏,以滿足信息安全的需要。
那在實(shí)際的研發(fā)過(guò)程中,我們?nèi)绾螌?shí)踐呢?
二、方案實(shí)踐
在此,提供三套方案以供大家選擇。
- 通過(guò) SQL 函數(shù)實(shí)現(xiàn)加解密
- 對(duì) SQL 進(jìn)行解析攔截,實(shí)現(xiàn)數(shù)據(jù)加解密
- 自定義一套脫敏工具
1. 通過(guò) SQL 函數(shù)實(shí)現(xiàn)加解密
最簡(jiǎn)單的方法,莫過(guò)于直接在數(shù)據(jù)庫(kù)層面操作,通過(guò)函數(shù)對(duì)某個(gè)字段進(jìn)行加、解密,例如如下這個(gè)案例!
- -- 對(duì)“你好,世界”進(jìn)行加密
- select HEX(AES_ENCRYPT('你好,世界','ABC123456'));
- -- 解密,輸出:你好,世界
- select AES_DECRYPT(UNHEX('A174E3C13FE16AA0FD071A4BBD7CD7C5'),'ABC123456');
采用MySQL內(nèi)置的AES協(xié)議加、解密函數(shù),密鑰是ABC123456,可以很輕松的對(duì)某個(gè)字段實(shí)現(xiàn)加、解密。
如果是很小的需求,需要加密的數(shù)據(jù)就是指定的信息,此方法可行。
但是當(dāng)需要加密的表字段非常多的時(shí)候,這個(gè)使用起來(lái)就比較雞肋了,例如我想更改加密算法或者不同的部署環(huán)境配置不同的密鑰,這個(gè)時(shí)候就不得不把所有的代碼進(jìn)行更改一遍。
2. 對(duì) SQL 進(jìn)行解析攔截,實(shí)現(xiàn)數(shù)據(jù)加解密
通過(guò)上面的方案,我們發(fā)現(xiàn)最大的痛點(diǎn)就是加密算法和密鑰都寫死在SQL上了,因此我們可以將這塊的服務(wù)從抽出來(lái),在JDBC層面,當(dāng)sql執(zhí)行的時(shí)候,對(duì)其進(jìn)行攔截處理。
Apache ShardingSphere 框架下的數(shù)據(jù)脫敏模塊,它就可以幫助我們實(shí)現(xiàn)這一需求,如果你是SpringBoot項(xiàng)目,可以實(shí)現(xiàn)無(wú)縫集成,對(duì)原系統(tǒng)的改造會(huì)非常少。
下面以用戶表為例,我們來(lái)看看采用ShardingSphere如何實(shí)現(xiàn)!
(1) 創(chuàng)建用戶表
- 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 '注冊(cè)時(shí)間',
- 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) 創(chuàng)建 springboot 項(xiàng)目并添加依賴包
- <dependencies>
- <!--spring boot核心-->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter</artifactId>
- </dependency>
- <!--spring boot 測(cè)試-->
- <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 數(shù)據(jù)源-->
- <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數(shù)據(jù)分片、脫敏工具-->
- <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相關(guān)配置,即可實(shí)現(xiàn)針對(duì)某個(gè)表進(jìn)行脫敏。
- server.port=8080
- loglogging.path=log
- #shardingsphere數(shù)據(jù)源集成
- 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
其中下面的配置信息是關(guān)鍵的一部,spring.shardingsphere.encrypt.tables是指要脫敏的表,user是表名,salary表示user表中的真實(shí)列,其中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) 編寫數(shù)據(jù)持久層
- <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();
- /**
- * 新增數(shù)據(jù)
- * @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) 最后我們來(lái)測(cè)試一下程序運(yùn)行情況
編寫啟用服務(wù)程序:
- @SpringBootApplication
- @MapperScan("com.example.shardingsphere.mapper")
- public class ShardingSphereApplication {
- public static void main(String[] args) {
- SpringApplication.run(ShardingSphereApplication.class, args);
- }
- }
編寫單元測(cè)試:
- @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));
- }
- }
插入數(shù)據(jù)后,如下圖,數(shù)據(jù)庫(kù)存儲(chǔ)的數(shù)據(jù)已被加密!
我們繼續(xù)來(lái)看看,運(yùn)行查詢服務(wù),結(jié)果如下圖,數(shù)據(jù)被成功解密!
采用配置方式,最大的好處就是直接通過(guò)配置脫敏列就可以完成對(duì)某些數(shù)據(jù)表字段的脫敏,非常方便。
3. 自定義一套脫敏工具
當(dāng)然,有的同學(xué)可能會(huì)覺(jué)得shardingsphere配置雖然簡(jiǎn)單,但是還是不放心,里面的很多規(guī)則自己無(wú)法掌控,想自己開(kāi)發(fā)一套數(shù)據(jù)庫(kù)的脫敏工具。
方案也是有的,例如如下這套實(shí)踐方案,以Mybatis為例:
- 首先編寫一套加解密的算法工具類
- 通過(guò)Mybatis的typeHandler插件,實(shí)現(xiàn)特定字段的加解密
實(shí)踐過(guò)程如下:
(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 需要加密內(nèi)容
- * @param key 任意字符串
- * @return
- * @throws Exception
- */
- public static String encryptByRandomKey(String content, String key) {
- try {
- //構(gòu)造密鑰生成器,生成一個(gè)128位的隨機(jī)源,產(chǎn)生原始對(duì)稱密鑰
- 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加密失敗,參數(shù):{},錯(cuò)誤信息:{}", content, e);
- return "";
- }
- }
- public static String decryptByRandomKey(String content, String key) {
- try {
- //構(gòu)造密鑰生成器,生成一個(gè)128位的隨機(jī)源,產(chǎn)生原始對(duì)稱密鑰
- 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解密失敗,參數(shù):{},錯(cuò)誤信息:{}", 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、針對(duì) salary 字段進(jìn)行單獨(dú)解析
- <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解析器,內(nèi)容如下:
- public class EncryptDataRuleTypeHandler implements TypeHandler<String> {
- private static final String EMPTY = "";
- /**
- * 寫入數(shù)據(jù)
- * @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"));
- }
- }
- /**
- * 讀取數(shù)據(jù)
- * @param resultSet
- * @param columnName
- * @return
- * @throws SQLException
- */
- @Override
- public String getResult(ResultSet resultSet, String columnName) throws SQLException {
- return decrypt(resultSet.getString(columnName));
- }
- /**
- * 讀取數(shù)據(jù)
- * @param resultSet
- * @param columnIndex
- * @return
- * @throws SQLException
- */
- @Override
- public String getResult(ResultSet resultSet, int columnIndex) throws SQLException {
- return decrypt(resultSet.getString(columnIndex));
- }
- /**
- * 讀取數(shù)據(jù)
- * @param callableStatement
- * @param columnIndex
- * @return
- * @throws SQLException
- */
- @Override
- public String getResult(CallableStatement callableStatement, int columnIndex) throws SQLException {
- return decrypt(callableStatement.getString(columnIndex));
- }
- /**
- * 對(duì)數(shù)據(jù)進(jìn)行解密
- * @param data
- * @return
- */
- private String decrypt(String data) {
- return AESCryptoUtil.decryptByRandomKey(data, "123456");
- }
- }
(3) 單元測(cè)試
再次運(yùn)行單元測(cè)試,程序讀寫正常!
通過(guò)如下的方式,也可以實(shí)現(xiàn)對(duì)數(shù)據(jù)表中某個(gè)特定字段進(jìn)行數(shù)據(jù)脫敏處理!
三、小結(jié)
因業(yè)務(wù)的需求,當(dāng)需要對(duì)某些數(shù)據(jù)表字段進(jìn)行脫敏處理的時(shí)候,有個(gè)細(xì)節(jié)很容易遺漏,那就是字典類型,例如salary字段,根據(jù)常規(guī),很容易想到使用數(shù)字類型,但是卻不是,要知道加密之后的數(shù)據(jù)都是一串亂碼,數(shù)字類型肯定是無(wú)法存儲(chǔ)字符串的,因此在定義的時(shí)候,這個(gè)要留心一下。
其次,很多同學(xué)可能會(huì)覺(jué)得,這個(gè)也不能防范比人竊取數(shù)據(jù)啊!
如果加密使用的密鑰和數(shù)據(jù)都在一個(gè)項(xiàng)目里面,答案是肯定的,你可以隨便解析任何人的數(shù)據(jù)。因此在實(shí)際的處理上,這個(gè)更多的是在流程上做變化。例如如下方式:
- 首先,加密采用的密鑰會(huì)在另外一個(gè)單獨(dú)的服務(wù)來(lái)存儲(chǔ)管理,保證密鑰不輕易泄露出去,最重要的是加密的數(shù)據(jù)不輕易被別人解密。
- 其次,例如某些人想要訪問(wèn)誰(shuí)的工資條數(shù)據(jù),那么就需要做二次密碼確認(rèn),也就是輸入自己的密碼才能獲取,可以進(jìn)一步防止研發(fā)人員隨意通過(guò)接口方式讀取數(shù)據(jù)。
- 最后就是,杜絕代碼留漏洞。
以上三套方案,都可以幫助大家實(shí)現(xiàn)數(shù)據(jù)庫(kù)字段數(shù)據(jù)的脫敏,希望能幫助到大家,謝謝欣賞!