引言
在日常開發中我們對HTTP數據傳輸并不陌生,前端需要與后端進行數據交互往往需要調用后端某個API,然而在前端與后端的請求過程中數據真的安全嗎?下面了解一件關于0.01購買phone事件;
事件
在之前報導一個新聞,某個電商App應用因為程序Bug,被灰色產業0.01元擼走千臺iphone手機,損失近千萬,后來了解到也并非高手所為,其實就是該APP在購買接口中對數據傳輸缺少安全防范,通過HTTP抓包工具對傳輸中的數據進行修改,將原本幾千元的iPhone手機價格修改為0.01進行購買;
案例
看完以上事件部分人可能還是不太抓包的概念,所謂抓包就是依賴某個工具,對客戶端與服務端進行請求、或者對服務端對客戶端響應時過程中進行攔截,可以將其請求數據或響應數據進行篡改;
為了能更好了解,這里我準備了一個模擬購買接口:
這里模擬前端正常調用購買接口;
http://localhost:8080/test?money=6000
按理后端應該會輸出6000,這里我利用fiddle抓包工具對該請求進行攔截,將money參數改為1;
成功將合法價格改為非法價格,如果后端接口驗證不注意即會按照該金額生成訂單,則支付1元即可買到高額商品;
原由
導致以上事件發生無非就兩種情況:
1.對敏感數據使用明文傳輸;
2.在前端進行校驗后,后端接口并未對數據第二次校驗;
如何解決?
在數據傳輸過程中對敏感數據進行加密,即使請求被截獲,也只能獲取到密文信息,無法篡改具體需要修改的數據;
傳輸過程中使用的加密方式通常分為兩類:對稱加密/非對稱加密;
對稱加密:市場上用的比較多的對稱加密有DES、AES、3DES等,對稱加密的流程是通過生成一把秘鑰,分別存儲客戶端以及服務端各自一份,客戶端在請求服務端前先將傳輸的數據通過該秘鑰進行加密,服務端接收到請求之后,先將數據用該秘鑰進行解密再進行處理,從而避免數據在傳輸中被篡改;
缺點:該秘鑰在客戶端加密與服務端解密使用的是同一把,而存儲在客戶端的秘鑰并無法避免泄漏的風險,通過反編譯等手段獲取到秘鑰,那么傳輸過程中攔截請求則照樣可以解密密文數據進行篡改;
Des加密案例:
public class DesDecrypt {
//加密字段
private static String src = "JAVA資料社區";
public static void main(String[] args) {
jdkDES();
bcDES();
}
/*JDK實現*/
public static void jdkDES() {
try {
//生成KEY
KeyGenerator keyGenerator = KeyGenerator.getInstance("DES");
keyGenerator.init(56);
SecretKey secretKey = keyGenerator.generateKey();
byte[] bytesKey = secretKey.getEncoded();
//KEY轉換
DESKeySpec desKeySpec = new DESKeySpec(bytesKey);
SecretKeyFactory factory = SecretKeyFactory.getInstance("DES");
Key convertSecretKey = factory.generateSecret(desKeySpec);
//加密
Cipher cipher = Cipher.getInstance("DES/ECB/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, convertSecretKey);
byte[] result = cipher.doFinal(src.getBytes());
System.out.println("jdk des encrypt : " + new String(Hex.encode(result)));
//解密
cipher.init(Cipher.DECRYPT_MODE, convertSecretKey);
result = cipher.doFinal(result);
System.out.println("jdk des decrypt : " + new String(result).toString());
} catch (Exception e) {
e.printStackTrace();
}
}
/*BC實現*/
public static void bcDES() {
try {
Security.addProvider(new BouncyCastleProvider());
//生成KEY
KeyGenerator keyGenerator = KeyGenerator.getInstance("DES", "BC");
keyGenerator.getProvider();
keyGenerator.init(56);
SecretKey secretKey = keyGenerator.generateKey();
byte[] bytesKey = secretKey.getEncoded();
//KEY轉換
DESKeySpec desKeySpec = new DESKeySpec(bytesKey);
SecretKeyFactory factory = SecretKeyFactory.getInstance("DES");
Key convertSecretKey = factory.generateSecret(desKeySpec);
//加密
Cipher cipher = Cipher.getInstance("DES/ECB/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, convertSecretKey);
byte[] result = cipher.doFinal(src.getBytes());
System.out.println("bc des encrypt : " + new String(Hex.encode(result)));
//解密
cipher.init(Cipher.DECRYPT_MODE, convertSecretKey);
result = cipher.doFinal(result);
System.out.println("bc des decrypt : " + new String(result).toString());
} catch (Exception e) {
e.printStackTrace();
} }
}//運行結果//jdk des encrypt : ab6afc9a81584573cf2f50ceccd28628bd10885ebd84f37c
//jdk des decrypt : Java資料社區
//bc des encrypt : 34fd3ed3d0b6d5ab7baf6db300420f3c4094a2061bb2dd34
//bc des decrypt : Java資料社區
非對稱加密:市場上用的比較多的通常為RSA加密,RSA加密的流程與對稱加密不同,相對于更加安全,通過生成兩把秘鑰(私鑰/公鑰),公鑰通常用來對傳輸過程中的數據進行加密,私鑰則用來對加密數據進行解密,公鑰加密的內容只能對應的私鑰才能解密,私鑰加密的內容只能由對應的公鑰解密,通??蛻舳舜鎯€、服務端則存儲私鑰,這樣,即使公鑰被泄漏也沒法破解密文信息;
缺點:相對對稱加密性能方面要慢很多,非敏感信息不建議使用;
RSA案例:
public class RSAUtils {
protected static final Log log = LogFactory.getLog(RSAUtils.class);
private static String KEY_RSA_TYPE = "RSA";
private static int KEY_SIZE = 1024;//JDK方式RSA加密最大只有1024位
private static int ENCODE_PART_SIZE = KEY_SIZE/8;
public static final String PUBLIC_KEY_NAME = "public";
public static final String PRIVATE_KEY_NAME = "private";
public static void main(String[] args) {
Map<String,String> map=RSAUtils.createRSAKeys(); //創建公私鑰
String str="Java資料社區";
String enStr=encode(str,map.get("public"));//公鑰加密
System.out.println("加密后:"+enStr);
String deStr=decode(enStr,map.get("private"));
System.out.println("解密后:"+deStr);
}
/**
* 創建公鑰秘鑰
* @return
*/
public static Map<String,String> createRSAKeys(){
Map<String,String> keyPairMap = new HashMap<>();//里面存放公私秘鑰的Base64位加密
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(KEY_RSA_TYPE);
keyPairGenerator.initialize(KEY_SIZE,new SecureRandom());
KeyPair keyPair = keyPairGenerator.generateKeyPair();
//獲取公鑰秘鑰
String publicKeyValue = Base64.encodeBase64String(keyPair.getPublic().getEncoded());
String privateKeyValue = Base64.encodeBase64String(keyPair.getPrivate().getEncoded());
//存入公鑰秘鑰,以便以后獲取
keyPairMap.put(PUBLIC_KEY_NAME,publicKeyValue);
keyPairMap.put(PRIVATE_KEY_NAME,privateKeyValue);
} catch (NoSuchAlgorithmException e) {
log.error("當前JDK版本沒找到RSA加密算法!");
e.printStackTrace();
}
return keyPairMap;
}
/**
* 公鑰加密
* 描述:
* 1字節 = 8位;
* 最大加密長度如 1024位私鑰時,最大加密長度為 128-11 = 117字節,不管多長數據,加密出來都是 128 字節長度。
* @param sourceStr
* @param publicKeyBase64Str
* @return
*/
public static String encode(String sourceStr,String publicKeyBase64Str){
byte [] publicBytes = Base64.decodeBase64(publicKeyBase64Str);
//公鑰加密
X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(publicBytes);
List<byte[]> alreadyEncodeListData = new LinkedList<>();
int maxEncodeSize = ENCODE_PART_SIZE - 11;
String encodeBase64Result = null;
try {
KeyFactory keyFactory = KeyFactory.getInstance(KEY_RSA_TYPE);
PublicKey publicKey = keyFactory.generatePublic(x509EncodedKeySpec);
Cipher cipher = Cipher.getInstance(KEY_RSA_TYPE);
cipher.init(Cipher.ENCRYPT_MODE,publicKey);
byte[] sourceBytes = sourceStr.getBytes("utf-8");
int sourceLen = sourceBytes.length;
for(int i=0;i<sourceLen;i+=maxEncodeSize){
int curPosition = sourceLen - i;
int tempLen = curPosition;
if(curPosition > maxEncodeSize){
tempLen = maxEncodeSize;
}
byte[] tempBytes = new byte[tempLen];//待加密分段數據
System.arraycopy(sourceBytes,i,tempBytes,0,tempLen);
byte[] tempAlreadyEncodeData = cipher.doFinal(tempBytes);
alreadyEncodeListData.add(tempAlreadyEncodeData);
}
int partLen = alreadyEncodeListData.size();//加密次數
int allEncodeLen = partLen * ENCODE_PART_SIZE;
byte[] encodeData = new byte[allEncodeLen];//存放所有RSA分段加密數據
for (int i = 0; i < partLen; i++) {
byte[] tempByteList = alreadyEncodeListData.get(i);
System.arraycopy(tempByteList,0,encodeData,i*ENCODE_PART_SIZE,ENCODE_PART_SIZE);
}
encodeBase64Result = Base64.encodeBase64String(encodeData);
} catch (Exception e) {
e.printStackTrace();
}
return encodeBase64Result;
}
/**
* 私鑰解密
* @param sourceBase64RSA
* @param privateKeyBase64Str
*/
public static String decode(String sourceBase64RSA,String privateKeyBase64Str){
byte[] privateBytes = Base64.decodeBase64(privateKeyBase64Str);
byte[] encodeSource = Base64.decodeBase64(sourceBase64RSA);
int encodePartLen = encodeSource.length/ENCODE_PART_SIZE;
List<byte[]> decodeListData = new LinkedList<>();//所有解密數據
String decodeStrResult = null;
//私鑰解密
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(privateBytes);
try {
KeyFactory keyFactory = KeyFactory.getInstance(KEY_RSA_TYPE);
PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
Cipher cipher = Cipher.getInstance(KEY_RSA_TYPE);
cipher.init(Cipher.DECRYPT_MODE,privateKey);
int allDecodeByteLen = 0;//初始化所有被解密數據長度
for (int i = 0; i < encodePartLen; i++) {
byte[] tempEncodedData = new byte[ENCODE_PART_SIZE];
System.arraycopy(encodeSource,i*ENCODE_PART_SIZE,tempEncodedData,0,ENCODE_PART_SIZE);
byte[] decodePartData = cipher.doFinal(tempEncodedData);
decodeListData.add(decodePartData);
allDecodeByteLen += decodePartData.length;
}
byte [] decodeResultBytes = new byte[allDecodeByteLen];
for (int i = 0,curPosition = 0; i < encodePartLen; i++) {
byte[] tempSorceBytes = decodeListData.get(i);
int tempSourceBytesLen = tempSorceBytes.length;
System.arraycopy(tempSorceBytes,0,decodeResultBytes,curPosition,tempSourceBytesLen);
curPosition += tempSourceBytesLen;
}
decodeStrResult = new String(decodeResultBytes,"UTF-8");
}catch (Exception e){
e.printStackTrace();
}
return decodeStrResult;
}
}
//運行結果
//加密后:bapvkf7RsocUqpr7JJ5yMDQSrMAKnWVuHG2buJDhV0DGMTlAKezcy7Qyc8f5DVVHralZ5I0nSZdQOVaIG7ndP/bvtNcQVJaaigRSaQTfmf7xiNNuWQf71nQJmiUlHdzijUWB0HbilWBeZ71FZT9djoV1X6EZDB3oH1DQwwOmvow=
//解密后:Java資料社區
經過以上加密之后,基本上能很大程度解決數據在傳輸當中數據被泄漏的幾率,但是并沒法保證絕對的安全,RSA加密中如果公鑰被泄漏,攻擊者照樣有辦法能夠根據泄漏的公鑰,重新模擬一份加密數據傳輸到服務端,而服務端并沒法辨別中途是否被篡改過,那么簽名技術就是用于驗證、防止中途是否被篡改;
簽名:通過使用兩對RSA秘鑰 (A[公鑰、私鑰],B[公鑰、私鑰]) ,通常客戶端存儲A公鑰、B私鑰,服務端則存儲A私鑰、B公鑰,A用于加解密,B用于簽名以及驗簽;
流程:客戶端用A公鑰加密過后,再通過B私鑰將加密過后的數據進行簽名,最后將加密數據與簽名一起發送到服務端,服務端接收到數據之后,首先通過B公鑰對該加密數據以及簽名進行對比驗簽,如果驗證不一致則代表中途被篡改過,否則正常;
RSA簽名案例:
public class RSASignature {
public static void main(String[] args) {
/*創建兩份公私鑰,map1用于加解密、map2用于簽名*/
Map<String,String> map1=RSAUtils.createRSAKeys();
Map<String,String> map2=RSAUtils.createRSAKeys();
String str="Java資料社區";
String enStr=RSAUtils.encode(str,map1.get("public"));//map1公鑰加密
System.out.println("加密后:"+enStr);
String sign=sign(enStr,map2.get("private"));//map2私鑰簽名
System.out.println("簽名:"+sign);
//enStr+="1"; //模擬中途被篡改,則驗簽false
boolean flag=doCheck(enStr,sign,map2.get("public"));//map2公鑰驗簽
System.out.println("驗簽結果:"+flag);
}
/**
* 簽名算法
*/
public static final String SIGN_ALGORITHMS = "SHA1WithRSA";
/**
* RSA簽名
*/
public static String sign(String content, String privateKey) {
try {
PKCS8EncodedKeySpec priPKCS8 = new PKCS8EncodedKeySpec(Base64.decode(privateKey));
KeyFactory keyf = KeyFactory.getInstance("RSA");
PrivateKey priKey = keyf.generatePrivate(priPKCS8);
java.security.Signature signature = java.security.Signature.getInstance(SIGN_ALGORITHMS);
signature.initSign(priKey);
signature.update(content.getBytes());
byte[] signed = signature.sign();
return Base64.encode(signed);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* RSA驗簽
*/
public static boolean doCheck(String content, String sign, String publicKey) {
try {
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
byte[] encodedKey = Base64.decode(publicKey);
PublicKey pubKey = keyFactory.generatePublic(new X509EncodedKeySpec(encodedKey));
java.security.Signature signature = java.security.Signature
.getInstance(SIGN_ALGORITHMS);
signature.initVerify(pubKey);
signature.update(content.getBytes());
boolean bverify = signature.verify(Base64.decode(sign));
return bverify;
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
}
//運行結果:
//加密后:Nv1kTCukFAziwsCYPZQDQ8WqII1v5DKlTaFlcgkXQACuO01rFuvPAu/PXlmVuHN0i2Xx5B4ZjsKIs2YJHUzIunuqkPchKZM29b52jv7TLPaeZUeQFmC5GKEpEHSTWfUK9T7YF20+kK6Ey0rWRgOBd3ZoVPjvCoNXAlhyEYkBx7Q=
//簽名:fAkREdyQsioXeDi/CLaBQDP+V0K6W8DGXTBYZsH5GS8O30+ZKCfyTwvGNIZ++XIekt0P6xo1T6L7BRdT/it5qNwcqudxoolgf8KkhkSRFCI6LjJ6TYxFfCnvtMv7dxXDkR30E0AR9jyqNCUVE6ljUDsSL7PvUFpqOBDTcd+l2uA=
//驗簽結果:true
總結
1.對稱加密效率性能高,由于秘鑰存在泄漏的可能,并不能保證加密數據不被破解,建議對普通數據進行使用,對于敏感數據(金額、身份證等等)避免使用;
2.非對稱加密效率性能低,分為公私鑰兩把,即使公鑰泄漏,也能保證數據不被破解,建議對敏感數據進行使用;
3.通過非對稱加密+簽名能夠很大程度防止傳輸過程中信息被篡改,但是并不是絕對的安全;
關注微信公眾號"Java資料社區",更多干貨等你學習;