概述
最近專案中需要對第三方開發介面呼叫,考慮了一下,準備採用MD5+RSA算對請求資料進行簽名,來達到請求鑑權,過濾非法請求的目標。
數字簽名採用MD5+RSA演算法實現。RSA私鑰要嚴格保密並提供安全儲存介質,數字簽名使用java.security.Signature 包中規定的“MD5withRSA”演算法實現。私鑰簽名,公鑰驗籤即介面呼叫方儲存私鑰並用私鑰對請求資料進行簽名,平臺方儲存呼叫方提供的公鑰,對於呼叫方的簽名進行驗籤,驗籤通過才會接收呼叫方請求的資料。
簡易流程
1、從平臺獲取32位businessId,備用
2、本地生成keyPair,其中privateKey自行儲存,需要將publicKey提供給平臺
3、signature欄位為businessId + signature結果
4、簽名資料根據以介面限定為準
KeyPair生成、簽名及驗籤
keyPair生成
private static final String KEY_ALGORITHM = "RSA";
private static final String SIGNATURE_ALGORITHM = "MD5withRSA";
private static final String CHARSET = "UTF-8";
private ThreadLocal<String> publicKey = new ThreadLocal<>();
private ThreadLocal<String> privateKey = new ThreadLocal<>();
public CustomKeyPair generateKeyPair() throws NoSuchAlgorithmException {
KeyPairGenerator keygen = java.security.KeyPairGenerator
.getInstance(KEY_ALGORITHM);
SecureRandom secureRandom = new SecureRandom();
secureRandom.setSeed("remote".getBytes()); // 初始化隨機產生器
keygen.initialize(1024);
KeyPair keys = keygen.genKeyPair();
RSAPublicKey publicKey = (RSAPublicKey) keys.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keys.getPrivate();
CustomKeyPair customKeyPair = CustomKeyPair.builder()
.privateKey(Base64Utils.encodeToString(privateKey.getEncoded()))
.publicKey(Base64Utils.encodeToString(publicKey.getEncoded()))
.build();
log.info("privateKey:{}", customKeyPair.getPrivateKey());
log.info("publicKey:{}", customKeyPair.getPublicKey());
return customKeyPair;
}
@Data
@Builder
private static class CustomKeyPair {
private String privateKey;
private String publicKey;
}
簽名呼叫示例
SignatureTool.I.putPrivateKey(customKeyPair.privateKey).signature("測試簽名資料")
簽名程式碼實現:
public String signature(String data) {
try {
return signature(data.getBytes(CHARSET));
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
throw new RuntimeException("加密演算法不存在");
} catch (SignatureException e) {
e.printStackTrace();
throw new RuntimeException("資料簽名不存在");
} catch (InvalidKeyException | InvalidKeySpecException e) {
e.printStackTrace();
throw new RuntimeException("數字簽名key異常");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
throw new RuntimeException("不支援的字元編碼");
}
}
/**
* 數字簽名演算法
*
* @param data 簽名資料
* @return 簽名結果
* @throws NoSuchAlgorithmException 沒有此種加密演算法異常
* @throws SignatureException 簽名異常
* @throws InvalidKeyException 不可用的私鑰
*/
private String signature(byte[] data) throws NoSuchAlgorithmException, SignatureException, InvalidKeyException, InvalidKeySpecException {
byte[] keyBytes = Base64Utils.decodeFromString(this.privateKey.get());
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);
signature.initSign(privateKey);
signature.update(data);
return Base64Utils.encodeToString(Base64Utils.encodeToString(signature.sign()).getBytes());
}
驗籤呼叫示例
SignatureTool.I.putPublicKey(customKeyPair.publicKey).verifySignature("測試簽名資料", signature);
驗籤程式碼實現:
public boolean verifySignature(String data, String signature) {
try {
return verifySignature(data.getBytes(CHARSET), signature);
} catch (NoSuchAlgorithmException e) {
log.warn("加密演算法不存在");
} catch (SignatureException e) {
log.warn("資料簽名不存在");
} catch (InvalidKeyException | InvalidKeySpecException e) {
log.warn("數字簽名key異常");
} catch (UnsupportedEncodingException e) {
log.warn("不支援的字元編碼");
}
return false;
}
/**
* 數字簽名驗證
*
* @param data 驗籤資料
* @param sign 簽名
* @return 驗簽結果
* @throws NoSuchAlgorithmException 沒有此種加密演算法異常
* @throws SignatureException 簽名異常
* @throws InvalidKeyException 不可用的私鑰
*/
private boolean verifySignature(byte[] data, String sign) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException, InvalidKeySpecException {
byte[] signs = Base64Utils.decode(Base64Utils.decodeFromString(sign));
byte[] publicKey = Base64Utils.decodeFromString(this.publicKey.get());
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicKey);
KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
PublicKey pubKey = keyFactory.generatePublic(keySpec);
Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);
signature.initVerify(pubKey);
signature.update(data);
return signature.verify(signs);
}
呼叫示例
簽名的資料可以根據實際的業務介面不同,單獨設定,雙方統一標準
public static void main(String[] args) throws NoSuchAlgorithmException {
//生成祕鑰對
CustomKeyPair customKeyPair = SignatureTool.I.generateKeyPair();
//私鑰簽名
String signature = SignatureTool.I.putPrivateKey(customKeyPair.privateKey).signature("測試簽名資料");
//公鑰驗籤
SignatureTool.I.putPublicKey(customKeyPair.publicKey).verifySignature("測試簽名資料", signature);
}
這樣,我們只要給每個呼叫方分配一個businessId,由呼叫提供公鑰並與對映到businessId,私鑰自始至終一直由呼叫方儲存,我們可以直接判定資料的提交來自哪個業務方,介面呼叫的安全性可以得到有效的保證,後續可以通過增加諸如ip白名單之類的限制,進一步加強介面的安全性,尤其在多方呼叫的場景下,防扯皮效果顯著,另外被呼叫方也可以為業務方生成單獨的keypair,實現雙向驗籤的雙保險機制。
SignatureTool 完整程式碼如下:
import lombok.Builder;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.Base64Utils;
import java.io.UnsupportedEncodingException;
import java.security.*;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Locale;
@Slf4j
public enum SignatureTool {
I;
private static final String KEY_ALGORITHM = "RSA";
private static final String SIGNATURE_ALGORITHM = "MD5withRSA";
private static final String CHARSET = "UTF-8";
private ThreadLocal<String> publicKey = new ThreadLocal<>();
private ThreadLocal<String> privateKey = new ThreadLocal<>();
/**
* 數字簽名
*
* @param data 簽名內容
* @return 簽名
*/
public String signature(String data) {
try {
return signature(data.getBytes(CHARSET));
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
throw new RuntimeException("加密演算法不存在");
} catch (SignatureException e) {
e.printStackTrace();
throw new RuntimeException("資料簽名不存在");
} catch (InvalidKeyException | InvalidKeySpecException e) {
e.printStackTrace();
throw new RuntimeException("數字簽名key異常");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
throw new RuntimeException("不支援的字元編碼");
}
}
public boolean verifySignature(String data, String signature) {
try {
return verifySignature(data.getBytes(CHARSET), signature);
} catch (NoSuchAlgorithmException e) {
log.warn("加密演算法不存在");
} catch (SignatureException e) {
log.warn("資料簽名不存在");
} catch (InvalidKeyException | InvalidKeySpecException e) {
log.warn("數字簽名key異常");
} catch (UnsupportedEncodingException e) {
log.warn("不支援的字元編碼");
}
return false;
}
/**
* 初始化公鑰、私鑰
*/
public SignatureTool initKeys(String privateKey, String publicKey) {
this.publicKey.set(publicKey);
this.privateKey.set(privateKey);
return this;
}
public SignatureTool putPublicKey(String publicKey) {
this.publicKey.set(publicKey);
return this;
}
public SignatureTool putPrivateKey(String privateKey) {
this.privateKey.set(privateKey);
return this;
}
public CustomKeyPair generateKeyPair() throws NoSuchAlgorithmException {
KeyPairGenerator keygen = KeyPairGenerator
.getInstance(KEY_ALGORITHM);
SecureRandom secureRandom = new SecureRandom();
secureRandom.setSeed("ainoteRemote".getBytes()); // 初始化隨機產生器
keygen.initialize(1024);
KeyPair keys = keygen.genKeyPair();
RSAPublicKey publicKey = (RSAPublicKey) keys.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keys.getPrivate();
CustomKeyPair customKeyPair = CustomKeyPair.builder()
.privateKey(Base64Utils.encodeToString(privateKey.getEncoded()))
.publicKey(Base64Utils.encodeToString(publicKey.getEncoded()))
.build();
log.info("privateKey:{}", customKeyPair.getPrivateKey());
log.info("publicKey:{}", customKeyPair.getPublicKey());
return customKeyPair;
}
/**
* 數字簽名演算法
*
* @param data 簽名資料
* @return 簽名結果
* @throws NoSuchAlgorithmException 沒有此種加密演算法異常
* @throws SignatureException 簽名異常
* @throws InvalidKeyException 不可用的私鑰
*/
private String signature(byte[] data) throws NoSuchAlgorithmException, SignatureException, InvalidKeyException, InvalidKeySpecException {
byte[] keyBytes = Base64Utils.decodeFromString(this.privateKey.get());
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);
signature.initSign(privateKey);
signature.update(data);
return Base64Utils.encodeToString(Base64Utils.encodeToString(signature.sign()).getBytes());
}
/**
* 數字簽名驗證
*
* @param data 驗籤資料
* @param sign 簽名
* @return 驗簽結果
* @throws NoSuchAlgorithmException 沒有此種加密演算法異常
* @throws SignatureException 簽名異常
* @throws InvalidKeyException 不可用的私鑰
*/
private boolean verifySignature(byte[] data, String sign) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException, InvalidKeySpecException {
byte[] signs = Base64Utils.decode(Base64Utils.decodeFromString(sign));
byte[] publicKey = Base64Utils.decodeFromString(this.publicKey.get());
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicKey);
KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
PublicKey pubKey = keyFactory.generatePublic(keySpec);
Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);
signature.initVerify(pubKey);
signature.update(data);
return signature.verify(signs);
}
@Data
@Builder
private static class CustomKeyPair {
private String privateKey;
private String publicKey;
}
public static void main(String[] args) throws NoSuchAlgorithmException {
System.out.println(CommonUtil.newUUID().toUpperCase(Locale.ROOT));
CustomKeyPair customKeyPair = SignatureTool.I.generateKeyPair();
Long timeStamp = System.currentTimeMillis();
String signature = SignatureTool.I.putPrivateKey(customKeyPair.privateKey).signature(timeStamp+"");
System.out.println(timeStamp);
log.debug(signature);
SignatureTool.I.putPublicKey(customKeyPair.publicKey).verifySignature(timeStamp+"", signature);
}
}