基于MD5+RSA算法实现接口调用防扯皮级鉴权概述简易流

概述

最近项目中需要对第三方开发接口调用,考虑了一下,准备采用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);
    }
}


复制代码