1.Http接口安全概述:
1.1、Http接口是互聯網各系統之間對接的重要方式之一,使用http接口,開發和調用都很方便,也是被大量採用的方式,它可以讓不同系統之間實現數據的交換和共享,但由於http接口開放在互聯網上,那麼我們就需要有一定的安全措施來保證不能是隨隨便便就可以調用;
1.2、目前國內互聯網公司主要採用兩種做法實現接口的安全:
一種是以支付寶等支付公司為代表的私鑰公鑰簽名驗證機制;
一種是大量互聯網企業都常採用的參數簽名驗證機制;
-
Http接口安全演進:
2.1.完全開放的接口(完全開放) 2.2.接口參數簽名(基本安全) 2.3.接口參數簽名+時效性驗證(更加安全) 2.4.接口參數私鑰簽名公鑰驗籤(固若金湯) 2.5.接口參數簽名+Https(金鐘罩) 2.6.口參數私鑰簽名公鑰驗籤+Https(金鐘罩) 總之:安全是相對的,只有相對的安全,沒有絕對的安全!
3.Http接口安全設計及應用
3.1 接口參數私鑰簽名公鑰驗籤
先來看看私鑰+公鑰的安全模式,這是一種更為安全的方式,它通過私鑰和公鑰實現接口的安全,目前互聯網中主要是以支付寶
為代表的公司採用這種機制;(有很多開放平臺也是採用這種機制) . 具體業務流所示:
該簽名是通過4個祕鑰來實現的,分別是:
客戶端應用私鑰 , 客戶端公鑰 , 服務端應用私鑰 , 服務端公鑰.
私鑰都是用來生成簽名的,公鑰都是用來解密的,客戶端的公鑰解密客戶端的私鑰生成的簽名,服務端的公鑰解密服務端的私鑰生
成的簽名,相信這樣解釋應該會比較好理解的.
好了,下面就來看看是如何具體操作的,我的具體操作步驟:
首先,4把密鑰都是通過OpenSSL工具生成,你需要先獲得這個工具:官方網站:https://www.openssl.org/
介紹一下這個工具吧,OpenSSL 是一個開源的安全套接字層密碼庫,囊括主要的密碼算法、常用的密鑰和證書封裝管理功能及SSL協議,並提供豐富的應用程序測試或其它目的使用;OpenSSL整個軟件包大概可以分成三個主要的功能部分:SSL協議庫、應用程序以及密碼算法庫;
3.1.1確保Linux已經安裝openssl :
使用命令檢查Linux是否已經安裝openssl:yum list installed | grep openssl
如果沒有安裝,則執行命令進行安裝:yum install openssl openssl-devel -y
3.1.2創建祕鑰生成的目的地(文件夾):
ps : 我是在根目錄中的soft文件夾下創建的
mkdir server , mkdir client 先創建這兩個文件,然後Linux會默認將生成的祕鑰放入其中.
3.1.3 生成祕鑰
進入server文件,輸入openssl 進入Openssl命令行;
使用openssl生成私鑰,執行如下命令:
genrsa -out rsa_private_key.pem 2048
注意一點Java開發者需要將私鑰轉換成PKCS8格式,其他語言不用這一步操作,執行如下命令:
pkcs8 -topk8 -inform PEM -in rsa_private_key.pem -outform PEM -nocrypt -out rsa_private_key_pkcs8.pem
使用openssl生成公鑰,執行如下命令:
rsa -in rsa_private_key.pem -pubout -out rsa_public_key.pem
退出openssl命令行:exit
經過以上步驟,我們可以在當前目錄中(server)看到三個文件:
rsa_private_key.pem(RSA私鑰)
rsa_private_key_pkcs8.pem(pkcs8格式RSA私鑰)(我們java要使用的私鑰是這一個)
rsa_public_key.pem(對應RSA公鑰)
client端的祕鑰與server端的祕鑰生成一樣,這裡就不敘述了,想體驗的朋友自己按照上一步操作再做一遍即可.
3.2 Demo
好了,現在就可以使用生成的祕鑰來做一個簡單的Demo來檢驗一下了.
首先這裡提供一個簽名處理工具類:
說明:該工具類依賴的包是java.security,該包JDK8才有,所以,如果使用下面這個Demo的話,建議檢查自己的JDK是不是JDK8,如果是JDK8以下的版本,以下的Demo是用不了的.解決方法就是,使用第三方提供的依賴包,效果是相同的,依賴包如下:
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>RELEASE</version>
</dependency>
package com.kinglong.http.utils;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.Signature;
import java.security.interfaces.RSAPrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
/**
* 簽名處理工具類
*
* @author haojinlong
*
*/
public class MyRSAUtils {
public static final String CHARSET = "utf-8";
/**
* RSA私鑰簽名
*
* @param src 客戶端傳過來的原始參數
* @param priKey 我們的客戶端私鑰
* @return
* @throws Exception
*/
public static String sign (String src, String priKey) {
try {
KeyFactory fac = KeyFactory.getInstance("RSA");
byte[] pribyte = Base64.getDecoder().decode(priKey);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(pribyte);
RSAPrivateKey privateKey = (RSAPrivateKey) fac.generatePrivate(keySpec);
Signature sigEng = Signature.getInstance("SHA1withRSA");
sigEng.initSign(privateKey);
sigEng.update(src.getBytes(MyRSAUtils.CHARSET));
byte[] signature = sigEng.sign();
return Base64.getEncoder().encodeToString(signature);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* RSA公鑰驗證簽名
*
* @param src 客戶端穿過來的原始數據
* @param sign 簽名
* @param publicKey 我們的客戶端公鑰
* @return
*/
public static boolean signVerify (String sign, String src, String publicKey) {
try {
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
//將公鑰變為一個字節數組
byte[] encodedKey = Base64.getDecoder().decode(publicKey);
//使用祕鑰工廠生成一個公鑰對象pubKey
PublicKey pubKey = keyFactory.generatePublic(new X509EncodedKeySpec(encodedKey));
//使用"SHA1WithRSA"算法,生成簽名對象signature
Signature signature = Signature.getInstance("SHA1WithRSA");
signature.initVerify(pubKey);
signature.update(src.getBytes(MyRSAUtils.CHARSET));
boolean bverify = signature.verify(Base64.getDecoder().decode(sign));
return bverify;
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
}
package com.kinglong.http.HttpUtils;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
/**
*生成簽名需要的輔助工具類
*/
public class SignUtils {
/**
*將傳進來的無序參數,轉換為有序的字符串輸出
*/
public static String generateSortSign(Map<String,Object>paramMap){
Map<String,Object> treeMap = new TreeMap<String, Object>(paramMap);
Set<Map.Entry<String,Object>> entrySet = treeMap.entrySet();
Iterator<Map.Entry<String,Object>> iterator = entrySet.iterator();
StringBuffer stringBuffer = new StringBuffer();
while (iterator.hasNext()){
Map.Entry<String,Object> entry=iterator.next();
String keys = entry.getKey();
String value = (String) entry.getValue();
stringBuffer.append(keys).append(value);
}
return stringBuffer.toString();
}
}
package com.kinglong.http.controller;
import com.alibaba.fastjson.JSONObject;
import com.kinglong.http.HttpUtils.HttpClientUtils;
import com.kinglong.http.HttpUtils.SignUtils;
import com.kinglong.http.constans.Constans;
import com.kinglong.http.utils.MyRSAUtils;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class HttpClient {
public static void main(String[] args) {
demo();
}
public static void demo(){
String url ="http://localhost:8080/api/verifydemo";
//這裡僅做演示使用,所以隨手寫了幾個信息
String realName = "我是驗證信息";
String phone = "17000000000";
String idCard = "6403021992120511111";
String bankCard = "5555555555555555555555";
//封裝參數
Map<String,Object> paramMap = new ConcurrentHashMap<String, Object>();
paramMap.put("realName",realName);
paramMap.put("phone",phone);
paramMap.put("idCard",idCard);
paramMap.put("bankCard",bankCard);
//生成客戶端簽名,注意看,客戶端這裡生成簽名的時候是使用的客戶端私鑰Constans.CLIENT_PRIVATE_KEY
String sign = MyRSAUtils.sign(SignUtils.generateSortSign(paramMap),Constans.CLIENT_PRIVATE_KEY);
paramMap.put("sign",sign);
//發送請求時注意:因為肯定會不可避免的要輸出中文字符,所以記得在使用HttpClient通信的時候,設置編碼格式為utf-8
//否則,我想你的簽名驗證成功的概率基本等於讓兩條平行線相交成功的概率
String json = HttpClientUtils.doPostByEncode(url,paramMap,"utf-8");
//解析json字符串
JSONObject jsonObject = JSONObject.parseObject(json);
String code = jsonObject.getString("code");
String erroMessage = jsonObject.getString("erroMessage");
JSONObject object = jsonObject.getJSONObject("object");
String result = object.getString("result");
String ret_sign = object.getString("ret_sign");//服務端發回的簽名
String resultDesc = object.getString("resultDesc");
//封裝參數準備進行驗證該返回信息是否是由服務端發回的
//驗證時的參數可以根據自己公司的規範去選擇,我這裡就選了這兩個參數,因為偷懶沒有生成新的map,所以這裡需要clear一下.
paramMap.clear();
paramMap.put("result",result);
paramMap.put("resultDesc",resultDesc);
//調用簽名驗證工具類進行簽名驗證
//重點!!!敲黑板啦!!注意看,這裡使用的是服務端的公鑰來解密服務端發送的簽名的,很多人肯定使用了客戶端公鑰來解密,必然是失敗的
//同理,在服務端要是用客戶端的公鑰解密客戶端發送的簽名,這個前面的流程圖我覺得已經說得很明確了
boolean isTrue = MyRSAUtils.signVerify(ret_sign,SignUtils.generateSortSign(paramMap),Constans.SERVER_PUBLIC_KEY);
if (isTrue){
System.out.println("返回正確信息,可以進行下一步操作");
}else {
System.out.println("返回錯誤信息,不可以進行下一步操作");
}
}
}
package com.kinglong.http.constans;
public class Constans {
//客戶端私鑰
public static final String CLIENT_PRIVATE_KEY="MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQClEAp0NDHtb9w5iJfyNOh6DeCRv0RjGFA1CIQ6ZxpfIc65h03hUGsDjcZtWQQZf7d30hiVCcQLylJYJidHQbPcDWULRhObgUxFIFQ37UW8c8DHDiHPNVRjH47ePi9sZIYbVuaWOJkS9NAoSDTRDA1vS7ewWosA9WyUSEuWSEm6eQQV02nlhf/cIsu3biDmq8Y9ffg9lLgEYbH4VQu6bRXgqpy90OxFh3Jh16nbAZXqAJMKCZyJYo5B9ZN4No8Q/EMZe98DNFybOod7WTuQmrS5FM0vCsjQpczBwn+dny5grWl4YgUYgAWOnvPr2iMXVXLqbQjcVEMFKwr/71K9yVehAgMBAAECggEBAJi6nvGm2gu41SznFrEmA3XsIT66m6yVcqGfn7nqbJxZy84fRBCXOG2xYUkMdJ6jbj+QRu6geqXuLwMhSnbEdIfIXRZxYPMiUFAl+cdF5KDa+iU1DlOMJOkS6j75iyfgW7YwUmvtMrY3j+O17CkB3ex9QxoKrVPVwwHxYv9LI+1FT0Fd3157gIbkBTdXnKUc4O4Z4/FTcvPYNR2h9O79xQbcx0clUbj61yQxkxVyN25plxxAVoUW7mKNleH6nFAkeb/gYxLdwlUHm53cYowSDtbzo4udB6qPWj/PvCVgu7UatnEhcyX9ZKCBmrX3+EvWTw9A9dTDSMu4D9CX2QsUq4ECgYEA2pTWhw6ulzWlORu12WcUxDzCVuKV7dGXCQ2MRnGELLm7HzAmRIjd0LubGWAKwUNa6NhWVNsupI8/hDh3hyjGAiTVn0gowDVQj4s/vIx5aYMsg6nxzUl3KrjBJyORHnY8+fFp5V1SHr0G6jOTynsqX0ONMpNZ/zyxiOQG5vGs7A8CgYEAwVHJAe0Eqz2jwLHA10Afsh3vS2Otcjj1cj9jxz1ZFoYIbD64Mc77bem+9x7xpI4w6/WezUOO4gZ8RaAh7PpyMWbTwzq0QHS7bSzKMfpRALyVf7HyIS/QuTUA0oLZXx9vk4wAe0rZBNFuuEtyU3XgHz8OSY/uHeTXJWYtJHTIkU8CgYAD6YgRcMTVNgOYCxPtKTgo7wF3dqTCVe8DHXf2Rs/b0RM1UrJMpbp6ovD6ukpW/TKiWkTpTeb+0QWNA0m4ZJVusmQUbsEz94BSoWZppIYDynJAhQkr6HW2kQn7/ln5lpouyxBfJ5VxsWZvSK8Lf7rZa6caUaLZu6dd0N8CwS6cJwKBgD74B9RTwtiQXF1wyNKUNX7MF1zkG+P/v5s2IKcOSY13nRi9GTxIIke8ApL2BlnGYxMIz3Am2EyxNhtrvIE3VqjWyJVn8ryoCUDXfQjocygdRUjxyl+a9o7NP/ZR3sIIOEzEJogCakwSd9EZ6iRbWeRzopC9jB86ogWxkXS1gXsrAoGAEOvVsv8lpY+TCs3Q78pnedwFIXXkrOknrXq2gb+WHmSSDCELOaqWsX73rdbW4IcmJ6kT2d5bn6hH+/2Zw/+Xpy6maBeLxxscXBfLHwT/85YG5z21LuyikB7ht/tZCNxQOjEfvo5MdCwNN6GhpPSVwNjLAvtiImJYnlDuGPaG+RU=";
//服務端公鑰
public static final String SERVER_PUBLIC_KEY="MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqlG1v814kyQIEpLQnyxo/4RUku8PE+csGzH073XoW8xPdXAH8C7Y3isyDKSClTEGeS/SaqioYPFI+YlD0Eag1DEoVPkDcJeWsPv4FZr2ZO2wCenwqUrH5BM7ZapSBLenkgdhTF3SIUJMrTfJPaRJM1wb75SPawHO1zueM0cvcbeGNJOyCG69XPhour0Cei/HcflY3bXVx9kKH8vvAmbosAOrIwwdGSnV1YqSlApBmoobGYcaFPv5Clntghq+6P1ut2RSOrKinr0Q4wc4kIpnSY+j2oQ20OZ7rZXuLyGmMSsglvGwgTIoxHqkXpWP3qGFmYHSQgGCMFWxlT1t3e1AawIDAQAB";
}
說明:服務端的簽名生成類,簽名生成的輔助類都與客戶端相同,所以,這裡就不寫了.
服務端的公共常量類:
package com.kinglong.http.constans;
public class Constans {
//服務端私鑰
public static final String SERVER_PRIVATE_KEY="MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCqUbW/zXiTJAgSktCfLGj/hFSS7w8T5ywbMfTvdehbzE91cAfwLtjeKzIMpIKVMQZ5L9JqqKhg8Uj5iUPQRqDUMShU+QNwl5aw+/gVmvZk7bAJ6fCpSsfkEztlqlIEt6eSB2FMXdIhQkytN8k9pEkzXBvvlI9rAc7XO54zRy9xt4Y0k7IIbr1c+Gi6vQJ6L8dx+VjdtdXH2Qofy+8CZuiwA6sjDB0ZKdXVipKUCkGaihsZhxoU+/kKWe2CGr7o/W63ZFI6sqKevRDjBziQimdJj6PahDbQ5nutle4vIaYxKyCW8bCBMijEeqRelY/eoYWZgdJCAYIwVbGVPW3d7UBrAgMBAAECggEBAJtfkxf4T4iblCmteVfb4aVHiQfJwc18VEYy2qkgvOoRhmMx4mv/sKNscGoMIXwMj0U6lQ/r8D8PnmzWBeEYrVslxQ9PYw3xm+y0z+qVxTTpiHBi08L8j0HHMaZbLBtVly6mQOKzrB/fJafXfmQXXRfXbTywH+2UZqb+oiFRTTzEnFMyku5HquA27Mp+K4KNFTVaKiCSadwz+XyFOf1cmUn4oRlYnhgbMKgn2JSyQLfJ5SSgYhnxat1Qbg1HDhOzo6L/NQwCkTzo3B52X56EQXZkk2p1pVRZiwaP/3FCHEenOv9jy+ZffdUFECRCv0Aw2isWqZ4zuVrgWCI801W/AVECgYEA0gwkMSHWlJaGtWhYifgWcOSr/v9l/SAiD0VN57j4THWIAq30WtOVE/ta4XhbSasxcHJIqNckq2inEm2b3jwcS7YKzlfKGl9xw8uL3yVM1YXtKwGrJr1xk8pcWOVBmFaAd8BaOHwsMkE8EhgiwZSctTbmRNeiOz6lIOj2aDu6fE0CgYEAz5SPKeNay00LIGk4Os+Zev1JPhwW4tfwTJxXV97TjvgYoRwUwV6XNjiXoxQD5iOXieK7Cy0GDMzJWxXpmEpI5BVv2X6GMkkH5iXz4rGtzsMNWZ5lF3d2PpRLvRIrSi3btevTohN7UkogyjEPhuEnV47Emsev36lB+bcJwgLBK5cCgYAuEaemdwt/T3yAMUCqEhWp8R2gMhgGapPN0Z+CoVkkO+r223xqp1ldJpYKOcGb6MZRKV+yWG2cgrmSGyRCm+CA4o6AL1UOb7yd+vjUmnO9qUAZXKZTOt28Unfqr22xoddPbIrdNK7k3tX0CgMlfhjYzg+3LaxRXi4Nh8rzlZYTSQKBgBahgq41bEun3aOt9QRsZ7ZB8P9FfrVCh59CmD8rOvNmVwERl62xS1kM+HM+FmK71KSixHOmd/djSDyW+f2xc5ryP1x979GBpsvPrXQ0nNdi6oyvuSPC0XBnKI63cWLH9yExUcRkzVgeXs7MZH33BBwGo6agSKtgv6Gi8/xj4n2HAoGBALD55ZzDDrB17ZOQSuEnn45R3FA62BGipCM8HFdZySeEOf9IPoOxy2xmzQtMc2VESRQeviw92E5gxNxrRgIHSQvPJryaHOjoMKI9+kDBejZvBEHWi23E4tobPUOUH/jBJJQ7Ue5aRB6IS/jlMzmVhan8vefnEXRDT5EnSfxV3Pa6";
//客戶端公鑰
public static final String CLIENT_PUBLIC_KEY="MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApRAKdDQx7W/cOYiX8jToeg3gkb9EYxhQNQiEOmcaXyHOuYdN4VBrA43GbVkEGX+3d9IYlQnEC8pSWCYnR0Gz3A1lC0YTm4FMRSBUN+1FvHPAxw4hzzVUYx+O3j4vbGSGG1bmljiZEvTQKEg00QwNb0u3sFqLAPVslEhLlkhJunkEFdNp5YX/3CLLt24g5qvGPX34PZS4BGGx+FULum0V4KqcvdDsRYdyYdep2wGV6gCTCgmciWKOQfWTeDaPEPxDGXvfAzRcmzqHe1k7kJq0uRTNLwrI0KXMwcJ/nZ8uYK1peGIFGIAFjp7z69ojF1Vy6m0I3FRDBSsK/+9SvclXoQIDAQAB";
}
package com.kinglong.http.controller;
import com.kinglong.http.constans.Constans;
import com.kinglong.http.rto.ResponseObject;
import com.kinglong.http.utils.MyRSAUtils;
import com.kinglong.http.utils.SignUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Controller
public class HttpController {
@RequestMapping("/api/verifydemo")
@ResponseBody
public Object demo(@RequestParam(value = "realName")String realName,
@RequestParam(value = "phone")String phone,
@RequestParam(value = "idCard")String idCard,
@RequestParam(value = "bankCard")String bankCard,
@RequestParam(value = "sign")String sign){
ResponseObject responseObject = new ResponseObject();
Map<String,Object> map = new ConcurrentHashMap<String,Object>();
//簽名參數驗證
if (StringUtils.isEmpty(realName)){
responseObject.setErroMessage("真實姓名不能為空");
responseObject.setCode("0000");
}else if (StringUtils.isEmpty(phone)){
responseObject.setErroMessage("手機號不能為空");
responseObject.setCode("0000");
}else if (StringUtils.isEmpty(idCard)){
responseObject.setErroMessage("身份證不能為空");
responseObject.setCode("0000");
}else if (StringUtils.isEmpty(bankCard)){
responseObject.setErroMessage("銀行卡不能為空");
responseObject.setCode("0000");
}else if (StringUtils.isEmpty(sign)){
responseObject.setErroMessage("簽名不能為空");
responseObject.setCode("0000");
}else {
//封裝參數進行驗證
map.put("realName",realName);
map.put("phone",phone);
map.put("idCard",idCard);
map.put("bankCard",bankCard);
//注意看,這裡使用的就是客戶端的公鑰進行簽名的解密
boolean isTrue = MyRSAUtils.signVerify(sign,SignUtils.generateSortSign(map),Constans.CLIENT_PUBLIC_KEY);
if (!isTrue){
responseObject.setErroMessage("簽名有誤,請核查後再次嘗試");
responseObject.setCode("0000");
}
}
//0000是失敗,1111是成功
String code = responseObject.getCode();
if(code!=null && code.equals("0000")){
map.clear();
map.put("result","0000");
map.put("resultDesc",responseObject.getErroMessage());
//使用服務端私鑰生成返回的簽名
String ret_sign = MyRSAUtils.sign(SignUtils.generateSortSign(map),Constans.SERVER_PRIVATE_KEY);
map.put("ret_sign",ret_sign);
responseObject.setObject(map);
}else {
map.clear();
map.put("result","ok");
map.put("resultDesc","驗證通過");
//使用服務端私鑰生成返回的簽名
String ret_sign = MyRSAUtils.sign(SignUtils.generateSortSign(map),Constans.SERVER_PRIVATE_KEY);
map.put("ret_sign",ret_sign);
responseObject.setObject(map);
responseObject.setCode("1111");
responseObject.setErroMessage("驗證成功");
}
return responseObject;
}
}