在不同的服务器或系统之间通过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算法对接口参数签名及验签