使用RSA演算法對介面引數簽名及驗籤
阿新 • • 發佈:2018-12-16
在不同的伺服器或系統之間通過API介面進行互動時,兩個系統之間必須進行身份的驗證,以滿足安全上的防抵賴和防篡改。
通常情況下為了達到以上所描述的目的,我們首先會想到使用非對稱加密演算法對傳輸的資料進行簽名以驗證傳送方的身份,而RSA加密演算法是目前比較通用的非對稱加密演算法,經常被用於數字簽名及資料加密,且很多程式語言的標準庫中都自帶有RSA演算法的庫,所以實現起來也是相對簡單的。
本文將使用Java標準庫來實現RSA金鑰對的生成及數字簽名和驗籤,金鑰對中的私鑰由請求方系統妥善保管,不能洩漏;而公鑰則交由系統的響應方用於驗證簽名。
RSA使用私鑰對資料簽名,使用公鑰進行驗籤,生成RSA金鑰對的程式碼如下:
package com.example.demo.util; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.NoSuchAlgorithmException; import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; import java.util.Base64; /** * @author 01 * @program demo * @description 生成RSA公/私鑰對 * @create 2018-12-15 21:25 * @since 1.0 **/ public class GeneratorRSAKey { public static void main(String[] args) { jdkRSA(); } public static void jdkRSA() { GeneratorRSAKey generatorKey = new GeneratorRSAKey(); try { // 初始化金鑰,產生公鑰私鑰對 Object[] keyPairArr = generatorKey.initSecretkey(); RSAPublicKey rsaPublicKey = (RSAPublicKey) keyPairArr[0]; RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) keyPairArr[1]; System.out.println("------------------PublicKey------------------"); System.out.println(Base64.getEncoder().encodeToString(rsaPublicKey.getEncoded())); System.out.println("\n------------------PrivateKey------------------"); System.out.println(Base64.getEncoder().encodeToString(rsaPrivateKey.getEncoded())); } catch (Exception e) { e.printStackTrace(); } } /** * 初始化金鑰,生成公鑰私鑰對 * * @return Object[] * @throws NoSuchAlgorithmException NoSuchAlgorithmException */ private Object[] initSecretkey() throws NoSuchAlgorithmException { KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); keyPairGenerator.initialize(512); KeyPair keyPair = keyPairGenerator.generateKeyPair(); RSAPublicKey rsaPublicKey = (RSAPublicKey) keyPair.getPublic(); RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) keyPair.getPrivate(); Object[] keyPairArr = new Object[2]; keyPairArr[0] = rsaPublicKey; keyPairArr[1] = rsaPrivateKey; return keyPairArr; } }
執行如上程式碼,控制檯將輸出一對RSA金鑰對,複製該金鑰對並儲存,後面我們將會用到:
------------------PublicKey------------------ MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAK2qpAANHhF6j5nTcHGhHlJBnt1ZsYV6Nye96s7VORZrmcMn9FbVYzXy6NbwjBKs7I5e/dwGfECP7sD0DE4VfPsCAwEAAQ== ------------------PrivateKey------------------ MIIBUwIBADANBgkqhkiG9w0BAQEFAASCAT0wggE5AgEAAkEAraqkAA0eEXqPmdNwcaEeUkGe3VmxhXo3J73qztU5FmuZwyf0VtVjNfLo1vCMEqzsjl793AZ8QI/uwPQMThV8+wIDAQABAkBRDBbXc0e6DoGf315VmUSmTLuQP8CqMzw0TtybREUNIcpxfi5EDCGhsSvKjsPq7TAoWcMKl+MolXbE0ncJ+3jxAiEA6KYJVB62XXjALk9iDDD4QCs9eqpMVYgQoYs3wxnxHnkCIQC/GQvpjEM79k8h/IY7+BhNW1bI9Mjxfb4B71/UsBuKEwIgfJRcrnT7xrXgg2vy3wBiD0qYU1VaJvsDnN3F8G211lECIEAublTLOg2ahStZ/8+GXMsmYThvFkodPEK0HdB2MVmnAiB9E4ORf2cWfVSmyY5QTlDYfvBHGccol/nU+4WKZFW/2g==
然後我們需要一個可以生成簽名字串及驗證簽名的工具類,這樣可以方便介面的開發,程式碼如下:
package com.example.demo.util;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
/**
* @author 01
* @program demo
* @description RSA簽名工具類
* @create 2018-12-15 21:26
* @since 1.0
**/
public class JdkSignatureUtil {
private final static String RSA = "RSA";
private final static String MD5_WITH_RSA = "MD5withRSA";
/**
* 執行簽名
*
* @param rsaPrivateKey 私鑰
* @param src 引數內容
* @return 簽名後的內容,base64後的字串
* @throws InvalidKeyException InvalidKeyException
* @throws NoSuchAlgorithmException NoSuchAlgorithmException
* @throws InvalidKeySpecException InvalidKeySpecException
* @throws SignatureException SignatureException
*/
public static String executeSignature(String rsaPrivateKey, String src) throws InvalidKeyException,
NoSuchAlgorithmException, InvalidKeySpecException, SignatureException {
// base64解碼私鑰
byte[] decodePrivateKey = Base64.getDecoder().decode(rsaPrivateKey.replace("\r\n", ""));
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(decodePrivateKey);
KeyFactory keyFactory = KeyFactory.getInstance(RSA);
PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
Signature signature = Signature.getInstance(MD5_WITH_RSA);
signature.initSign(privateKey);
signature.update(src.getBytes());
// 生成簽名
byte[] result = signature.sign();
// base64編碼簽名為字串
return Base64.getEncoder().encodeToString(result);
}
/**
* 驗證簽名
*
* @param rsaPublicKey 公鑰
* @param sign 簽名
* @param src 引數內容
* @return 驗證結果
* @throws NoSuchAlgorithmException NoSuchAlgorithmException
* @throws InvalidKeySpecException InvalidKeySpecException
* @throws InvalidKeyException InvalidKeyException
* @throws SignatureException SignatureException
*/
public static boolean verifySignature(String rsaPublicKey, String sign, String src) throws NoSuchAlgorithmException,
InvalidKeySpecException, InvalidKeyException, SignatureException {
// base64解碼公鑰
byte[] decodePublicKey = Base64.getDecoder().decode(rsaPublicKey.replace("\r\n", ""));
X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(decodePublicKey);
KeyFactory keyFactory = KeyFactory.getInstance(RSA);
PublicKey publicKey = keyFactory.generatePublic(x509EncodedKeySpec);
Signature signature = Signature.getInstance(MD5_WITH_RSA);
signature.initVerify(publicKey);
signature.update(src.getBytes());
// base64解碼簽名為位元組陣列
byte[] decodeSign = Base64.getDecoder().decode(sign);
// 驗證簽名
return signature.verify(decodeSign);
}
}
接著我們來基於SpringBoot編寫一個簡單的demo,看看如何實際的使用RSA演算法對介面引數進行簽名及驗籤。傳送方程式碼如下:
package com.example.demo.controller;
import com.example.demo.util.JdkSignatureUtil;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;
import java.security.spec.InvalidKeySpecException;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
/**
* @author zeroJun
* @program demo
* @description 傳送端
* @create 2018-12-16 09:48
* @since 1.0
**/
public class ClientController {
/**
* 私鑰
*/
private final static String PRIVATE_KEY = "MIIBUwIBADANBgkqhkiG9w0BAQEFAASCAT0wggE5AgEAAkEAraqkAA0eEXqPmdNwcaEeUkGe3VmxhXo3J73qztU5FmuZwyf0VtVjNfLo1vCMEqzsjl793AZ8QI/uwPQMThV8+wIDAQABAkBRDBbXc0e6DoGf315VmUSmTLuQP8CqMzw0TtybREUNIcpxfi5EDCGhsSvKjsPq7TAoWcMKl+MolXbE0ncJ+3jxAiEA6KYJVB62XXjALk9iDDD4QCs9eqpMVYgQoYs3wxnxHnkCIQC/GQvpjEM79k8h/IY7+BhNW1bI9Mjxfb4B71/UsBuKEwIgfJRcrnT7xrXgg2vy3wBiD0qYU1VaJvsDnN3F8G211lECIEAublTLOg2ahStZ/8+GXMsmYThvFkodPEK0HdB2MVmnAiB9E4ORf2cWfVSmyY5QTlDYfvBHGccol/nU+4WKZFW/2g==";
public static String sender() throws InvalidKeySpecException, NoSuchAlgorithmException,
InvalidKeyException, SignatureException {
// 請求所需的引數
Map<String, Object> requestParam = new HashMap<>(16);
requestParam.put("userName", "小明");
requestParam.put("phone", "15866552236");
requestParam.put("address", "北京");
requestParam.put("status", 1);
// 將需要簽名的引數內容按引數名的字典順序進行排序,並拼接為字串
StringBuilder sb = new StringBuilder();
requestParam.entrySet().stream().sorted(Comparator.comparing(Map.Entry::getKey)).forEach(entry ->
sb.append(entry.getKey()).append("=").append(entry.getValue()).append("&")
);
String paramStr = sb.toString().substring(0, sb.length() - 1);
// 使用私鑰生成簽名字串
String sign = JdkSignatureUtil.executeSignature(PRIVATE_KEY, paramStr);
// 對簽名字串進行url編碼
String urlEncodeSign = URLEncoder.encode(sign, StandardCharsets.UTF_8);
// 請求引數中需帶上簽名字串
requestParam.put("sign", urlEncodeSign);
// 傳送請求
return postJson("http://localhost:8080/server", requestParam);
}
/**
* 傳送資料型別為json的post請求
*
* @param url
* @param param
* @param <T>
* @return
*/
public static <T> String postJson(String url, T param) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
HttpEntity<T> httpEntity = new HttpEntity<>(param, headers);
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> responseEntity = restTemplate.postForEntity(url, httpEntity, String.class);
return responseEntity.getBody();
}
public static void main(String[] args) {
try {
System.out.println(sender());
} catch (Exception e) {
e.printStackTrace();
}
}
}
接收方程式碼如下:
package com.example.demo.controller;
import com.example.demo.util.JdkSignatureUtil;
import org.springframework.web.bind.annotation.*;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;
import java.security.spec.InvalidKeySpecException;
import java.util.Comparator;
import java.util.Map;
/**
* @author 01
* @program demo
* @description 接收端
* @create 2018-12-16 09:48
* @since 1.0
**/
@RestController
public class ServerController {
/**
* 公鑰
*/
private final static String PUBLIC_KEY = "MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAK2qpAANHhF6j5nTcHGhHlJBnt1ZsYV6Nye96s7VORZrmcMn9FbVYzXy6NbwjBKs7I5e/dwGfECP7sD0DE4VfPsCAwEAAQ==";
@PostMapping(value = "/server")
public String server(@RequestBody Map<String, Object> param) throws InvalidKeySpecException,
NoSuchAlgorithmException, InvalidKeyException, SignatureException {
// 從引數中取出簽名字串並刪除,因為sign不參與字串拼接
String sign = (String) param.remove("sign");
// 對簽名字串進行url解碼
String decodeSign = URLDecoder.decode(sign, StandardCharsets.UTF_8);
// 將簽名的引數內容按引數名的字典順序進行排序,並拼接為字串
StringBuilder sb = new StringBuilder();
param.entrySet().stream().sorted(Comparator.comparing(Map.Entry::getKey)).forEach(entry ->
sb.append(entry.getKey()).append("=").append(entry.getValue()).append("&")
);
String paramStr = sb.toString().substring(0, sb.length() - 1);
// 使用公鑰進行驗籤
boolean result = JdkSignatureUtil.verifySignature(PUBLIC_KEY, decodeSign, paramStr);
if (result) {
return "簽名驗證成功";
}
return "簽名驗證失敗,非法請求";
}
}
編寫完以上程式碼後,啟動SpringBoot專案,然後執行傳送方的程式碼,控制檯輸出結果如下: