接口安全規范


前后端分離架構中的接口安全(上篇)

互聯網發展至今,已由傳統的前后端統一架構演變為如今的前后端分離架構,最初的前端網頁大多由JSP、ASP、PHP等動態網頁技術生成,前后端十分耦合,也不利於擴展。現在的前端分支很多,如:Web前端、Android端、IOS端,甚至還有物聯網等。前后端分離的好處就是后端只需要實現一套界面,所有前端即可通用。
前后端的傳輸通過HTTP進行傳輸,也帶來了一些安全問題,如果抓包、模擬請求、洪水攻擊、參數劫持、網絡爬蟲等等。如何對非法請求進行有效攔截,保護合法請求的權益是這篇文章需要討論的。
作者依據多年互聯網后端開發經驗,總結出了以下提升網絡安全的方式:

采用HTTPS協議

密鑰存儲到服務端而非客戶端,客戶端應從服務端動態獲取密鑰

請求隱私接口,利用token機制校驗其合法性

對請求參數進行合法性校驗

對請求參數進行簽名認證,防止參數被篡改

對輸入輸出參數進行加密,客戶端加密輸入參數,服務端加密輸出參數

那么,下面我將對以上方式展開做詳細說明。

HTTP VS HTTPS
普通的HTTP協議是以明文形式進行傳輸,不提供任何方式的數據加密,很容易解讀傳輸報文。而HTTPS協議在HTTP基礎上加入了SSL層,而SSL層通過證書來驗證服務器的身份,並為瀏覽器和服務器之間的通信加密,保護了傳輸過程中的數據安全。

動態密鑰的獲取
對於可逆加密算法,是需要通過密鑰進行加解密,如果直接放到客戶端,那么很容易反編譯后拿到密鑰,這是相當不安全的做法,因此考慮將密鑰放到服務端,由服務端提供接口,讓客戶單動態獲取密鑰,具體做法如下:
1、客戶端先通過RSA算法生成一套客戶端的公私鑰對(clientPublicKey和clientPrivateKey)
2、調用getRSA接口,服務端會返回serverPublicKey
3、客戶端拿到serverPublicKey后,用serverPublicKey作為公鑰,clientPublicKey作為明文對clientPublicKey進行RSA加密,調用getKey接口,將加密后的clientPublicKey傳給服務端,服務端接收到請求后會傳給客戶端RSA加密后的密鑰
4、客戶端拿到后以clientPrivateKey為私鑰對其解密,得到最終的密鑰,此流程結束。
(注:上述提到的所以數據均不能保存到文件里,必須保存到內存中,因為只有保存到內存中,黑客才拿不到這些核心數據,所以每次使用獲取的密鑰前先判斷內存中的密鑰是否存在,不存在,則需要獲取。)
為了便於理解,我畫了一個簡單的流程圖:

那么具體是如何實現的呢,請看代碼:

 

#全局密鑰配置,所以加密算法統一密鑰
api:
encrypt:
key: d7b85c6e414dbcda
#此配置的公司鑰信息為測試數據,不能直接使用,請自行重新生成公私鑰
rsa:
publicKey: MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCcZlkHaSN0fw3CWGgzcuPeOKPdNKHdc2nR6KLXazhhzFhe78NqMrhsyNTf3651acS2lADK3CzASzH4T0bT+GnJ77joDOP+0SqubHKwAIv850lT0QxS+deuUHg2+uHYhdhIw5NCmZ0SkNalw8igP1yS+2TEIYan3lakPBvZISqRswIDAQAB
privateKey: MIICeAIBADANBgkqhkiG9w0BAQeFAcSCAmIwggJeAgEAAoGBAJxmWQdpI3R/DcJYaDNy4944o900od1zadHootdrOGHMWF7vw2oyuGzI1N/frmxoVLaUAMrcLMBLMfhPRtP4acnvuOgM4/7RKq5scrAAi/znSVPRDFL5165QeDb64diF2EjDk0KZnRKQ1qXDyKA/XJL7ZMQhhqfeVqQ8G9khKpGzAgMBAAECgYEAj+5AkGlZj6Q9bVUez/ozahaF9tSxAbNs9xg4hDbQNHByAyxzkhALWVGZVk3rnyiEjWG3OPlW1cBdxD5w2DIMZ6oeyNPA4nehYrf42duk6AI//vd3GsdJa6Dtf2has1R+0uFrq9MRhfRunAf0w6Z9zNbiPNSd9VzKjjSvcX7OTsECQQD20kekMToC6LZaZPr1p05TLUTzXHvTcCllSeXWLsjVyn0AAME17FJRcL9VXQuSUK7PQ5Lf5+OpjrCRYsIvuZg9AkEAojdC6k3SqGnbtftLfGHMDn1fe0nTJmL05emwXgJvwToUBdytvgbTtqs0MsnuaOxMIMrBtpbhS6JiB5Idb7GArwJAfKTkmP5jFWT/8dZdBgFfhJGv6FakEjrqLMSM1QT7VzvStFWtPNYDHC2b8jfyyAkGvpSZb4ljZxUwBbuh5QgM4QJBAJDrV7+lOP62W9APqdd8M2X6gbPON3JC09EW3jaObLKupTa7eQicZsX5249IMdLQ0A43tanez3XXo0ZqNhwT8wcCQQDUubpNLwgAwN2X7kW1btQtvZW47o9CbCv+zFKJYms5WLrVpotjkrCgPeuloDAjxeHNARX8ZTVDxls6KrjLH3lT

