Demo - Java
使用 Java 编程语言编写的请求示例。
RsaUtil
RsaUtil 工具类,用于加密解密加签验签
package util;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import java.math.BigInteger;
import java.security.*;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.*;
import java.util.Arrays;
import java.util.Base64;
/**
* @author DaoDao
*/
public class RsaUtil {
private static final String ALGORITHM = "RSA";
private static final String RSA_ECB_PADDING = "RSA/ECB/PKCS1Padding";
private static final String SIGNATURE = "SHA256withRSA";
/**
* RSA算法规定:待加密的字节数不能超过密钥的长度值除以8再减去11。
* 而加密后得到密文的字节数,正好是密钥的长度值除以 8。
*/
private static final int RESERVE_BYTES = 11;
private static final int BIT = 8;
/**
* 生成密钥对
*
* @throws NoSuchAlgorithmException e
*/
public static KeyPair keyPair(int keySize) throws NoSuchAlgorithmException {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(ALGORITHM);
keyPairGenerator.initialize(keySize, new SecureRandom());
return keyPairGenerator.generateKeyPair();
}
public static PublicKey publicKey(String text) throws NoSuchAlgorithmException, InvalidKeySpecException {
byte[] keyBytes = Base64.getDecoder().decode(text);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM);
return keyFactory.generatePublic(keySpec);
}
public static PrivateKey privateKey(String text) throws NoSuchAlgorithmException, InvalidKeySpecException {
byte[] keyBytes = Base64.getDecoder().decode(text);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM);
return keyFactory.generatePrivate(keySpec);
}
public static PublicKey publicKey(PrivateKey privateKey) throws NoSuchAlgorithmException, InvalidKeySpecException {
RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) privateKey;
String algorithm = rsaPrivateKey.getAlgorithm();
KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
RSAPrivateKeySpec privateKeySpec = keyFactory.getKeySpec(privateKey, RSAPrivateKeySpec.class);
RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec(privateKeySpec.getModulus(), BigInteger.valueOf(65537), privateKeySpec.getParams());
return keyFactory.generatePublic(publicKeySpec);
}
public static String encrypt(PublicKey publicKey, String plains) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
RSAPublicKey key = (RSAPublicKey) publicKey;
BigInteger modulus = key.getModulus();
int bitLength = modulus.bitLength();
int block = (bitLength / BIT) - RESERVE_BYTES;
Cipher cipher = Cipher.getInstance(RSA_ECB_PADDING);
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] bytes = cipher(cipher, block, plains.getBytes());
return Base64.getEncoder().encodeToString(bytes);
}
public static String decrypt(PrivateKey privateKey, String ciphers) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
RSAPrivateKey key = (RSAPrivateKey) privateKey;
BigInteger modulus = key.getModulus();
int bitLength = modulus.bitLength();
int block = (bitLength / BIT);
Cipher cipher = Cipher.getInstance(RSA_ECB_PADDING);
cipher.init(Cipher.DECRYPT_MODE, key);
byte[] bytes = cipher(cipher, block, Base64.getDecoder().decode(ciphers));
return new String(bytes);
}
public static String sign(PrivateKey privateKey, String data) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
Signature signature = Signature.getInstance(SIGNATURE);
signature.initSign(privateKey);
signature.update(data.getBytes());
return new String(Base64.getEncoder().encode(signature.sign()));
}
public static boolean verify(PublicKey publicKey, String data, String sign) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
Signature signature = Signature.getInstance(SIGNATURE);
signature.initVerify(publicKey);
signature.update(data.getBytes());
return signature.verify(Base64.getDecoder().decode(sign.getBytes()));
}
private static byte[] cipher(Cipher cipher, int block, byte[] input) throws BadPaddingException, IllegalBlockSizeException {
int length = input.length;
int offset = 0;
byte[] bytes = new byte[0];
byte[] cache;
while (length - offset > 0) {
if (length - offset > block) {
cache = cipher.doFinal(input, offset, block);
offset += block;
} else {
cache = cipher.doFinal(input, offset, length - offset);
offset = length;
}
bytes = Arrays.copyOf(bytes, bytes.length + cache.length);
System.arraycopy(cache, 0, bytes, bytes.length - cache.length, cache.length);
}
return bytes;
}
}
FireHttpTest
以下提供一个 Java 版本的客户端调用的代码示例,其中仅引用了一个 jackson 的 json 库的 外部 lib ,其它均为 jdk 原生 lib
package http;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import util.RsaUtil;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpHeaders;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
/**
* 模拟 agency 向 ice-run 发送请求
*
* @author DaoDao
*/
class FireHttpTest {
/**
* 服务商(agency)的编号
* 此处使用 agency_no = 100000000000000000 作为模拟
* 真实请求场景,请咨询 ice-run 的技术支持获取正式的 服务商编号 agency_no
*/
private static final String AGENCY_NO = "100000000000000000";
private static final String X_CLIENT = "X-Client";
private static final String X_SECURITY = "X-Security";
private static final String X_SIGN = "X-Sign";
private static final String X_TIME = "X-Time";
private static final String X_TRACE = "X-Trace";
/**
* 安全算法 RSA
*/
private static final String SECURITY = "RSA";
/**
* ice-run 的 公钥
*/
private static final String PUBLIC_KEY = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC78QqLKTkVcwvB2Scm4IoImOXmIthVmx4HHr5Im09aol2r2UlMK3jl4lamdxktvjFswWx41hQBD4tyYFHTY+mQJsRK8Zwm7LZmLJDHcgibIuLxEddln+MI+rWwT+SOdOt7lBvd9PcO4nYr/3Nqkyk8L4PrT+GF7dpVk8iE/VpTwQIDAQAB";
/**
* agency 的 私钥
*/
private static final String PRIVATE_KEY = "MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAOyC76Y+/2NqkkIVUtAHkbZkBYI2XXkuqE9o4oIDkbyAY04jC5MLa3oVc3uGG7T/Elodo9dIbIxNh7/ir7GQLR/hY+ookHAewurJ1z4Jn5TQeRk3+nYQcX6a6orfUOMffwcn0Jtat8D+cNAu3uCi+N7Ez/yNpItNORYqTa8AkgzrAgMBAAECgYAS/rsfl3ysb+E6RHsnsQvvYZ4dpJ8iPfCPnCVg+sdoI8mV+3ORBkBGCFYDjDRKd5fyO+IuRqdNJ2bpLtwcfy9YchcV/x40GIuAlre6A6qt4Raq+0l4FJufaqgdmSiLxd5zPX79gdMvBqTbdYzCSDGP5Pf3pTqIS3iKvSixFTQBzQJBAO6k6rE6JE/EHrbRjVtZNitDZ8rouCL0s9nGsgIDBSfgOCClO+HlZKcHq6W9ry8j1gOFeGH2rF1yMslA/ZymRL0CQQD9tk/gk9Xs1Eg1if7SLwvNxUP+z96oEqQTF26tSudPRwOO88fnfE4ZZ4heBt1QI+s7IhG39qecsFW269nvKfbHAkEArXmEgUBalQFjslGyB+1Zyyk8keuJrx9ifbRKQdwgK1R6eICkfxlZiXGx/NFeP041jGnBkLTXpzYUZOexc+YJoQJBANR3r87vnxAU+l+zr62O7oCk+XtT0y/HZJYEYpBHEQyn+MfnSXqG89R8iovLjd0GJ4E+173KlrU2SqHEQ57w8pMCQAEoDjFUXpYzBl8e0M9XsSpGY531mbOl8ASnVp4OSyavaCsn56eEkL9TI5q69KrYUEA4PN3BazMWfll2s/wu6G0=";
/**
* ice-run 的 test 环境的 API 网关 的域名地址
* 正式生产环境的域名地址请查阅 ice-run 的技术文档
*/
private static final String DOMAIN = "https://test-fire-api.ice.run";
/**
* 示例接口地址
* 仅用于测试加密解密加签验签流程,无任何实际业务场景
*/
private static final String DEMO = DOMAIN + "/demo/api/demo";
/**
* 模拟请求 demo 接口
*/
@SuppressWarnings("unchecked")
@Test
void test() {
/*
* 对方公钥
*/
PublicKey publicKey;
/*
* 己方私钥
*/
PrivateKey privateKey;
try {
publicKey = RsaUtil.publicKey(PUBLIC_KEY);
privateKey = RsaUtil.privateKey(PRIVATE_KEY);
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
throw new RuntimeException(e);
}
/*
* 此处使用 ${DEMO} 模拟请求地址
*/
URI uri = URI.create(DEMO);
System.out.println("request uri" + " : " + uri);
/*
* json 序列化和反序列化工具包
* 此处使用 jackson 模拟,在实际场景中,开发者可以自行选择 json 工具包
*/
ObjectMapper objectMapper = new ObjectMapper();
/*
* 请求参数:通常为复杂对象。
* 此处使用 LinkedHashMap 模拟,实际场景可以定义数据模型类
* 注意:不可传 null 或 空字符串 或 单值 ( string, number, boolean )
* 如果当前业务接口不需要传递具体参数,可以传空对象参数 {},json 序列化为 `{"param":{}}`
*/
Map<String, Object> param = new LinkedHashMap<>();
param.put("name", "!");
/*
* 参数明文:参数对象的 json 序列化后的字符串的明文
*/
String paramPlains;
try {
paramPlains = objectMapper.writeValueAsString(param);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
assert paramPlains != null && !paramPlains.isEmpty();
System.out.println("request param plains" + " : " + paramPlains);
/*
* 参数密文:agency 使用 ice-run 公钥 对参数明文加密后得到的密文
*/
String paramCipher;
try {
paramCipher = RsaUtil.encrypt(publicKey, paramPlains);
} catch (Exception e) {
throw new RuntimeException(e);
}
assert paramCipher != null && !paramCipher.isEmpty();
System.out.println("request param cipher" + " : " + paramCipher);
/*
* 请求签名:agency 使用 私钥 对参数密文进行签名
* 当 ice-run 收到请求时,将会使用 agency 公钥 进行验签
*/
String requestSign;
try {
requestSign = RsaUtil.sign(privateKey, paramCipher);
} catch (Exception e) {
throw new RuntimeException(e);
}
assert !requestSign.isEmpty();
System.out.println("request X-Sign" + " : " + requestSign);
/*
* 请求数据 request body
* 此处使用 LinkedHashMap 模拟,实际场景建议定义数据模型类
* body 数据的 键 固定为 param ,值为 json 序列化后的字符串 RSA 加密后的密文
*/
Map<String, Object> request = new LinkedHashMap<>();
request.put("param", paramCipher);
String requestBody;
try {
requestBody = objectMapper.writeValueAsString(request);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
assert requestBody != null && !requestBody.isEmpty();
System.out.println("request body" + " : " + requestBody);
/*
* http 客户端,此处使用 jdk 标准库模拟,实际场景建议自行选择 HTTP 客户端组件包
* 发起 HTTP 请求之前,请确认 ${DOMAIN} 可以正常访问,部分客户端的服务器环境可能需要设置请求代理
*/
HttpResponse<String> httpResponse;
try (HttpClient httpClient = HttpClient.newHttpClient()) {
HttpRequest.BodyPublisher bodyPublisher = HttpRequest.BodyPublishers.ofString(requestBody);
/*
* 发送 http 请求
* 注意如果有大批量请求场景,请控制单位时间内的请求频次
* ice-run 服务网关有请求限流策略
*/
HttpRequest httpRequest = HttpRequest.newBuilder()
.uri(uri)
// 默认使用 json 作为结构化数据的格式
.header("Content-Type", "application/json")
// 固定传值 服务商编号
.header(X_CLIENT, AGENCY_NO)
// 固定传值 RSA
.header(X_SECURITY, SECURITY)
// 请求签名
.header(X_SIGN, requestSign)
// 请求时间,当前世界标准时间的字符串,格式为 RFC-1123 标准,服务器会校验时间差,不允许超过设置的时间间隔(默认为 2 分钟)
.header(X_TIME, ZonedDateTime.now(ZoneOffset.UTC).format(DateTimeFormatter.RFC_1123_DATE_TIME))
// 链路追踪号(防止重放攻击的请求编号):单位时间内(默认为 1 分钟)不重复的字符串,建议使用 uuid 或者时间戳+随机数。限制长度 = 32。正则表达式 = ^[0-9a-f]{32}$
.header(X_TRACE, UUID.randomUUID().toString().replaceAll("-", ""))
// 默认使用 POST 作为请求方法
.POST(bodyPublisher)
.build();
HttpHeaders httpHeaders = httpRequest.headers();
System.out.println("request headers" + " : " + httpHeaders);
httpResponse = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString());
} catch (IOException | InterruptedException e) {
throw new RuntimeException(e);
}
/*
* 响应状态码
*/
assert httpResponse != null;
int statusCode = httpResponse.statusCode();
System.out.println("response status code" + " : " + statusCode);
assert statusCode == 200;
/*
* 响应 header
*/
HttpHeaders responseHeaders = httpResponse.headers();
assert responseHeaders != null && !responseHeaders.map().isEmpty();
System.out.println("response headers" + " : " + responseHeaders);
/*
* 响应 body ,json 格式的字符串
*/
String responseBody = httpResponse.body();
assert responseBody != null && !responseBody.isEmpty();
System.out.println("response body" + " : " + responseBody);
/*
* 响应 header X-Sign
*/
Optional<String> signOptional = responseHeaders.firstValue(X_SIGN);
assert signOptional.isPresent();
String responseSign = signOptional.get();
assert !responseSign.isEmpty();
System.out.println("response " + X_SIGN + " : " + responseSign);
/*
* 响应数据 response body
* 此处使用 LinkedHashMap 模拟,实际场景建议定义数据模型类
* 数据的 键 有 code 和 message 和 data
* 其中 data 为 json 序列化后的字符串 RSA 加密后的密文
*/
Map<String, Object> responseMap;
try {
responseMap = objectMapper.readValue(responseBody, LinkedHashMap.class);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
assert responseMap != null && !responseMap.isEmpty();
/*
* 响应码,默认 000000 表示请求成功
*/
String code = (String) responseMap.get("code");
assert code != null && !code.isEmpty() && "000000".equals(code);
/*
* 响应 数据密文
*/
String dataCipher = (String) responseMap.get("data");
assert dataCipher != null && !dataCipher.isEmpty();
System.out.println("response data cipher" + " : " + dataCipher);
/*
* 响应验签:ice-run 处理响应时,会使用 ice-run 私钥对响应密文进行签名
* agency 收到响应后,需要使用 ice-run 公钥进行验签
*/
boolean responseSignVerify;
try {
responseSignVerify = RsaUtil.verify(publicKey, dataCipher, responseSign);
} catch (Exception e) {
throw new RuntimeException(e);
}
System.out.println("response sign verify : " + responseSignVerify);
assert responseSignVerify;
/*
* 响应 数据明文
*/
String dataPlains;
try {
dataPlains = RsaUtil.decrypt(privateKey, dataCipher);
} catch (Exception e) {
throw new RuntimeException(e);
}
assert !dataPlains.isEmpty();
System.out.println("response data plains" + " : " + dataPlains);
Map<String, Object> data;
try {
data = objectMapper.readValue(dataPlains, LinkedHashMap.class);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
assert data != null;
}
}