最近项目中需要对第三方开发接口调用,考虑了一下,准备采用MD5+RSA算对请求数据进行签名,来达到请求鉴权,过滤非法请求的目标。
数字签名采用MD5+RSA算法实现。RSA私钥要严格保密并提供安全存储介质,数字签名使用java.security.Signature 包中规定的“MD5withRSA”算法实现。私钥签名,公钥验签即接口调用方存储私钥并用私钥对请求数据进行签名,平台方存储调用方提供的公钥,对于调用方的签名进行验签,验签通过才会接收调用方请求的数据。
1、从平台获取32位businessId,备用
2、本地生成keyPair,其中privateKey自行保存,需要将publicKey提供给平台
3、signature字段为businessId + signature结果
4、签名数据根据以接口限定为准
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); } }