##依賴包

<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>

###驗簽過程

public class AesEncryptUtils {

private static final String KEY = "d7585fde114abcda";
private static final String ALGORITHMSTR = "AES/CBC/NoPadding";

public static String base64Encode(byte[] bytes) {
return Base64.encodeBase64String(bytes);
}

public static byte[] base64Decode(String base64Code) throws Exception {
return Base64.decodeBase64(base64Code);
}

public static byte[] aesEncryptToBytes(String content, String encryptKey) throws Exception {
KeyGenerator kgen = KeyGenerator.getInstance("AES");
kgen.init(128);
Cipher cipher = Cipher.getInstance(ALGORITHMSTR);
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(encryptKey.getBytes(), "AES"));
return cipher.doFinal(content.getBytes("utf-8"));
}

public static String aesEncrypt(String content, String encryptKey) throws Exception {
return base64Encode(aesEncryptToBytes(content, encryptKey));
}

public static String aesDecryptByBytes(byte[] encryptBytes, String decryptKey) throws Exception {
KeyGenerator kgen = KeyGenerator.getInstance("AES");
kgen.init(128);
Cipher cipher = Cipher.getInstance(ALGORITHMSTR);
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(decryptKey.getBytes(), "AES"));
byte[] decryptBytes = cipher.doFinal(encryptBytes);
return new String(decryptBytes);
}

public static String aesDecrypt(String encryptStr, String decryptKey) throws Exception {
return aesDecryptByBytes(base64Decode(encryptStr), decryptKey);
}

public static void main(String[] args) throws Exception {
String content = "{name:\"lynn\",id:1}";
System.out.println("加密前:" + content);

String encrypt = aesEncrypt(content, KEY);
System.out.println(encrypt.length() + ":加密后:" + encrypt);

String decrypt = aesDecrypt("H9pGuDMV+iJoS8YSfJ2Vx0NYN7v7YR0tMm1ze5zp0WvNEFXQPM7K0k3IDUbYr5ZIckTkTHcIX5Va/cstIPrYEK3KjfCwtOG19l82u+x6soa9FzAtdL4EW5HAFMmpVJVyG3wz/XUysIRCwvoJ20ruEwk07RB3ojc1Vtns8t4kKZE=", "d7b85f6e214abcda");
System.out.println("解密后:" + decrypt);
}
}
public class RSAUtils {

public static final String CHARSET = "UTF-8";
public static final String RSA_ALGORITHM = "RSA";


public static Map<String, String> createKeys(int keySize){
//為RSA算法創建一個KeyPairGenerator對象
KeyPairGenerator kpg;
try{
kpg = KeyPairGenerator.getInstance(RSA_ALGORITHM);
}catch(NoSuchAlgorithmException e){
throw new IllegalArgumentException("No such algorithm-->[" + RSA_ALGORITHM + "]");
}

//初始化KeyPairGenerator對象,密鑰長度
kpg.initialize(keySize);
//生成密匙對
KeyPair keyPair = kpg.generateKeyPair();
//得到公鑰
Key publicKey = keyPair.getPublic();
String publicKeyStr = Base64.encodeBase64String(publicKey.getEncoded());
//得到私鑰
Key privateKey = keyPair.getPrivate();
String privateKeyStr = Base64.encodeBase64String(privateKey.getEncoded());
Map<String, String> keyPairMap = new HashMap<>(2);
keyPairMap.put("publicKey", publicKeyStr);
keyPairMap.put("privateKey", privateKeyStr);

return keyPairMap;
}

/**
* 得到公鑰
* @param publicKey 密鑰字符串(經過base64編碼)
* @throws Exception
*/
public static RSAPublicKey getPublicKey(String publicKey) throws NoSuchAlgorithmException, InvalidKeySpecException {
//通過X509編碼的Key指令獲得公鑰對象
KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM);
X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(Base64.decodeBase64(publicKey));
RSAPublicKey key = (RSAPublicKey) keyFactory.generatePublic(x509KeySpec);
return key;
}

