javascript
实战:隐藏SpringBoot中的私密数据!
這幾天公司在排查內(nèi)部數(shù)據(jù)賬號泄漏,原因是發(fā)現(xiàn)某些實習(xí)生小可愛居然連帶著賬號、密碼將源碼私傳到GitHub上,導(dǎo)致核心數(shù)據(jù)外漏,孩子還是沒挨過社會毒打,這種事的后果可大可小。
說起這個我是比較有感觸的,之前我TM被刪庫的經(jīng)歷,到現(xiàn)在想起來心里還難受,我也是把數(shù)據(jù)庫賬號明文密碼誤提交到GitHub,然后被哪個大寶貝給我測試庫刪了,后邊我長記性了把配置文件內(nèi)容都加密了,數(shù)據(jù)安全問題真的不容小覷,不管工作匯還是生活,敏感數(shù)據(jù)一定要做脫敏處理。
所以接下來,咱們需要開展兩方面的工作:
對配置文件進行脫敏操作;
對敏感的數(shù)據(jù)庫字段進行脫敏操作。
說干就干,接下來咱們一起來對項目進行脫敏操作。
1.配置脫敏
實現(xiàn)配置的脫敏我使用了Java的一個加解密工具Jasypt,它提供了單密鑰對稱加密和非對稱加密兩種脫敏方式。
單密鑰對稱加密:一個密鑰加鹽,可以同時用作內(nèi)容的加密和解密依據(jù);
非對稱加密:使用公鑰和私鑰兩個密鑰,才可以對內(nèi)容加密和解密;
以上兩種加密方式使用都非常簡單,咱們以springboot集成單密鑰對稱加密方式做示例。
首先引入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值替換成預(yù)先經(jīng)過加密的內(nèi)容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/Shanghaiusername:?xiaofupassword:?ENC(mVTvp4IddqdaYGqPl9lCQbzM3H/b0B6l)#?秘鑰 jasypt:encryptor:password:?程序員內(nèi)點事(然而不支持中文)秘鑰是個安全性要求比較高的屬性,所以一般不建議直接放在項目內(nèi),可以通過啟動時-D參數(shù)注入,或者放在配置中心,避免泄露。
java?-jar?-Djasypt.encryptor.password=1123??springboot-jasypt-2.3.3.RELEASE.jar預(yù)先生成的加密值,可以通過代碼內(nèi)調(diào)用API生成
@Autowired private?StringEncryptor?stringEncryptor;public?void?encrypt(String?content)?{String?encryptStr?=?stringEncryptor.encrypt(content);System.out.println("加密后的內(nèi)容:"?+?encryptStr); }或者通過如下Java命令生成,幾個參數(shù)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一頓操作后如果還能正常啟動,說明配置文件脫敏就沒問題了。
2.敏感字段脫敏
生產(chǎn)環(huán)境用戶的隱私數(shù)據(jù),比如手機號、身份證或者一些賬號配置等信息,入庫時都要進行不落地脫敏,也就是在進入我們系統(tǒng)時就要實時的脫敏處理。
用戶數(shù)據(jù)進入系統(tǒng),脫敏處理后持久化到數(shù)據(jù)庫,用戶查詢數(shù)據(jù)時還要進行反向解密。這種場景一般需要全局處理,那么用AOP切面來實現(xiàn)在適合不過了。
首先自定義兩個注解@EncryptField、@EncryptMethod分別用在字段屬性和方法上,實現(xiàn)思路很簡單,只要方法上應(yīng)用到@EncryptMethod注解,則檢查入?yún)⒆侄问欠駱?biāo)注@EncryptField注解,有則將對應(yīng)字段內(nèi)容加密。
@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; }切面的實現(xiàn)也比較簡單,對入?yún)⒓用?#xff0c;返回結(jié)果解密。為了方便閱讀這里就只貼出部分代碼,完整案例Github地址:https://github.com/chengxy-nds/Springboot-Notebook/tree/master/springboot-jasypt
@Slf4j @Aspect @Component public?class?EncryptHandler?{@Autowiredprivate?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("加密后的數(shù)據(jù):user"?+?JSON.toJSONString(user));return?user; }@Data public?class?UserVo?implements?Serializable?{private?Long?userId;@EncryptFieldprivate?String?mobile;@EncryptFieldprivate?String?address;private?String?age; }請求這個接口,看到參數(shù)被成功加密,而返回給用戶的數(shù)據(jù)依然是脫敏前的數(shù)據(jù),符合我們的預(yù)期,那到這簡單的脫敏實現(xiàn)就完事了。
3.原理分析
Jasypt工具雖然簡單好用,但作為程序員我們不能僅滿足于熟練使用,底層實現(xiàn)原理還是有必要了解下的,這對后續(xù)調(diào)試bug、二次開發(fā)擴展功能很重要。
個人認(rèn)為Jasypt配置文件脫敏的原理很簡單,無非就是在具體使用配置信息之前,先攔截獲取配置的操作,將對應(yīng)的加密配置解密后再使用。
具體是不是如此我們簡單看下源碼的實現(xiàn),既然是以springboot方式集成,那么就先從jasypt-spring-boot-starter源碼開始入手。
starter代碼很少,主要的工作就是通過SPI機制注冊服務(wù)和@Import注解來注入需前置處理的類JasyptSpringBootAutoConfiguration。
在前置加載類EnableEncryptablePropertiesConfiguration中注冊了一個核心處理類EnableEncryptablePropertiesBeanFactoryPostProcessor。
它的構(gòu)造器有兩個參數(shù),ConfigurableEnvironment用來獲取所有配屬信息,EncryptablePropertySourceConverter對配置信息做解析處理。
順藤摸瓜發(fā)現(xiàn)具體負(fù)責(zé)解密的處理類EncryptablePropertySourceWrapper,它通過對Spring屬性管理類PropertySource<T>做拓展,重寫了getProperty(String name)方法,在獲取配置時,凡是指定格式如ENC(x) 包裹的值全部解密處理。
既然知道了原理那么后續(xù)我們二次開發(fā),比如:切換加密算法或者實現(xiàn)自己的脫敏工具就容易的多了。
“案例Github地址:https://github.com/chengxy-nds/Springboot-Notebook/tree/master/springboot-jasypt
PBE算法
再來聊一下Jasypt中用的加密算法,其實它是在JDK的JCE.jar包基礎(chǔ)上做了封裝,本質(zhì)上還是用的JDK提供的算法,默認(rèn)使用的是PBE算法PBEWITHMD5ANDDES,看到這個算法命名很有意思,段個句看看,PBE、WITH、MD5、AND、DES 好像有點故事,繼續(xù)看。
PBE算法(Password Based Encryption,基于口令(密碼)的加密)是一種基于口令的加密算法,其特點在于口令是由用戶自己掌握,在加上隨機數(shù)多重加密等方法保證數(shù)據(jù)的安全性。
PBE算法本質(zhì)上并沒有真正構(gòu)建新的加密、解密算法,而是對我們已知的算法做了包裝。比如:常用的消息摘要算法MD5和SHA算法,對稱加密算法DES、RC2等,而PBE算法就是將這些算法進行合理組合,這也呼應(yīng)上前邊算法的名字。
既然PBE算法使用我們較為常用的對稱加密算法,那就會涉及密鑰的問題。但它本身又沒有鑰的概念,只有口令密碼,密鑰則是口令經(jīng)過加密算法計算得來的。
口令本身并不會很長,所以不能用來替代密鑰,只用口令很容易通過窮舉攻擊方式破譯,這時候就得加點鹽了。
鹽通常會是一些隨機信息,比如隨機數(shù)、時間戳,將鹽附加在口令上,通過算法計算加大破譯的難度。
源碼里的貓膩
簡單了解PBE算法,回過頭看看Jasypt源碼是如何實現(xiàn)加解密的。
在加密的時候首先實例化秘鑰工廠SecretKeyFactory,生成八位鹽值,默認(rèn)使用的jasypt.encryptor.RandomSaltGenerator生成器。
public?byte[]?encrypt(byte[]?message)?{//?根據(jù)指定算法,初始化秘鑰工廠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);//?構(gòu)建加密器final?Cipher?cipherEncrypt?=?Cipher.getInstance(algorithm1);cipherEncrypt.init(Cipher.ENCRYPT_MODE,?key);//?密文頭部(鹽值)byte[]?params?=?cipherEncrypt.getParameters().getEncoded();//?調(diào)用底層實現(xiàn)加密byte[]?encryptedMessage?=?cipherEncrypt.doFinal(message);//?組裝最終密文內(nèi)容并分配內(nèi)存(鹽值+密文)return?ByteBuffer.allocate(1?+?params.length?+?encryptedMessage.length).put((byte)?params.length).put(params).put(encryptedMessage).array(); }由于默認(rèn)使用的是隨機鹽值生成器,導(dǎo)致相同內(nèi)容每次加密后的內(nèi)容都是不同的。
那么解密時該怎么對應(yīng)上呢?
看上邊的源碼發(fā)現(xiàn),最終的加密文本是由兩部分組成的,params消息頭里邊包含口令和隨機生成的鹽值,encryptedMessage密文。
加密而在解密時會根據(jù)密文encryptedMessage的內(nèi)容拆解出params內(nèi)容解析出鹽值和口令,在調(diào)用JDK底層算法解密出實際內(nèi)容。
@Override @SneakyThrows public?byte[]?decrypt(byte[]?encryptedMessage)?{//?獲取密文頭部內(nèi)容int?paramsLength?=?Byte.toUnsignedInt(encryptedMessage[0]);//?獲取密文內(nèi)容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);//?構(gòu)建頭部鹽值口令參數(shù)AlgorithmParameters?algorithmParameters?=?AlgorithmParameters.getInstance(algorithm1);algorithmParameters.init(params);//?構(gòu)建加密器,調(diào)用底層算法final?Cipher?cipherDecrypt?=?Cipher.getInstance(algorithm1);cipherDecrypt.init(Cipher.DECRYPT_MODE,key,algorithmParameters);return?cipherDecrypt.doFinal(message); } 解密我是磊哥~,如果對你有用在看、關(guān)注支持下,咱們下期見~
往期推薦SpringBoot 如何統(tǒng)一后端返回格式?老鳥們都是這樣玩的!
SpringBoot時間格式化的5種方法!
SpringBoot 優(yōu)雅的參數(shù)效驗!
總結(jié)
以上是生活随笔為你收集整理的实战:隐藏SpringBoot中的私密数据!的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java中实现定时任务的3种方法!
- 下一篇: 调整灰度图像的大小,而无需在Python