你們好!我是小富~java
這幾天公司在排查內部數據帳號泄漏,緣由是發現某些實習生小可愛竟然連帶着帳號、密碼將源碼私傳到GitHub
上,致使核心數據外漏,孩子仍是沒捱過社會毒打,這種事的後果可大可小。mysql
提及這個我是比較有感觸的,以前我TM被刪庫的經歷,到如今想起來內心還難受,我也是把數據庫帳號明文密碼誤提交到GitHub
,而後被哪一個大寶貝給我測試庫刪了,後邊我長記性了把配置文件內容都加密了,數據安全問題真的不容小覷,無論工做匯仍是生活,敏感數據必定要作脫敏處理。git
若是對脫敏概念不熟悉,能夠看一下我以前寫過的一篇大廠也在用的6種數據脫敏方案,裏邊對脫敏作了簡單的描述,接下來分享工做中兩個比較常見的脫敏場景。程序員
配置脫敏
實現配置的脫敏我使用了Java
的一個加解密工具Jasypt
,它提供了單密鑰對稱加密
和非對稱加密
兩種脫敏方式。github
單密鑰對稱加密:一個密鑰加鹽,能夠同時用做內容的加密和解密依據;算法
非對稱加密:使用公鑰和私鑰兩個密鑰,才能夠對內容加密和解密;spring
以上兩種加密方式使用都很是簡單,我們以springboot
集成單密鑰對稱加密方式作示例。sql
首先引入jasypt-spring-boot-starter
jar數據庫
<!--配置文件加密--> <dependency> <groupId>com.github.ulisesbocchio</groupId> <artifactId>jasypt-spring-boot-starter</artifactId> <version>2.1.0</version> </dependency>
配置文件加入祕鑰配置項jasypt.encryptor.password
,並將須要脫敏的value
值替換成預先通過加密的內容ENC(mVTvp4IddqdaYGqPl9lCQbzM3H/b0B6l)
。安全
這個格式咱們是能夠隨意定義的,好比想要abc[mVTvp4IddqdaYGqPl9lCQbzM3H/b0B6l]
格式,只要配置前綴和後綴便可。
jasypt: encryptor: property: prefix: "abc[" suffix: "]"
ENC(XXX)格式主要爲了便於識別該值是否須要解密,如不按照該格式配置,在加載配置項的時候jasypt
將保持原值,不進行解密。
spring: datasource: url: jdbc:mysql://1.2.3.4:3306/xiaofu?useSSL=false&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&ze oDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai username: xiaofu password: ENC(mVTvp4IddqdaYGqPl9lCQbzM3H/b0B6l) # 祕鑰 jasypt: encryptor: password: 程序員內點事(然而不支持中文)
祕鑰是個安全性要求比較高的屬性,因此通常不建議直接放在項目內,能夠經過啓動時-D
參數注入,或者放在配置中心,避免泄露。
java -jar -Djasypt.encryptor.password=1123 springboot-jasypt-2.3.3.RELEASE.jar
預先生成的加密值,能夠經過代碼內調用API生成
@Autowired private StringEncryptor stringEncryptor; public void encrypt(String content) { String encryptStr = stringEncryptor.encrypt(content); System.out.println("加密後的內容:" + encryptStr); }
或者經過以下Java命令生成,幾個參數D:\maven_lib\org\jasypt\jasypt\1.9.3\jasypt-1.9.3.jar
爲jasypt核心jar包,input
待加密文本,password
祕鑰,algorithm
爲使用的加密算法。
java -cp D:\maven_lib\org\jasypt\jasypt\1.9.3\jasypt-1.9.3.jar org.jasypt.intf.cli.JasyptPBEStringEncryptionCLI input="root" password=xiaofu algorithm=PBEWithMD5AndDES
一頓操做後若是還能正常啓動,說明配置文件脫敏就沒問題了。
敏感字段脫敏
生產環境用戶的隱私數據,好比手機號、身份證或者一些帳號配置等信息,入庫時都要進行不落地脫敏,也就是在進入咱們系統時就要實時的脫敏處理。
用戶數據進入系統,脫敏處理後持久化到數據庫,用戶查詢數據時還要進行反向解密。這種場景通常須要全局處理,那麼用AOP
切面來實如今適合不過了。
首先自定義兩個註解@EncryptField
、@EncryptMethod
分別用在字段屬性和方法上,實現思路很簡單,只要方法上應用到@EncryptMethod
註解,則檢查入參字段是否標註@EncryptField
註解,有則將對應字段內容加密。
@Documented @Target({ElementType.FIELD,ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) public @interface EncryptField { String[] value() default ""; }
@Documented @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface EncryptMethod { String type() default ENCRYPT; }
切面的實現也比較簡單,對入參加密,返回結果解密。爲了方便閱讀這裏就只貼出部分代碼,完整案例Github地址:https://github.com/chengxy-nds/Springboot-Notebook/tree/master/springboot-jasypt
@Slf4j @Aspect @Component public class EncryptHandler { @Autowired private StringEncryptor stringEncryptor; @Pointcut("@annotation(com.xiaofu.annotation.EncryptMethod)") public void pointCut() { } @Around("pointCut()") public Object around(ProceedingJoinPoint joinPoint) { /** * 加密 */ encrypt(joinPoint); /** * 解密 */ Object decrypt = decrypt(joinPoint); return decrypt; } public void encrypt(ProceedingJoinPoint joinPoint) { try { Object[] objects = joinPoint.getArgs(); if (objects.length != 0) { for (Object o : objects) { if (o instanceof String) { encryptValue(o); } else { handler(o, ENCRYPT); } //TODO 其他類型本身看實際狀況加 } } } catch (IllegalAccessException e) { e.printStackTrace(); } } public Object decrypt(ProceedingJoinPoint joinPoint) { Object result = null; try { Object obj = joinPoint.proceed(); if (obj != null) { if (obj instanceof String) { decryptValue(obj); } else { result = handler(obj, DECRYPT); } //TODO 其他類型本身看實際狀況加 } } catch (Throwable e) { e.printStackTrace(); } return result; } 。。。 }
緊接着測試一下切面註解的效果,咱們對字段mobile
、address
加上註解@EncryptField
作脫敏處理。
@EncryptMethod @PostMapping(value = "test") @ResponseBody public Object testEncrypt(@RequestBody UserVo user, @EncryptField String name) { return insertUser(user, name); } private UserVo insertUser(UserVo user, String name) { System.out.println("加密後的數據:user" + JSON.toJSONString(user)); return user; } @Data public class UserVo implements Serializable { private Long userId; @EncryptField private String mobile; @EncryptField private String address; private String age; }
請求這個接口,看到參數被成功加密,而返回給用戶的數據依然是脫敏前的數據,符合咱們的預期,那到這簡單的脫敏實現就完事了。
知其然知其因此然
Jasypt
工具雖然簡單好用,但做爲程序員咱們不能僅知足於熟練使用,底層實現原理仍是有必要了解下的,這對後續調試bug、二次開發擴展功能很重要。
我的認爲Jasypt
配置文件脫敏的原理很簡單,無非就是在具體使用配置信息以前,先攔截獲取配置的操做,將對應的加密配置解密後再使用。
具體是否是如此咱們簡單看下源碼的實現,既然是以springboot
方式集成,那麼就先從jasypt-spring-boot-starter
源碼開始入手。
starter
代碼不多,主要的工做就是經過SPI
機制註冊服務和@Import
註解來注入需前置處理的類JasyptSpringBootAutoConfiguration
。
在前置加載類EnableEncryptablePropertiesConfiguration
中註冊了一個核心處理類EnableEncryptablePropertiesBeanFactoryPostProcessor
。
它的構造器有兩個參數,ConfigurableEnvironment
用來獲取全部配屬信息,EncryptablePropertySourceConverter
對配置信息作解析處理。
順藤摸瓜發現具體負責解密的處理類EncryptablePropertySourceWrapper
,它經過對Spring
屬性管理類PropertySource<T>
作拓展,重寫了getProperty(String name)
方法,在獲取配置時,凡是指定格式如ENC(x) 包裹的值所有解密處理。
既然知道了原理那麼後續咱們二次開發,好比:切換加密算法或者實現本身的脫敏工具就容易的多了。
案例Github地址:https://github.com/chengxy-nds/Springboot-Notebook/tree/master/springboot-jasypt
PBE算法
再來聊一下Jasypt
中用的加密算法,其實它是在JDK的JCE.jar
包基礎上作了封裝,本質上仍是用的JDK提供的算法,默認使用的是PBE
算法PBEWITHMD5ANDDES
,看到這個算法命名頗有意思,段個句看看,PBE、WITH、MD五、AND、DES 好像有點故事,繼續看。
PBE
算法(Password Based Encryption
,基於口令(密碼)的加密)是一種基於口令的加密算法,其特色在於口令是由用戶本身掌握,在加上隨機數多重加密等方法保證數據的安全性。
PBE算法本質上並無真正構建新的加密、解密算法,而是對咱們已知的算法作了包裝。好比:經常使用的消息摘要算法MD5
和SHA
算法,對稱加密算法DES
、RC2
等,而PBE
算法就是將這些算法進行合理組合,這也呼應上前邊算法的名字。
既然PBE算法使用咱們較爲經常使用的對稱加密算法,那就會涉及密鑰的問題。但它自己又沒有鑰的概念,只有口令密碼,密鑰則是口令通過加密算法計算得來的。
口令自己並不會很長,因此不能用來替代密鑰,只用口令很容易經過窮舉攻擊方式破譯,這時候就得加點鹽了。
鹽一般會是一些隨機信息,好比隨機數、時間戳,將鹽附加在口令上,經過算法計算加大破譯的難度。
源碼裏的貓膩
簡單瞭解PBE算法,回過頭看看Jasypt
源碼是如何實現加解密的。
在加密的時候首先實例化祕鑰工廠SecretKeyFactory
,生成八位鹽值,默認使用的jasypt.encryptor.RandomSaltGenerator
生成器。
public byte[] encrypt(byte[] message) { // 根據指定算法,初始化祕鑰工廠 final SecretKeyFactory factory = SecretKeyFactory.getInstance(algorithm1); // 鹽值生成器,只選八位 byte[] salt = saltGenerator.generateSalt(8); // final PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray(), salt, iterations); // 鹽值、口令生成祕鑰 SecretKey key = factory.generateSecret(keySpec); // 構建加密器 final Cipher cipherEncrypt = Cipher.getInstance(algorithm1); cipherEncrypt.init(Cipher.ENCRYPT_MODE, key); // 密文頭部(鹽值) byte[] params = cipherEncrypt.getParameters().getEncoded(); // 調用底層實現加密 byte[] encryptedMessage = cipherEncrypt.doFinal(message); // 組裝最終密文內容並分配內存(鹽值+密文) return ByteBuffer .allocate(1 + params.length + encryptedMessage.length) .put((byte) params.length) .put(params) .put(encryptedMessage) .array(); }
因爲默認使用的是隨機鹽值生成器,致使相同內容每次加密後的內容都是不一樣的。
那麼解密時該怎麼對應上呢?
看上邊的源碼發現,最終的加密文本是由兩部分組成的,params
消息頭裏邊包含口令和隨機生成的鹽值,encryptedMessage
密文。
而在解密時會根據密文encryptedMessage
的內容拆解出params
內容解析出鹽值和口令,在調用JDK底層算法解密出實際內容。
@Override @SneakyThrows public byte[] decrypt(byte[] encryptedMessage) { // 獲取密文頭部內容 int paramsLength = Byte.toUnsignedInt(encryptedMessage[0]); // 獲取密文內容 int messageLength = encryptedMessage.length - paramsLength - 1; byte[] params = new byte[paramsLength]; byte[] message = new byte[messageLength]; System.arraycopy(encryptedMessage, 1, params, 0, paramsLength); System.arraycopy(encryptedMessage, paramsLength + 1, message, 0, messageLength); // 初始化祕鑰工廠 final SecretKeyFactory factory = SecretKeyFactory.getInstance(algorithm1); final PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray()); SecretKey key = factory.generateSecret(keySpec); // 構建頭部鹽值口令參數 AlgorithmParameters algorithmParameters = AlgorithmParameters.getInstance(algorithm1); algorithmParameters.init(params); // 構建加密器,調用底層算法 final Cipher cipherDecrypt = Cipher.getInstance(algorithm1); cipherDecrypt.init( Cipher.DECRYPT_MODE, key, algorithmParameters ); return cipherDecrypt.doFinal(message); }
我是小富,下期見~
整理了幾百本各種技術電子書,有須要的同窗自取。技術羣快滿了,想進的同窗能夠加我好友,和大佬們一塊兒吹吹技術。
我的公衆號: 程序員內點事,歡迎交流