/**
* 得到私鑰
* @param privateKey 密鑰字符串(經過base64編碼)
* @throws Exception
*/
public static RSAPrivateKey getPrivateKey(String privateKey) throws NoSuchAlgorithmException, InvalidKeySpecException {
//通過PKCS#8編碼的Key指令獲得私鑰對象
KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM);
PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKey));
RSAPrivateKey key = (RSAPrivateKey) keyFactory.generatePrivate(pkcs8KeySpec);
return key;
}

/**
* 公鑰加密
* @param data
* @param publicKey
* @return
*/
public static String publicEncrypt(String data, RSAPublicKey publicKey){
try{
Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
return Base64.encodeBase64String(rsaSplitCodec(cipher, Cipher.ENCRYPT_MODE, data.getBytes(CHARSET), publicKey.getModulus().bitLength()));
}catch(Exception e){
throw new RuntimeException("加密字符串[" + data + "]時遇到異常", e);
}
}

/**
* 私鑰解密
* @param data
* @param privateKey
* @return
*/

public static String privateDecrypt(String data, RSAPrivateKey privateKey){
try{
Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, privateKey);
return new String(rsaSplitCodec(cipher, Cipher.DECRYPT_MODE, Base64.decodeBase64(data), privateKey.getModulus().bitLength()), CHARSET);
}catch(Exception e){
throw new RuntimeException("解密字符串[" + data + "]時遇到異常", e);
}
}

/**
* 私鑰加密
* @param data
* @param privateKey
* @return
*/

public static String privateEncrypt(String data, RSAPrivateKey privateKey){
try{
Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
return Base64.encodeBase64String(rsaSplitCodec(cipher, Cipher.ENCRYPT_MODE, data.getBytes(CHARSET), privateKey.getModulus().bitLength()));
}catch(Exception e){
throw new RuntimeException("加密字符串[" + data + "]時遇到異常", e);
}
}

/**
* 公鑰解密
* @param data
* @param publicKey
* @return
*/

public static String publicDecrypt(String data, RSAPublicKey publicKey){
try{
Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, publicKey);
return new String(rsaSplitCodec(cipher, Cipher.DECRYPT_MODE, Base64.decodeBase64(data), publicKey.getModulus().bitLength()), CHARSET);
}catch(Exception e){
throw new RuntimeException("解密字符串[" + data + "]時遇到異常", e);
}
}

