实例-第三方接口签名加密


title: 实例-第三方接口签名加密
date: 2019-08-06 19:32:21
categories:

  • 实战
  • 安全
    tags:
  • 安全
    top:

文章目录

      • 总述
      • 签名算法
        • 签名规则
        • 签名示例
      • 业务数据加/解密算法
        • 加密规则
        • 解密规则
      • 代码示例

总述

最近和第三方接口做对接,接口涉及到了签名和加密,因此在这里总结一下。签名和加密涉及对称加密、非对称加密、SSL等概念,可以在其他文章中参考。这里只做具体项目中实践的示例。

总体思路是用固定的key进行SHA1摘要签名,用对称加密的secret对数据进行aes加密。由于签名key和对称加密的secret是双方预先沟通好写死在代码里的,不像SSL需要在网络中传输,因此可以保证安全。

具体如下:

数据报文结构:

{
	"appid":"1a6dfe4b3a7740d1a37fa1615a3f6bd9",
	"nonce":"YSBuLCgvmdSdB3oS",
	"timestamp":1552900602924,
	"signature":"296a25e1d61b10d359b2893befc3a417d89b4708",
    "data":"sdfjasIOISADFLASlsdaf12142rsakdfsafjsawKJL34343"
}

发送过程:

  • 先加密:将String的data、秘钥、初始化向量转换为字节码,完成aes对称加密,再转换为Base64字符串。
  • 再签名:将**“所有数据” + “签名key”**根据一定规则(比如根据askall码排序)拼装成字符串,然后接用SHA1或者MD5进行摘要用作签名就OK了。由于有timestamp和nonce,因此可以防止签名key被破解。

接收过程:

  • 先验签:使用和签名同样的方式生成签名,然后对比数据的签名进行验签即可。
  • 再解密:将Base64字符串的data转换为字节码,再进行aes解密,再转换为String即可(Json字符串)。

由于appid在报文里是明文的,因此很容易根据appid获得验签和解密所需的签名key,以及秘钥和初始化向量。

下面进行具体解释。

签名算法

签名规则

签名规则如下:

  1. 将appid、timestamp、nonce、data及key不为null的字段值按ASCII码从小到大排序(字典序);
  2. 将排序后的字段值拼接成字符串;
  3. 对拼接后的字符串计算SHA1摘要,作为签名。

其中签名key由平台提供。

签名示例

签名key:Odgs!^36M$aAXrpp

请求体字段
appid “1a6dfe4b3a7740d1a37fa1615a3f6bd9”
nonce “YSBuLCgvmdSdB3oS”
timestamp 1552900602924
data null
  1. 将appid、timestamp、nonce、key及data不为null的字段值按ASCII码从小到大排序(字典序),因为data为null不加入签名计算,结果如下:

    请求体字段值
    1552900602924
    1a6dfe4b3a7740d1a37fa1615a3f6bd9
    Odgs!^36M$aAXrpp
    YSBuLCgvmdSdB3oS
  2. 将排序后的字段值拼接成字符串,结果如下:

    15529006029241a6dfe4b3a7740d1a37fa1615a3f6bd9Odgs!^36M$aAXrppYSBuLCgvmdSdB3oS
    
  3. 对拼接后的字符串计算SHA1摘要,签名如下:

    296a25e1d61b10d359b2893befc3a417d89b4708
    

签名后的请求体:

{
	"appid":"1a6dfe4b3a7740d1a37fa1615a3f6bd9",
	"nonce":"YSBuLCgvmdSdB3oS",
	"timestamp":1552900602924,
	"signature":"296a25e1d61b10d359b2893befc3a417d89b4708"
}

业务数据加/解密算法

加密规则

  1. 将业务数据序列化为JSON格式字符串;
  2. 通过AES加密算法和密钥、初始化向量对JSON格式字符串进行加密;
  3. 将加密结果进行Base64编码。

解密规则

  1. 将加密业务数据进行Base64解码;
  2. 通过AES解密算法和密钥、初始化向量进行解密;
  3. 获得JSON格式的业务数据。

其中加*/解密的密钥和初始化向量由平台提供,字符串均采用UTF-8*编码格式

代码示例

项目实际使用的签名、加密/解密代码如下:

import java.security.MessageDigest;
import java.security.Security;
import java.util.Arrays;
import java.util.Base64;
import java.util.stream.Collectors;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import org.bouncycastle.jce.provider.BouncyCastleProvider;

