前後端API交互如何保證數據安全性?

前言

前後端分離的開發方式,我們以介面為標準來進行推動,定義好介面,各自開發自己的功能,最後進行聯調整合。無論是開發原生的APP還是webapp還是PC端的軟體,只要是前後端分離的模式,就避免不了調用後端提供的介面來進行業務交互。
網頁或者app,只要抓下包就可以清楚的知道這個請求獲取到的數據,這樣的介面對爬蟲工程師來說是一種福音,要抓你的數據簡直輕而易舉。
數據的安全性非常重要,特別是用戶相關的資訊,稍有不慎就會被不法分子盜用,所以我們對這塊要非常重視,容不得馬虎。

如何保證API調用時數據的安全性?

  1. 通訊使用https
  2. 請求籤名,防止參數被篡改
  3. 身份確認機制,每次請求都要驗證是否合法
  4. APP中使用ssl pinning防止抓包操作
  5. 對所有請求和響應都進行加解密操作
  6. 等等方案…….

對所有請求和響應都進行加解密操作

方案有很多種,當你做的越多,也就意味著安全性更高,今天我跟大家來介紹一下對所有請求和響應都進行加解密操作的方案,即使能抓包,即使能調用我的介面,但是我返回的數據是加密的,只要加密演算法夠安全,你得到了我的加密內容也對我沒什麼影響。
像這種工作最好做成統一處理的,你不能讓每個開發都去關注這件事情,如果讓每個開發去關注這件事情就很麻煩了,返回數據時還得手動調用下加密的方法,接收數據後還得調用下解密的方法。
為此,我基於Spring Boot封裝了一個Starter, 內置了AES加密演算法。GitHub地址如下:
//github.com/feifuzeng/spring-boot-starter-encrypt

入門使用

  1. 下載源碼並在項目工程中引入
  2. 在啟動類上增加加解密註解
    在啟動類上增加@EnableEncrypt註解開啟加解密操作:
@EnableEncrypt
@SpringBootApplication
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }
}
  1. 配置文件增加配置
spring.encrypt.key=abcdef0123456789
spring.encrypt.debug=false

spring.encrypt.key:加密key,必須是16位
spring.encrypt.debug:是否開啟調試模式,默認為false,如果為true則不啟用

  1. 加解密操作
    為了考慮通用性,不會對所有請求都執行加解密,基於註解來做控制
    響應數據需要加密的話,就在Controller的方法上加@Encrypt註解即可。
    @Encrypt
    @RequestMapping("/list")
    @ResponseBody
    public List<User> query(){
        List<User> list = new ArrayList<>();
        User user = new User();
        user.setGender(1);
        user.setId("1");
        user.setName("11111");
        list.add(user);
        return list;
    }

當我們訪問/list介面時,返回的數據就是加密之後base64編碼的格式。
還有一種操作就是前端提交的數據,分為2種情況,一種是get請求,這種暫時沒處理,後面再考慮,目前只處理的post請求,基於json格式提交的方式,也就是說後台需要用@RequestBody接收數據才行, 需要解密的操作我們加上@Decrypt註解即可。

    @Encrypt
    @Decrypt
    @RequestMapping("/queryList")
    @ResponseBody
    public List<User> queryList(@RequestBody User user){
        List<User> list = new ArrayList<>();
        list.add(user);
        return list;
    }

加了@Decrypt註解後,前端提交的數據需要按照AES加密演算法,進行加密,然後提交到後端,後端這邊會自動解密,然後再映射到參數對象中。
上面講解的都是後端的程式碼,前端使用的話我們以js來講解,當然你也能用別的語言來做。
前端需要做的就2件事情:

  1. 統一處理數據的響應,在渲染到頁面之前進行解密操作
  2. 當有POST請求的數據發出時,統一加密
    js加密文件請參考我GitHub中示例工程中引入的js文件
    示例程式碼:
<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8">
		<title>數據傳輸解密示例</title>
	</head>
	<script type="text/javascript" src = "js/aes.js"></script>
	<script type="text/javascript" src = "js/crypto-js.js"></script>
	<script type="text/javascript" src = "js/pad-zeropadding.js"></script>
	<script src="//cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
