使用RSA算法對接口參數簽名及驗籤

在不同的服務器或系統之間通過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項目,然後運行發送方的代碼,控制檯輸出結果如下:
使用RSA算法對接口參數簽名及驗籤

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章