import lombok.SneakyThrows;

public class SecurityUtil {
	public static final String CHARSET = "UTF-8";

	private static final char[] HEX_CHARS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e',
			'f' };

	static {
		Security.addProvider(new BouncyCastleProvider());
	}

	private static String hex(byte[] bytes) {
		int len = bytes.length;
		StringBuilder buf = new StringBuilder(len * 2);
		for (int j = 0; j < len; j++) {
			buf.append(HEX_CHARS[(bytes[j] >> 4) & 0x0f]);
			buf.append(HEX_CHARS[bytes[j] & 0x0f]);
		}
		return buf.toString();
	}

	@SneakyThrows
	private static byte[] sha1(byte[] input) {
		MessageDigest messageDigest = MessageDigest.getInstance("SHA1");
		messageDigest.update(input);
		return messageDigest.digest();
	}

	@SneakyThrows
	private static String encodeBase64(byte[] data) {
		return new String(Base64.getEncoder().encode(data), CHARSET);
	}

	@SneakyThrows
	private static byte[] decodeBase64(String data) {
		return Base64.getDecoder().decode(data.getBytes(CHARSET));
	}

	@SneakyThrows
	private static byte[] aes(byte[] data, byte[] key, byte[] iv, boolean encrypt) {
		Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
		cipher.init(encrypt ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"),
				new IvParameterSpec(iv));
		return cipher.doFinal(data);
	}

	/**
	 * 生成签名
	 * 
	 * @param fieldValues 参与签名的字段
	 * @return 签名
	 */
	@SneakyThrows
	public static String generateSign(Object... fieldValues) {
		String joinedStr = Arrays.asList(fieldValues).stream()
				.map(fieldValue -> fieldValue != null ? fieldValue.toString() : "").sorted()
				.collect(Collectors.joining(""));
		return hex(sha1(joinedStr.getBytes(CHARSET)));
	}

	/**
	 * 加密业务数据
	 * 
	 * @param rawData 业务数据
	 * @param key     密钥
	 * @param iv      初始化向量
	 * @return 加密业务数据
	 */
	@SneakyThrows
	public static String encryptData(String rawData, String key, String iv) {
		return encodeBase64(aes(rawData.getBytes(CHARSET), decodeBase64(key), decodeBase64(iv), true));
	}

	/**
	 * 解密业务数据
	 * 
	 * @param encryptedData 加密业务数据
	 * @param key           密钥
	 * @param iv            初始化向量
	 * @return 业务数据
	 */
	@SneakyThrows
	public static String decryptData(String encryptedData, String key, String iv) {
		return new String(aes(decodeBase64(encryptedData), decodeBase64(key), decodeBase64(iv), false), CHARSET);
	}

	// 测试Demo:签名
	public static void testGenerateSign() {
		String appid = "fs8342f8542c13e9ba0400342d015716";
		long timestamp = 1554189997590l;
		String nonce = "YDBuFQgbmladR3oK";
		String data = "{\"count\":1,\"productId\":\"c3e8fd65544f11e9ba0400155d015705\"}";
		String key = "4jSuQA0HNraqPjfq";

		System.out.println("[签名测试]");
		System.out.println("appid:" + appid);
		System.out.println("timestamp:" + timestamp);
		System.out.println("nonce:" + nonce);
		System.out.println("data:" + data);
		System.out.println("key:" + key);
		String sign = generateSign(appid, timestamp, nonce, data, key);
		System.out.println("签名:" + sign);
		System.out.println("----------------");
	}

	// 测试Demo:加密解密
	public static void testEncryptData() {
		String data = "{\"key\":\"value\"}";
		String key = "D3QE5sMzHlNCpxQ5+GMOzK==";
		String iv = "Qb6HMJIDlico8/9lWyKpLf==";

		System.out.println("[加解密测试]");
		System.out.println("原始数据:" + data);
		String encryptedData = encryptData(data, key, iv);
		System.out.println("加密数据:" + encryptedData);
		System.out.println("解密数据:" + decryptData(encryptedData, key, iv));
		System.out.println("----------------");
	}

	@SneakyThrows
	public static void main(String[] args) {
		testGenerateSign();
		testEncryptData();
	}
}

你可能感兴趣的:(实战)