<body>
	<input type="button" value="獲取數據" onclick="getData()"/>
	<hr>
	<br>
	姓名:<input type="text" id="name" /><br>
	性別:<input type="text" id="gender" /><br>
	<input type="button" value="發送數據" onclick="sendData()"/>
	<script>
	var backUrl ='';
	//var backUrl ='//localhost:8080';
	function getData() {
		$.ajax({
	        type: "GET",
	        url:backUrl+"/user/list",
	        success: function(resData) {
	        	alert("返回的數據:"+resData);
	        	alert("解密之後的數據:"+Decrypt(resData));
	        }
		});
	}
	
	function sendData() {
	var name=document.getElementById("name").value;
	var gender=document.getElementById("gender").value;
		alert("發送的數據:"+JSON.stringify({"name":name,"gender":gender}));
		$.ajax({
	        type: "POST",
	        url:backUrl+"/user/queryList",
	        data:JSON.stringify({"name":name,"gender":gender}),
	        dataType:'json',
	        contentType: "application/json",
	        success: function(resData) {
	        	alert("返回的數據:"+resData);
	        	alert("解密之後:"+Decrypt(resData));
	        }
		}); 
	}
	
	var key = CryptoJS.enc.Utf8.parse("abcdef0123456789");
	function Encrypt(word) {
		var srcs = CryptoJS.enc.Utf8.parse(word);
		var encrypted = CryptoJS.AES.encrypt(srcs, key, {
			mode : CryptoJS.mode.ECB,
			padding : CryptoJS.pad.Pkcs7
		});
		return encrypted.toString();
	}
	
	function Decrypt(word) {
		var decrypt = CryptoJS.AES.decrypt(word, key, {
			mode : CryptoJS.mode.ECB,
			padding : CryptoJS.pad.Pkcs7
		});
		return CryptoJS.enc.Utf8.stringify(decrypt).toString();
	}
	
	</script>  
</body>
</html>

到此為止,我們就為整個前後端交互的通訊做了一個加密的操作,只要加密的key不泄露,別人得到你的數據也沒用,問題是如何保證key不泄露呢?
服務端的安全性較高,可以存儲在資料庫中或者配置文件中,畢竟在我們自己的伺服器上,最危險的其實就時前端了,app還好,可以打包,但是要防止反編譯等等問題。
如果是webapp則可以依賴於js加密來實現,下面我給大家介紹一種動態獲取加密key的方式,只不過實現起來比較複雜,我們不上程式碼,只講思路:
加密演算法有對稱加密和非對稱加密,AES是對稱加密,RSA是非對稱加密。之所以用AES加密數據是因為效率高,RSA運行速度慢,可以用於簽名操作。
我們可以用這2種演算法互補,來保證安全性,用RSA來加密傳輸AES的秘鑰,用AES來加密數據,兩者相互結合,優勢互補。
其實大家理解了HTTPS的原理的話對於下面的內容應該是一看就懂的,HTTPS比HTTP慢的原因都是因為需要讓客戶端與伺服器端安全地協商出一個對稱加密演算法。剩下的就是通訊時雙方使用這個對稱加密演算法進行加密解密。
客戶端啟動,發送請求到服務端,服務端用RSA演算法生成一對公鑰和私鑰,我們簡稱為pubkey1,prikey1,將公鑰pubkey1返回給客戶端。
客戶端拿到服務端返回的公鑰pubkey1後,自己用RSA演算法生成一對公鑰和私鑰,我們簡稱為pubkey2,prikey2,並將公鑰pubkey2通過公鑰pubkey1加密,加密之後傳輸給服務端。
此時服務端收到客戶端傳輸的密文,用私鑰prikey1進行解密,因為數據是用公鑰pubkey1加密的,通過解密就可以得到客戶端生成的公鑰pubkey2
然後自己在生成對稱加密,也就是我們的AES,其實也就是相對於我們配置中的那個16的長度的加密key,生成了這個key之後我們就用公鑰pubkey2進行加密,返回給客戶端,因為只有客戶端有pubkey2對應的私鑰prikey2,只有客戶端才能解密,客戶端得到數據之後,用prikey2進行解密操作,得到AES的加密key,最後就用加密key進行數據傳輸的加密,至此整個流程結束。

spring-boot-starter-encrypt原理

最後我們來簡單的介紹下spring-boot-starter-encrypt的原理吧,也讓大家能夠理解為什麼Spring Boot這麼方便,只需要簡單的配置一下就可以實現很多功能。

啟動類上的@EnableEncrypt註解是用來開啟功能的,通過@Import導入自動配置類

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({EncryptAutoConfiguration.class})
public @interface EnableEncrypt {

}

EncryptAutoConfiguration中配置請求和響應的處理類,用的是Spring中的RequestBodyAdvice和ResponseBodyAdvice,在Spring中對請求進行統計處理比較方便。如果還要更底層去封裝那就要從servlet那塊去處理了。

/**
 * 加解密自動配置
 *
 */
@Configuration
@Component
@EnableAutoConfiguration
@EnableConfigurationProperties(EncryptProperties.class)
public class EncryptAutoConfiguration {


	/**
	 * 配置請求解密
	 * @return
	 */
	@Bean
	public EncryptResponseBodyAdvice encryptResponseBodyAdvice() {
		return new EncryptResponseBodyAdvice();
	}

	/**
	 * 配置請求加密
	 * @return
	 */
	@Bean
	public EncryptRequestBodyAdvice encryptRequestBodyAdvice() {
		return new EncryptRequestBodyAdvice();
	}

}

通過RequestBodyAdvice和ResponseBodyAdvice就可以對請求響應做處理了,大概的原理就是這麼多了。

結語

歡迎關注微信公眾號『碼仔zonE』,專註於分享Java、雲計算相關內容,包括SpringBoot、SpringCloud、微服務、Docker、Kubernetes、Python等領域相關技術乾貨,期待與您相遇!