保護敏感數(shù)據(jù):Spring Boot中敏感字段加密解密的高效解決方案
前言
相信大家都有這樣一個煩惱,就是經常會接到各種推銷、廣告的電話和短信,如果你沒有在他那里留下過聯(lián)系方式,他又是如何得到了你的聯(lián)系方式呢?毫無疑問,是個人信息被泄漏了。個人信息的泄漏有人為不合法謀利的因素,也有系統(tǒng)不合理的安全設計造成泄漏的因素。當然系統(tǒng)設計的角度出發(fā),敏感信息需要加密存儲的,數(shù)據(jù)展示的時候也要進行相應的脫敏處理,但是從一些關于個信息泄漏的新聞報道來看,有好多的網站后臺竟然是“裸奔”狀態(tài),簡直太可怕了。其實敏感數(shù)據(jù)的處理也不復雜,說到底是安全意識不強。當然,這篇文章和大家分享的重點是加密和解密的方法,不是數(shù)據(jù)安全的重要性。
基本概念
敏感數(shù)據(jù)
敏感數(shù)據(jù)是指那些泄漏后可能會給社會或個人造成嚴重危害的數(shù)據(jù),以個人隱私信息為例,如手機號碼、家庭住址、郵箱、身份證號、銀行卡帳號、購物網站的支付密碼、登陸密碼等等。另外從社會的角度出發(fā),也有很多數(shù)據(jù)是屬于敏感數(shù)據(jù),如:居民的生物基因信息等等。
數(shù)據(jù)加密
數(shù)據(jù)加密是指對數(shù)據(jù)重新編碼來保護數(shù)據(jù),獲取實際數(shù)據(jù)的唯一辦法就是使用密鑰解密數(shù)據(jù);
數(shù)據(jù)解密
數(shù)據(jù)解密與數(shù)據(jù)加密是相對的,即使用密鑰對加密的數(shù)據(jù)進行解密的過程;
加密方式
加密的方式,一般是兩種:對稱加密和非對稱加密;
對稱加密只有一個秘鑰,加密和解密都是用同一個秘鑰,如AES、DES等;
非對稱加密有兩個秘鑰,一個是公鑰,一個是私鑰。使用公鑰對數(shù)據(jù)進行加密,加密后的數(shù)據(jù)只有私鑰可以解密,一般公鑰是公開的,私鑰是不公開的;如RSA、DSA等;
實現(xiàn)原理
Springboot項目中,客戶端通過接口向服務端讀取或寫入敏感數(shù)據(jù)時,常會有這樣的業(yè)務需求:
1、在客戶端向服務器端發(fā)起寫入請求,服務端需要對寫入的敏感數(shù)據(jù)進行加密后存儲;
2、在客戶端從服務器端向外讀取數(shù)據(jù)的時候,需要對輸出的敏感數(shù)據(jù)進行解密;
顯然這種場景,對于加密的方式的選擇,對稱加密是最好的選擇;那么如何實現(xiàn)對寫入請求、讀取請求的敏感數(shù)據(jù)的加密、解密處理呢?解決方案如下:
1、自定義兩個切面注解,分別是加密切面注解、解密切面注解,作用于需要加密或解密的敏感數(shù)據(jù)處理的業(yè)務處理類的具體業(yè)務處理方法上;
2、自定義兩個敏感字段處理注解,分別是加密字段注解、解密字段注解,作用于需要輸入或輸出的對象的敏感字段上;如果輸入對象上標記了加密字段注解,則表示該字段在對內寫入數(shù)據(jù)庫的時候,需要加密處理;同理,如果輸出對象上標記了解密字段注解,則表示該字段在對外輸出的時候,需要進行解密;
3、使用面向切面編程,定義兩個切面類,分別是加密切面類和解密切面類,選擇Spring AOP的環(huán)繞通知來具體實現(xiàn);加密切面類中,以注解的方式定義切入點,用到的注解就是自定義的加密切面注解;
4、如果新增、編輯等寫入類的業(yè)務請求處理方法上標記了加密切面注解,那么寫入請求在正式被業(yè)務處理方法處理前,會命中加密切面類,加密切面類的環(huán)繞通知方法被觸發(fā),然后根據(jù)輸入的參數(shù)對象中的字段是否標記了自定義的加密字段注解,來決定是否對當前字段進行加密處理;
5、同理,如果是查詢等讀取類的業(yè)務請求處理方法上標記了解密切面注解,那么讀取請求被業(yè)務處理類處理完之后,會命中解密切面類,解密切面類的環(huán)繞通知方法被觸發(fā),然后根據(jù)返回對象的字段是否標記了解密字段注解,來決定是否對當前字段進行解密處理。
實現(xiàn)方案
環(huán)境配置
jdk版本:1.8開發(fā)工具:Intellij iDEA 2020.1
springboot:2.3.9.RELEASE
mybatis-spring-boot-starter:2.1.4
依賴配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.3.3</version>
</dependency>
示例時序圖
圖片
示例代碼
1、自定義四個注解:@DecryptField(解密字段注解)、@EncryptField(加密字段注解)、@NeedEncrypt(解密切面注解)、@NeedEncrypt(加密切面注解),其中@DecryptField作用于需要解密的字段上;@EncryptField作用于需要加密的字段上;@NeedEncrypt作用于需要對入參數(shù)進行加密處理的方法上;@NeedDecrypt作用于需要對返回值進行解密處理的方法上;
//解密字段注解
@Target(value = {ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface DecryptField {
}
//加密字段注解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptField {
}
//作用于對返回值進行解密處理的方法上
@Target(value = {ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface NeedDecrypt {
}
//作用于需要對入參數(shù)進行加密處理的方法上
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NeedEncrypt {
}
2、把自定義的加密字段注解、解密字段注解標記在需要加密或者解密的字段上;這里表示在寫入人員的手機號碼、身份證號碼、家庭住址門牌號碼時,要進行加密處理;在讀取人員的手機號碼、身份證號碼、家庭住址門牌號碼時,要進行解密處理;
@Slf4j
@Data
public class Person {
private Integer id;
private String userName;
private String loginNo;
@EncryptField
@DecryptField
private String phoneNumber;
private String sex;
@DecryptField
@EncryptField
private String IDCard;
private String address;
@EncryptField
@DecryptField
private String houseNumber;
}
3、把@NeedEncrypt和@NeedDecrypt標記在需要對入參數(shù)、返回值中的敏感字段進行加密、解密處理的業(yè)務處理方法上;
@RestController
@RequestMapping("/person")
@Slf4j
public class PersonController {
@Autowired
private IPersonService personService;
//添加人員信息
@PostMapping("/add")
@NeedEncrypt
public Person add(@RequestBody Person person, Model model) {
Person result = this.personService.registe(person);
log.info("http://增加person執(zhí)行完成");
return result;
}
//人員信息列表查詢
@GetMapping("/list")
@NeedDecrypt
public List<Person> getPerson() {
List<Person> persons = this.personService.getPersonList();
log.info("http://查詢person列表執(zhí)行完成");
return persons;
}
//人員信息詳情查詢
@GetMapping("/{id}")
@NeedDecrypt
public Person get(@PathVariable Integer id) {
Person persnotallow= this.personService.get(id);
log.info("http://查詢person詳情執(zhí)行完成");
return person;
}
}
4、自定義加密切面類(EncryptAop)和解密切面類(DecryptAop):用@NeedEncrypt注解定義加密切點,在加密切點的環(huán)繞通知方法里執(zhí)行到具體的業(yè)務處理方法之前,判斷輸入對象的參數(shù)字段是否標記了@EncryptField(加密字段注解),如果判斷結果為true,則使用java反射對該字段進行加密處理,注意這里引用了hutool的工具包,使用了工具包里的加密和解密方法,這里也可以替換成其他的方式;用@NeedDecrypt注解定義解密切點,在解密切點的環(huán)繞通知方法里執(zhí)行完具體的業(yè)務處理方法之后,判斷輸出對象的參數(shù)字段是否標記了@DecryptField(解密字段注解),如果判斷結果為true,則使用java反射對該 字段進行解密處理;
@Component
@Aspect
@Slf4j
public class EncryptAop {
/**
* 定義加密切入點
*/
@Pointcut(value = "@annotation(com.fanfu.anno.NeedEncrypt)")
public void pointcut() {
}
/**
* 命中加密切入點的環(huán)繞通知
*
* @param proceedingJoinPoint
* @return
* @throws Throwable
*/
@Around("pointcut()")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
log.info("http://環(huán)繞通知 start");
//獲取命中目標方法的入參數(shù)
Object[] args = proceedingJoinPoint.getArgs();
if (args.length > 0) {
for (Object arg : args) {
//按參數(shù)的類型進行判斷,如果業(yè)務中還有其他的類型,可酌情增加
if (arg != null) {
if (arg instanceof List) {
for (Object tmp : ((List) arg)) {
//加密處理
this.deepProcess(tmp);
}
} else {
this.deepProcess(arg);
}
}
}
}
//對敏感數(shù)據(jù)加密后執(zhí)行目標方法
Object result = proceedingJoinPoint.proceed();
log.info("http://環(huán)繞通知 end");
return result;
}
public void deepProcess(Object obj) throws IllegalAccessException {
if (obj != null) {
//獲取對象的所有字段屬性并遍歷
Field[] declaredFields = obj.getClass().getDeclaredFields();
for (Field declaredField : declaredFields) {
//判斷字段屬性上是否標記了@EncryptField注解
if (declaredField.isAnnotationPresent(EncryptField.class)) {
//如果判斷結果為真,則取出字段屬性值,進行加密、重新賦值
declaredField.setAccessible(true);
Object valObj = declaredField.get(obj);
if (valObj != null) {
String value = valObj.toString();
//開始敏感字段屬性值加密
String decrypt = this.encrypt(value);
//把加密后的字段屬性值重新賦值
declaredField.set(obj, decrypt);
}
}
}
}
}
private String encrypt(String value) {
//這里特別注意一下,對稱加密是根據(jù)密鑰進行加密和解密的,加密和解密的密鑰是相同的,一旦泄漏,就無秘密可言,
//“fanfu-csdn”就是我自定義的密鑰,這里僅作演示使用,實際業(yè)務中,這個密鑰要以安全的方式存儲;
byte[] key = SecureUtil.generateKey(SymmetricAlgorithm.DES.getValue(), "fanfu-csdn".getBytes()).getEncoded();
SymmetricCrypto aes = new SymmetricCrypto(SymmetricAlgorithm.DES, key);
String encryptValue = aes.encryptBase64(value);
return encryptValue;
}
}
@Component
@Aspect
@Slf4j
public class DecryptAop {
/**
* 定義需要解密的切入點
*/
@Pointcut(value = "@annotation(com.fanfu.anno.NeedDecrypt)")
public void pointcut() {
}
/**
* 命中的切入點時的環(huán)繞通知
*
* @param proceedingJoinPoint
* @return
* @throws Throwable
*/
@Around("pointcut()")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
log.info("http://環(huán)繞通知 start");
//執(zhí)行目標方法
Object result = proceedingJoinPoint.proceed();
//判斷目標方法的返回值類型
if (result instanceof List) {
for (Object tmp : ((List) result)) {
//數(shù)據(jù)脫敏處理邏輯
this.deepProcess(tmp);
}
} else {
this.deepProcess(result);
}
log.info("http://環(huán)繞通知 end");
return result;
}
public void deepProcess(Object obj) throws IllegalAccessException {
if (obj != null) {
//取出輸出對象的所有字段屬性,并遍歷
Field[] declaredFields = obj.getClass().getDeclaredFields();
for (Field declaredField : declaredFields) {
//判斷字段屬性上是否標記DecryptField注解
if (declaredField.isAnnotationPresent(DecryptField.class)) {
//如果判斷結果為真,則取出字段屬性數(shù)據(jù)進行解密處理
declaredField.setAccessible(true);
Object valObj = declaredField.get(obj);
if (valObj != null) {
String value = valObj.toString();
//加密數(shù)據(jù)的解密處理
value = this.decrypt(value);
DecryptField annotation = declaredField.getAnnotation(DecryptField.class);
boolean open = annotation.open();
//把解密后的數(shù)據(jù)重新賦值
declaredField.set(obj, value);
}
}
}
}
}
private String decrypt(String value) {
//這里特別注意一下,對稱加密是根據(jù)密鑰進行加密和解密的,加密和解密的密鑰是相同的,一旦泄漏,就無秘密可言,
//“fanfu-csdn”就是我自定義的密鑰,這里僅作演示使用,實際業(yè)務中,這個密鑰要以安全的方式存儲;
byte[] key = SecureUtil.generateKey(SymmetricAlgorithm.DES.getValue(), "fanfu-csdn".getBytes()).getEncoded();
SymmetricCrypto aes = new SymmetricCrypto(SymmetricAlgorithm.DES, key);
String decryptStr = aes.decryptStr(value);
return decryptStr;
}
}
加密結果
圖片
解密結果
圖片
總結
這篇著重和大家分享的內容如下:
1、敏感數(shù)據(jù)的一些基礎概念;
2、敏感數(shù)據(jù)處理的解決思路;
3、敏感數(shù)據(jù)處理的具體實現(xiàn)方式;