private static byte[] rsaSplitCodec(Cipher cipher, int opmode, byte[] datas, int keySize){
int maxBlock = 0;
if(opmode == Cipher.DECRYPT_MODE){
maxBlock = keySize / 8;
}else{
maxBlock = keySize / 8 - 11;
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offSet = 0;
byte[] buff;
int i = 0;
try{
while(datas.length > offSet){
if(datas.length-offSet > maxBlock){
buff = cipher.doFinal(datas, offSet, maxBlock);
}else{
buff = cipher.doFinal(datas, offSet, datas.length-offSet);
}
out.write(buff, 0, buff.length);
i++;
offSet = i * maxBlock;
}
}catch(Exception e){
throw new RuntimeException("加解密閥值為["+maxBlock+"]的數據時發生異常", e);
}
byte[] resultDatas = out.toByteArray();
IOUtils.closeQuietly(out);
return resultDatas;
}

public static void main(String[] args) throws Exception{
Map<String, String> keyMap = RSAUtils.createKeys(1024);
String publicKey = keyMap.get("publicKey");
String privateKey = "MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBAJxmWQdpI3R/DcJYaDNy4944o900od1zadHootdrOGHMWF7vw2oyuGzI1N/frmxoVLaUAMrcLMBLMfhPRtP4acnvuOgM4/7RKq5scrAAi/znSVPRDFL5165QeDb64diF2EjDk0KZnRKQ1qXDyKA/XJL7ZMQhhqfeVqQ8G9khKpGzAgMBAAECgYEAj+5AkGlZj6Q9bVUez/ozahaF9tSxAbNs9xg4hDbQNHByAyxzkhALWVGZVk3rnyiEjWG3OPlW1cBdxD5w2DIMZ6oeyNPA4nehYrf42duk6AI//vd3GsdJa6Dtf2has1R+0uFrq9MRhfRunAf0w6Z9zNbiPNSd9VzKjjSvcX7OTsECQQD20kekMToC6LZaZPr1p05TLUTzXHvTcCllSeXWLsjVyn0AAME17FJRcL9VXQuSUK7PQ5Lf5+OpjrCRYsIvuZg9AkEAojdC6k3SqGnbtftLfGHMDn1fe0nTJmL05emwXgJvwToUBdytvgbTtqs0MsnuaOxMIMrBtpbhS6JiB5Idb7GArwJAfKTkmP5jFWT/8dZdBgFfhJGv6FYkEjrqLMSM1QT7VzvStFWtPNYDHC2b8jfyyAkGvpSZb4ljZxUwBbuh5QgM4QJBAJDrV7+lOP62W9APqdd8M2X6gbPON3JC09EW3jaObLKupTa7eQicZsX5249IMdLQ0A43tanez3XXo0ZqNhwT8wcCQQDUubpNLwgAwN2X7kW1btQtvZW47o9CbCv+zFKJYms5WLrVpotjkrCgPeuloDAjxeHNARX8ZTVDxls6KrjLH3lT";
System.out.println("公鑰: \n\r" + publicKey);
System.out.println("私鑰: \n\r" + privateKey);

System.out.println("公鑰加密——私鑰解密");
String str = "站在大明門前守衛的禁衛軍,事先沒有接到\n" +
"有關的命令,但看到大批盛裝的官員來臨,也就\n" +
"以為確系舉行大典,因而未加詢問。進大明門即\n" +
"為皇城。文武百官看到端門午門之前氣氛平靜,\n" +
"城樓上下也無朝會的跡象,既無幾案,站隊點名\n" +
"的御史和御前侍衛“大漢將軍”也不見蹤影,不免\n" +
"心中揣測,互相詢問:所謂午朝是否訛傳?";
System.out.println("\r明文:\r\n" + str);
System.out.println("\r明文大小:\r\n" + str.getBytes().length);
String encodedData = RSAUtils.publicEncrypt(str, RSAUtils.getPublicKey(publicKey));
System.out.println("密文:\r\n" + encodedData);
String decodedData = RSAUtils.privateDecrypt("X4hHPa9NjPd5QJGPus+4+hWmOzbWg7oCJ1+Vc+7dHW81nEhkYnJpFyV5xcDkg70N2Mym+YAJ1PvYY9sQWf9/EkUE61TpUKBmDaGWLjEr3A1f9cKIelqLKLsJGdXEOr7Z55k4vYFvA7N3Vf5KQo3NrouvIT4wR+SjH4tDQ8tNh3JH8BvXLtXqGa2TCK2z1AzHNgYzcLCrqDasd7UDHRPZPiW4thktM/whjBn0tU9B/kKjAjLuYttKLEmy5nT7v7u16aZ6ehkk+kzvuCXF%2B3RsqraISDPbsTki2agJyqsycRx3w7CvKRyUbZhFaNcWigOwmcbZVoiom+ldh7Vh6HYqDA==", RSAUtils.getPrivateKey(privateKey));
System.out.println("解密后文字: \r\n" + decodedData);

}
}
/**
* 私鑰輸入參數(其實就是客戶端通過服務端返回的公鑰加密后的客戶端自己生成的公鑰)
*/
public class KeyRequest {

/**
* 客戶端自己生成的加密后公鑰
*/
@NotNull
private String clientEncryptPublicKey;

public String getClientEncryptPublicKey() {
return clientEncryptPublicKey;
}

public void setClientEncryptPublicKey(String clientEncryptPublicKey) {
this.clientEncryptPublicKey = clientEncryptPublicKey;
}
}
/**
* RSA生成的公私鑰輸出參數
*/
public class RSAResponse extends BaseResponse{

private String serverPublicKey;

private String serverPrivateKey;

public static class Builder{
private String serverPublicKey;

private String serverPrivateKey;

public Builder setServerPublicKey(String serverPublicKey){
this.serverPublicKey = serverPublicKey;
return this;
}

public Builder setServerPrivateKey(String serverPrivateKey){
this.serverPrivateKey = serverPrivateKey;
return this;
}

public RSAResponse build(){
return new RSAResponse(this);
}

}

public static Builder options(){
return new Builder();
}

public RSAResponse(Builder builder){
this.serverPrivateKey = builder.serverPrivateKey;
this.serverPublicKey = builder.serverPublicKey;
}

public String getServerPrivateKey() {
return serverPrivateKey;
}

public String getServerPublicKey() {
return serverPublicKey;
}
}
/**
* 私鑰輸出參數
*/
public class KeyResponse extends BaseResponse{

/**
* 整個系統所有加密算法共用的密鑰
*/
private String key;

public static class Builder{
private String key;

public Builder setKey(String key){
this.key = key;
return this;
}

public KeyResponse build(){
return new KeyResponse(this);
}
}

public static Builder options(){
return new Builder();
}

private KeyResponse(Builder builder){
this.key = builder.key;
}

public String getKey() {
return key;
}

}
/**
* API傳輸加解密相關接口
*/
public interface EncryptOpenService {

/**
* 生成RSA公私鑰
* @return
*/
SingleResult<RSAResponse> getRSA();

/**
* 獲得加解密用的密鑰
* @param request
* @return
*/
SingleResult<KeyResponse> getKey(KeyRequest request) throws Exception;
}

