'前後端分離架構中的接口安全(上)'

"

互聯網發展至今,已由傳統的前後端統一架構演變為如今的前後端分離架構,最初的前端網頁大多由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為私鑰對其解密,得到最終的密鑰,此流程結束。

(注:上述提到的所以數據均不能保存到文件裡,必須保存到內存中,因為只有保存到內存中,黑客才拿不到這些核心數據,所以每次使用獲取的密鑰前先判斷內存中的密鑰是否存在,不存在,則需要獲取。)

為了便於理解,我畫了一個簡單的流程圖:

"

互聯網發展至今,已由傳統的前後端統一架構演變為如今的前後端分離架構,最初的前端網頁大多由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;
}
@Servicepublic 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,則跳轉到登錄界面。

這種算法較為簡單,這裡就不寫出具體實現了。

由於篇幅問題,剩餘方式下篇會繼續介紹,敬請期待!

"

相關推薦

推薦中...