@Service
public class EncryptOpenServiceImpl implements EncryptOpenService{

@Value("${rsa.publicKey}")
private String publicKey;
@Value("${rsa.privateKey}")
private String privateKey;
@Value("${api.encrypt.key}")
private String key;

@Override
public SingleResult<RSAResponse> getRSA() {
RSAResponse response = RSAResponse.options()
.setServerPublicKey(publicKey)
.build();
return SingleResult.buildSuccess(response);
}

@Override
public SingleResult<KeyResponse> getKey(KeyRequest request)throws Exception {
String clientPublicKey = RSAUtils.privateDecrypt(request.getClientEncryptPublicKey(), RSAUtils.getPrivateKey(privateKey));
String encryptKey = RSAUtils.publicEncrypt(key,RSAUtils.getPublicKey(clientPublicKey));
KeyResponse response = KeyResponse.options()
.setKey(encryptKey)
.build();
return SingleResult.buildSuccess(response);
}
}
@RestController
@RequestMapping("open/encrypt")
public class EncryptController {

@Autowired
private EncryptOpenService encryptOpenService;

@RequestMapping(value = "getRSA",method = RequestMethod.POST)
//@DisabledEncrypt
public SingleResult<RSAResponse> getRSA(){
return encryptOpenService.getRSA();
}

@RequestMapping(value = "getKey",method = RequestMethod.POST)
//@DisabledEncrypt
public SingleResult<KeyResponse> getKey(@Valid @RequestBody KeyRequest request)throws Exception{
return encryptOpenService.getKey(request);
}
}
---------------------

接口請求的合法性校驗
對於一些隱私接口(即必須要登錄才能調用的接口),我們需要校驗其合法性,即只有登錄用戶才能成功調用,具體思路如下:
1、調用登錄或注冊接口成功后,服務端會返回token(設置較短有效時間)和refreshToken(設定較長有效時間)
2、隱私接口每次請求接口在請求頭帶上token如header(“token”,token),若服務端 返回403錯誤,則調用refreshToken接口獲取新的token重新調用接口,若refreshToken接口繼續返回403,則跳轉到登錄界面。
這種算法較為簡單,這里就不寫出具體實現了。
由於篇幅問題,剩余方式下篇會繼續介紹,敬請期待!
---------------------

一種安全的前后端數據交互方案

 

加密方案:AES + RSA兩種加密方式混合使用,能夠實現數據的全程加密(無論是上傳,還是拉取)。

 

1、從客戶端動態生成16位AES密碼

2、使用第一步生成的AES密碼加密要上發的請求數據,由於AES加密后是byte[]數據,所以這里還需要使用base64封裝一層以方便傳輸。格式大概如下:

{
"key":"1234567890123456"
"data":"5rWL6K+V5pWw5o2u"
}

3、使用RSA公鑰加密第二步生成的數據中的key,從而實現對key的保密,RSA加密后生成的二進制數據同樣還需要再使用base64封裝一層以方便傳輸,客戶端的加密過程到這里就基本完成,然后就可以將該請求發送到服務端了。(RSA公鑰客戶端持有,RSA秘鑰服務端持有)

 

4、服務端收到了客戶端發送過來的請求后,拿到key參數,即為RSA加密byte。

 

5、使用服務端持有的私鑰解密第4步獲取到的RSA加密byte。從而獲取到了第二步時候的數據,同時需要base64解碼data數據。也即拿到了AES的key。

 

6、獲取到AES的key后,便可以使用其來解密第5步中的data字段,也就是客戶端的真正請求數據。進而做相關操作,並生成相應返回值。

 

7、服務端返回值生成后,同樣使用第5步獲取到的key進行加密,並得到返回的data(同樣的base64封裝)。與客戶端加密不同的是,服務端的返回中key字段是客戶端的key字段加了rsa簽名后的數據。格式大概如下。

{
"key":"xxxxxxxxxx簽名后的key",
"data":"5rWL6K+V5pWw5o2u"
}

8、使用服務端持有的私鑰對從客戶端傳過來的key的二進制數據進行簽名(以防止中間人攻擊),然后將數據向客戶端返回。

 

9、客戶端拿到服務端的數據返回后,先使用本地持有的公鑰驗證簽名。然后base64解碼。

 

10、使用請求時候生成的key來解碼第9步驗證通過的數據,解碼后便得到了服務器端的真正返回,至此流程大概就完成了。

 

最后我們來分析下,為什么說,這套方案是比較安全的。

 

首先我們假設客戶端被反編譯,那他能獲取到什么呢,一個動態生成的rsa加密key嗎,拿過來並沒有卵用。不過他能拿到我們的客戶端公鑰,拿到公鑰之后,他可以做兩件事情,1、偽造一個客戶端,發送請求。 2、可以用來驗證任意請求是否來自我們的服務器。 這兩種情況也就夠他自己一個人玩玩,都無法構成威脅。

 

其次,我們假設他通過抓包,獲取了到了我們某個用戶的請求全過程。接下來他可能首先分析上行數據,得到的是一個rsa加密后的數據,同樣我們假設他反編譯了我們的客戶端,並且拿到了公鑰,然而他還是解不了我們的rsa加密。上行數據無法破解,那他接下來就要來分析下行數據了,下行數據封裝比較簡單,而他也有我們的公鑰,完全可以驗證通過,並長驅直入直接拿到了我們的AES加密串,可惜啊,可惜,下行數據中並沒有AES的秘鑰啊。

 

總結一下,這套方案要被破解,思路只有通過其他途徑直接控制服務器,然后再拿到我們的私鑰,那就死翹翹了。不過真到了服務器都被人家攻陷了,那人家還拿你私鑰干嘛,人家直接在上面掛個木馬來轉接客戶端請求不就可以了。綜上所述,這其實是一套相當完美的前后端數據交互方案。

 

盡管簡單,但是記錄下來,以防忘記。


---------------------

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

前言

前后端分離的開發方式,我們以接口為標准來進行推動,定義好接口,各自開發自己的功能,最后進行聯調整合。無論是開發原生的APP還是webapp還是PC端的軟件,只要是前后端分離的模式,就避免不了調用后端提供的接口來進行業務交互。

網頁或者app,只要抓下包就可以清楚的知道這個請求獲取到的數據,這樣的接口對爬蟲工程師來說是一種福音,要抓你的數據簡直輕而易舉。

數據的安全性非常重要,特別是用戶相關的信息,稍有不慎就會被不法分子盜用,所以我們對這塊要非常重視,容不得馬虎。

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

  1. 通信使用https

  2. 請求簽名,防止參數被篡改

  3. 身份確認機制,每次請求都要驗證是否合法

  4. APP中使用ssl pinning防止抓包操作

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

  6. 等等方案…….

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

方案有很多種,當你做的越多,也就意味着安全性更高,今天我跟大家來介紹一下對所有請求和響應都進行加解密操作的方案,即使能抓包,即使能調用我的接口,但是我返回的數據是加密的,只要加密算法夠安全,你得到了我的加密內容也對我沒什么影響。

像這種工作最好做成統一處理的,你不能讓每個開發都去關注這件事情,如果讓每個開發去關注這件事情就很麻煩了,返回數據時還得手動調用下加密的方法,接收數據后還得調用下解密的方法。

為此,我基於Spring Boot封裝了一個Starter, 內置了AES加密算法。GitHub地址如下:

https://github.com/yinjihuan/spring-boot-starter-encrypt

先來看看怎么使用,可以下載源碼,然后引入即可,然后在啟動類上增加@EnableEncrypt注解開啟加解密操作:

@EnableEncrypt
@SpringBootApplication
public class App {
   public static void main(String[] args) {
       SpringApplication.run(App.class, args);
   }
}

增加加密的key配置:

 
        

spring.encrypt.key=abcdef0123456789
spring.encrypt.debug=false

  • spring.encrypt.key:加密key,必須是16位

  • spring.encrypt.debug:是否開啟調試模式,默認為false,如果為true則不啟用加解密操作

為了考慮通用性,不會對所有請求都執行加解密,基於注解來做控制

響應數據需要加密的話,就在Controller的方法上加@Encrypt注解即可。

@Encrypt
@GetMapping("/list")
public Response queryNews(String city) {
   return Response.ok(city);
}

當我們訪問/list接口時,返回的數據就是加密之后base64編碼的格式。

還有一種操作就是前段提交的數據,分為2種情況,一種是get請求,這種暫時沒處理,后面再考慮,目前只處理的post請求,基於json格式提交的方式,也就是說后台需要用@RequestBody接收數據才行, 需要解密的操作我們加上@Decrypt注解即可。

@Decrypt
@PostMapping("/save")
public Response savePageLog(@RequestBody PageLogParam logParam, HttpServletRequest request) {
   pageLogService.save(logParam);
   return Response.ok();
}

加了@Decrypt注解后,前端提交的數據需要按照AES加密算法,進行加密,然后提交到后端,后端這邊會自動解密,然后再映射到參數對象中。

上面講解的都是后端的代碼,前端使用的話我們以js來講解,當然你也能用別的語言來做,如果是原生的安卓app也是用java代碼來處理。

前端需要做的就2件事情:

  1. 統一處理數據的響應,在渲染到頁面之前進行解密操作

  2. 當有POST請求的數據發出時,統一加密

js加密文件請參考我GitHub中encrypt中的aes.js,crypto-js.js,pad-zeropadding.js

我們以axios來作為請求數據的框架,用axios的攔截器來統一處理加密解密操作

首先還是要封裝一個js加解密的類,需要注意的是加密的key需要和后台的對上,不然無法相互解密,代碼如下:

var key  = CryptoJS.enc.Latin1.parse('abcdef0123456789');
var iv   = CryptoJS.enc.Latin1.parse('abcdef0123456789');
// 加密
function EncryptData(data) {
   var srcs = CryptoJS.enc.Utf8.parse(data);
   var encrypted = CryptoJS.AES.encrypt(srcs, key, {
       mode : CryptoJS.mode.ECB,
       padding : CryptoJS.pad.Pkcs7
   });
   return encrypted.toString();
}
// 解密
function DecryptData(data) {
   var stime = new Date().getTime();
   var decrypt = CryptoJS.AES.decrypt(data, key, {
       mode : CryptoJS.mode.ECB,
       padding : CryptoJS.pad.Pkcs7
   });
   var result = JSON.parse(CryptoJS.enc.Utf8.stringify(decrypt).toString());
   var etime = new Date().getTime();
   console.log("DecryptData Time:" + (etime - stime));
   return result;
}

axios攔截器中統一處理代碼:

// 添加請求攔截器
axios.interceptors.request.use(function (config) {
   // 對所有POST請加密,必須是json數據提交,不支持表單
   if (config.method == "post") {
       config.data = EncryptData(JSON.stringify(config.data));
   }
   return config;
 }, function (error) {
   return Promise.reject(error);
});
// 添加響應攔截器
axios.interceptors.response.use(function (response) {
   // 后端返回字符串表示需要解密操作
   if(typeof(response.data) == "string"){
       response.data = DecryptData(response.data);
   }
   return response;
 }, function (error) {
   return Promise.reject(error);
});

到此為止,我們就為整個前后端交互的通信做了一個加密的操作,只要加密的key不泄露,別人得到你的數據也沒用,問題是如何保證key不泄露呢?

服務端的安全性較高,可以存儲在數據庫中或者配置文件中,畢竟在我們自己的服務器上,最危險的其實就時前端了,app還好,可以打包,但是要防止反編譯等等問題。

如果是webapp則可以依賴於js加密來實現,下面我給大家介紹一種動態獲取加密key的方式,只不過實現起來比較復雜,我們不上代碼,只講思路:

加密算法有對稱加密和非對稱加密,AES是對稱加密,RSA是非對稱加密。之所以用AES加密數據是因為效率高,RSA運行速度慢,可以用於簽名操作。

我們可以用這2種算法互補,來保證安全性,用RSA來加密傳輸AES的秘鑰,用AES來加密數據,兩者相互結合,優勢互補。

其實大家理解了HTTPS的原理的話對於下面的內容應該是一看就懂的,HTTPS比HTTP慢的原因都是因為需要讓客戶端與服務器端安全地協商出一個對稱加密算法。剩下的就是通信時雙方使用這個對稱加密算法進行加密解密。

  1. 客戶端啟動,發送請求到服務端,服務端用RSA算法生成一對公鑰和私鑰,我們簡稱為pubkey1,prikey1,將公鑰pubkey1返回給客戶端。

  2. 客戶端拿到服務端返回的公鑰pubkey1后,自己用RSA算法生成一對公鑰和私鑰,我們簡稱為pubkey2,prikey2,並將公鑰pubkey2通過公鑰pubkey1加密,加密之后傳輸給服務端。

  3. 此時服務端收到客戶端傳輸的密文,用私鑰prikey1進行解密,因為數據是用公鑰pubkey1加密的,通過解密就可以得到客戶端生成的公鑰pubkey2

  4. 然后自己在生成對稱加密,也就是我們的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就可以對請求響應做處理了,大概的原理就是這么多了。

END

API接口安全性設計

接口的安全性主要圍繞Token、Timestamp和Sign三個機制展開設計,保證接口的數據不會被篡改和重復調用,下面具體來看:

Token授權機制:用戶使用用戶名密碼登錄后服務器給客戶端返回一個Token(通常是UUID),並將Token-UserId以鍵值對的形式存放在緩存服務器中。服務端接收到請求后進行Token驗證,如果Token不存在,說明請求無效。

時間戳超時機制:用戶每次請求都帶上當前時間的時間戳timestamp,服務端接收到timestamp后跟當前時間進行比對,如果時間差大於一定時間(比如5分鍾),則認為該請求失效,這個時間要保證足夠完成本次請求的同時盡量短,可以減少緩存服務器的壓力(見簽名機制)。

簽名機制:將Token和時間戳加上其他請求參數就行MD5或SHA-1算法(可根據情況加點鹽)加密,加密后的數據為本次請求的簽名sign,並將該簽名存放到緩存服務器中,超時時間設定為跟時間戳的超時時間一致(這就是為什么要盡量短,二者時間一致可以保證無論在timestamp規定時間內還是外本URL都只能訪問一次)。服務端接收到請求后以同樣的算法得到簽名,並跟當前的簽名進行比對,如果不一樣,說明參數被更改過,直接返回錯誤標識。同一個簽名只能使用一次,如果發現緩存服務器中已經存在了本次簽名,則拒絕服務。

整個流程如下:

1、客戶端通過用戶名密碼登錄服務器並獲取Token

2、客戶端生成時間戳timestamp,並將timestamp作為其中一個參數

3、客戶端將所有的參數,包括Token和timestamp按照自己的算法進行排序加密得到簽名sign

4、將token、timestamp和sign作為請求時必須攜帶的參數加在每個請求的URL后邊(http://url/request?token=123×tamp=123&sign=123123123)

5、服務端寫一個過濾器對token、timestamp和sign進行驗證,只有三個參數都正確且在規定時間內,本次請求才有效
1
2
3
4
5
6
7
8
9
在以上三中機制的保護下,

如果黑客劫持了請求,並對請求中的參數進行了修改,簽名就無法通過;

如果黑客使用已經劫持的URL進行DOS攻擊,服務器則會因為緩存服務器中已經存在簽名而拒絕服務,所以DOS攻擊也是不可能的;

如果黑客隔一段時間進行一次DOS攻擊(假如這個時間大於簽名在緩存服務器中的緩存時長),則會因為時間戳超時而無法完成請求,這就是為什么簽名的緩存時長要跟時間戳的超時時長一樣。

如果簽名算法和用戶名密碼都暴露了,那齊天大聖來了估計也不好使吧。。。。
---------------------

互聯網開發--HTTP接口安全設計

http接口安全設計的必要性
作為http接口的服務端,要能控制你本身自有數據的讀寫權限。
如果任何客戶端在任何時間都能讀寫你的數據,那么用戶數據很容易被修改。這就好比沒加用戶登錄就可以訪問和讀寫所有的系統數據。根本沒有安全性可言了。

安全設計方案
方案1:(加密時間戳+可變的加密串)進行安全控制
原始請求:
http://www.zsfy.com/layy/getLayyList.htm?layyId=1001
安全控制后的請求:
http://www.zsfy.com/layy/getLayyList.htm?layyId=1001&timestamp=543fafaf678vnkdnfgsakjfdf676&key=HGYT65475Gt57UABNJKH677677KKJJ

時間戳timestamp說明:
(1)作用是:固定時間范圍內,減少同一請求被暴力調用的次數。
(2)客戶端請求里加上時間戳傳到服務端。服務端獲取時間戳與當前時間做比較,如果時間相差3分鍾,則拒絕訪問本接口。
(3)時間戳的作用可以理解為給接口加了個有效期,超過這個有效期就不能再使用了。主要作用就是防止黑客暴力調用。
(4)注意客戶端服務器與接口服務器的時間要進行校准,保持一致;否則有效期會不對,導致接口無法正常使用。
(5)時間戳要進行加密傳輸。因為數據加密后是沒有規律可循的,防止黑客模擬數據,進行非法請求。
加密串key說明:
(1)可變加密串組成:(可變字符串+固定字符串)。采取某種加密算法對(可變字符串+固定字符串)進行加密。
(2)如果沒有可變字符串,只是對某固定字符串加密,數據很容易被黑客模擬,進行非法調用。
(3)如果沒有固定字符串,只是對某可變字符串加密,萬一黑客通過各種測試,知道了key加密的規則,那么客戶端給的key在服務端校驗總是ok的,接口就並非是安全的了。添加某固定的加密串,增加了黑客破解難度,提高了接口調用的安全性。
(4)客戶端與服務端約定好加密串里”可變字符串“和”固定字符串“以及加密算法。服務端接收客戶端的字符串,根據約定好的協議自己生成加密串,再與客戶端的作比較,一致的話可以調用接口,否則不能調用接口。
比如加密串生成規則如下:md5(layyId+”helloword”)
生成規則為參數layyId的值和某個約定的字符串helloword經過md5編碼后的值。
(5)加密算法也可以自定義,不過要注意性能問題,加密時間不要太長。
---------------------

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM