🍵 Java
声明
本 SDK 由 AI 生成,仅供参考使用。建议在实际生产环境使用前进行充分测试。
概述
平台 Java Server SDK 提供了便捷的服务端接入方式,支持签名生成、请求加密、响应验签和解密等核心功能。
功能特性
- 支持 SHA256withRSA 签名算法
- 支持 AES 对称加密/解密
- 灵活的 HTTP 客户端接口设计,支持多种 HTTP 客户端实现
- 自动处理签名和加密逻辑
- 完整的响应验签和解密
环境要求
- Java 8+
- Maven 3.x
快速开始
1. 创建配置
import com.kudian.sdk.KudianClient;
import com.kudian.sdk.model.ClientConfig;
import com.kudian.sdk.http.OkHttpClient;
// 创建配置
ClientConfig config = ClientConfig.builder()
.gatewayUrl("https://pay.kudianvip.com") // 正式环境
// .gatewayUrl("https://pay.test.kudianvip.com") // 测试环境
.mchId("your_mch_id") // 商户编号
.appId("your_app_id") // 应用ID
.appPrivateKey("your_app_private_key") // 应用私钥(Base64格式)
.appSecretKey("your_secret_key") // AES加密密钥
.apiPublicKey("your_api_public_key") // API平台公钥(Base64格式)
.build();
// 创建客户端(使用OkHttp)
KudianClient client = new KudianClient(config, new OkHttpClient());
2. 发起API请求
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.HashMap;
import java.util.Map;
// 构造业务参数
Map<String, Object> bizParams = new HashMap<>();
bizParams.put("param1", "value1");
bizParams.put("param2", "value2");
// 发送请求
String response = client.post("/api/path", bizParams);
// 解析响应
ObjectMapper mapper = new ObjectMapper();
ApiResponse apiResponse = mapper.readValue(response, ApiResponse.class);
if (apiResponse.getCode() == 0) {
// 请求成功,response.getData()已经自动解密
System.out.println("业务数据: " + apiResponse.getData());
} else {
System.out.println("请求失败: " + apiResponse.getMsg());
}
核心 API
KudianClient
主客户端类,提供所有API调用功能。
配置参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| gatewayUrl | String | 是 | 网关地址,正式环境: https://pay.kudianvip.com,测试环境: https://pay.test.kudianvip.com |
| mchId | String | 是 | 商户编号 |
| appId | String | 是 | 应用ID |
| appPrivateKey | String | 是 | 应用私钥(Base64编码格式),用于请求签名 |
| appSecretKey | String | 是 | AES加密密钥,用于请求/响应内容加密 |
| apiPublicKey | String | 是 | API平台公钥(Base64编码格式),用于响应验签 |
主要方法
post(String apiPath, Map<String, Object> bizParams)
发送POST请求到指定API路径。
参数:
apiPath: API路径,如/api/pay/createbizParams: 业务参数Map
返回:
- 自动解密后的业务数据JSON字符串
示例:
Map<String, Object> params = new HashMap<>();
params.put("order_no", "ORDER202401010001");
params.put("amount", 10000);
params.put("subject", "测试订单");
String result = client.post("/api/pay/create", params);
verifyNotify(Map<String, String> notifyParams)
验证回调通知的签名。
参数:
notifyParams: 回调参数Map
返回:
true: 验签成功false: 验签失败
示例:
// 在你的Controller中
@PostMapping("/notify")
public String handleNotify(@RequestBody Map<String, String> params) {
// 验证签名
boolean isValid = client.verifyNotify(params);
if (!isValid) {
return "FAIL";
}
// 获取业务数据(已自动解密)
String bizData = client.decryptNotify(params.get("result"));
System.out.println("回调数据: " + bizData);
// 处理业务逻辑...
return "SUCCESS";
}
完整代码实现
1. ClientConfig.java - 配置类
package com.kudian.sdk.model;
import lombok.Builder;
import lombok.Data;
/**
* SDK配置类
*/
@Data
@Builder
public class ClientConfig {
/**
* 网关地址
*/
private String gatewayUrl;
/**
* 商户编号
*/
private String mchId;
/**
* 应用ID
*/
private String appId;
/**
* 应用私钥(Base64格式)
*/
private String appPrivateKey;
/**
* API平台公钥(Base64格式)
*/
private String apiPublicKey;
/**
* AES加密密钥
*/
private String appSecretKey;
/**
* 连接超时时间(毫秒),默认30秒
*/
@Builder.Default
private int connectTimeout = 30000;
/**
* 读取超时时间(毫秒),默认30秒
*/
@Builder.Default
private int readTimeout = 30000;
}
2. CryptoUtil.java - 加密工具类
package com.kudian.sdk.util;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
/**
* 加密工具类
*/
public class CryptoUtil {
private static final String AES_ALGORITHM = "AES/ECB/PKCS5Padding";
private static final String RSA_SIGNATURE_ALGORITHM = "SHA256withRSA";
/**
* AES加密
*
* @param plainText 明文
* @param key 密钥
* @return Base64编码的密文
* @throws Exception 加密异常
*/
public static String aesEncrypt(String plainText, String key) throws Exception {
// 确保密钥是32字节(AES-256)
byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8);
byte[] keyBytes32 = new byte[32];
System.arraycopy(keyBytes, 0, keyBytes32, 0, Math.min(keyBytes.length, 32));
Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
SecretKeySpec keySpec = new SecretKeySpec(keyBytes32, "AES");
cipher.init(Cipher.ENCRYPT_MODE, keySpec);
byte[] encrypted = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(encrypted);
}
/**
* AES解密
*
* @param cipherText Base64编码的密文
* @param key 密钥
* @return 明文
* @throws Exception 解密异常
*/
public static String aesDecrypt(String cipherText, String key) throws Exception {
// 确保密钥是32字节(AES-256)
byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8);
byte[] keyBytes32 = new byte[32];
System.arraycopy(keyBytes, 0, keyBytes32, 0, Math.min(keyBytes.length, 32));
Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
SecretKeySpec keySpec = new SecretKeySpec(keyBytes32, "AES");
cipher.init(Cipher.DECRYPT_MODE, keySpec);
byte[] decoded = Base64.getDecoder().decode(cipherText);
byte[] decrypted = cipher.doFinal(decoded);
return new String(decrypted, StandardCharsets.UTF_8);
}
/**
* SHA256withRSA签名
*
* @param message 待签名消息
* @param privateKeyBytes 私钥字节数组(Base64解码后)
* @return Base64编码的签名
* @throws Exception 签名异常
*/
public static String signSHA256withRSA(byte[] message, byte[] privateKeyBytes) throws Exception {
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PrivateKey privateKey = keyFactory.generatePrivate(keySpec);
Signature signature = Signature.getInstance(RSA_SIGNATURE_ALGORITHM);
signature.initSign(privateKey);
signature.update(message);
byte[] signBytes = signature.sign();
return Base64.getEncoder().encodeToString(signBytes);
}
/**
* SHA256withRSA验签
*
* @param message 待验签消息
* @param publicKeyBytes 公钥字节数组(Base64解码后)
* @param sign Base64编码的签名
* @return true-验签成功,false-验签失败
* @throws Exception 验签异常
*/
public static boolean verifySHA256withRSA(byte[] message, byte[] publicKeyBytes, String sign) throws Exception {
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicKeyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey publicKey = keyFactory.generatePublic(keySpec);
Signature signature = Signature.getInstance(RSA_SIGNATURE_ALGORITHM);
signature.initVerify(publicKey);
signature.update(message);
byte[] signBytes = Base64.getDecoder().decode(sign);
return signature.verify(signBytes);
}
}
3. SignUtil.java - 签名工具类
package com.kudian.sdk.util;
import org.apache.commons.lang3.StringUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* 签名工具类
*/
public class SignUtil {
/**
* 生成签名字符串
* 将参数按ASCII码排序后拼接成 key1=value1&key2=value2 格式
*
* @param params 参数Map
* @return 待签名字符串
*/
public static String makeSignString(Map<String, String> params) {
List<String> keys = new ArrayList<>();
// 过滤sign字段和空值
for (Map.Entry<String, String> entry : params.entrySet()) {
if ("sign".equals(entry.getKey())) {
continue;
}
if (StringUtils.isNotBlank(entry.getValue())) {
keys.add(entry.getKey());
}
}
// 按ASCII码排序
Collections.sort(keys);
// 拼接字符串
List<String> pairs = new ArrayList<>();
for (String key : keys) {
String value = params.get(key);
pairs.add(key + "=" + value);
}
return String.join("&", pairs);
}
}
4. HttpClient.java - HTTP客户端接口
package com.kudian.sdk.http;
import java.util.Map;
/**
* HTTP客户端接口
* 支持多种HTTP客户端实现
*/
public interface HttpClient {
/**
* 发送POST请求
*
* @param url 请求URL
* @param headers 请求头
* @param requestBody 请求体(JSON字符串)
* @return 响应体(JSON字符串)
* @throws Exception 请求异常
*/
String post(String url, Map<String, String> headers, String requestBody) throws Exception;
}
5. KudianClient.java - 主客户端类
package com.kudian.sdk;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.kudian.sdk.http.HttpClient;
import com.kudian.sdk.model.ClientConfig;
import com.kudian.sdk.util.CryptoUtil;
import com.kudian.sdk.util.SignUtil;
import lombok.extern.slf4j.Slf4j;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* 酷点支付SDK主客户端
*/
@Slf4j
public class KudianClient {
private final ClientConfig config;
private final HttpClient httpClient;
private final ObjectMapper objectMapper;
public KudianClient(ClientConfig config, HttpClient httpClient) {
this.config = config;
this.httpClient = httpClient;
this.objectMapper = new ObjectMapper();
}
/**
* 发送POST请求
*
* @param apiPath API路径
* @param bizParams 业务参数
* @return 解密后的业务数据JSON字符串
* @throws Exception 请求异常
*/
public String post(String apiPath, Map<String, Object> bizParams) throws Exception {
// 1. 构造完整请求URL
String url = config.getGatewayUrl() + apiPath;
// 2. 将业务参数转换为JSON并加密
String bizJson = objectMapper.writeValueAsString(bizParams);
String encryptedContent = CryptoUtil.aesEncrypt(bizJson, config.getAppSecretKey());
// 3. 构造公共参数
Map<String, String> publicParams = new HashMap<>();
publicParams.put("mch_id", config.getMchId());
publicParams.put("app_id", config.getAppId());
publicParams.put("timestamp", String.valueOf(System.currentTimeMillis()));
publicParams.put("nonce_str", generateNonceStr());
publicParams.put("sign_type", "SHA");
publicParams.put("content", encryptedContent);
publicParams.put("version", "2.0");
// 4. 生成签名
String signString = SignUtil.makeSignString(publicParams);
byte[] privateKeyBytes = Base64.getDecoder().decode(config.getAppPrivateKey());
String sign = CryptoUtil.signSHA256withRSA(signString.getBytes(StandardCharsets.UTF_8), privateKeyBytes);
publicParams.put("sign", sign);
// 5. 发送请求
String requestBody = objectMapper.writeValueAsString(publicParams);
log.info("请求URL: {}", url);
log.info("请求参数: {}", requestBody);
Map<String, String> headers = new HashMap<>();
headers.put("Content-Type", "application/json");
String responseBody = httpClient.post(url, headers, requestBody);
log.info("响应结果: {}", responseBody);
// 6. 解析响应并验签
Map<String, Object> response = objectMapper.readValue(responseBody, Map.class);
Integer code = ((Double) response.get("code")).intValue();
if (code == 0) {
// 成功响应,验证签名
String responseSign = (String) response.get("sign");
Map<String, String> verifyParams = new HashMap<>();
for (Map.Entry<String, Object> entry : response.entrySet()) {
// 排除sign字段
if (!"sign".equals(entry.getKey()) && entry.getValue() != null) {
verifyParams.put(entry.getKey(), String.valueOf(entry.getValue()));
}
}
String verifyString = SignUtil.makeSignString(verifyParams);
byte[] publicKeyBytes = Base64.getDecoder().decode(config.getApiPublicKey());
boolean verifyResult = CryptoUtil.verifySHA256withRSA(
verifyString.getBytes(StandardCharsets.UTF_8),
publicKeyBytes,
responseSign
);
if (!verifyResult) {
throw new Exception("响应签名验证失败");
}
// 解密业务数据
String encryptedResult = (String) response.get("result");
String decryptedData = CryptoUtil.aesDecrypt(encryptedResult, config.getAppSecretKey());
log.info("解密后的业务数据: {}", decryptedData);
return decryptedData;
} else {
// 失败响应
String msg = (String) response.get("msg");
throw new Exception("请求失败: code=" + code + ", msg=" + msg);
}
}
/**
* 验证回调通知签名
*
* @param notifyParams 回调参数
* @return true-验签成功,false-验签失败
* @throws Exception 验签异常
*/
public boolean verifyNotify(Map<String, String> notifyParams) throws Exception {
String sign = notifyParams.get("sign");
if (sign == null || sign.isEmpty()) {
return false;
}
Map<String, String> verifyParams = new HashMap<>(notifyParams);
String verifyString = SignUtil.makeSignString(verifyParams);
byte[] publicKeyBytes = Base64.getDecoder().decode(config.getApiPublicKey());
return CryptoUtil.verifySHA256withRSA(
verifyString.getBytes(StandardCharsets.UTF_8),
publicKeyBytes,
sign
);
}
/**
* 解密回调通知的业务数据
*
* @param encryptedResult 加密的业务数据
* @return 解密后的业务数据JSON字符串
* @throws Exception 解密异常
*/
public String decryptNotify(String encryptedResult) throws Exception {
return CryptoUtil.aesDecrypt(encryptedResult, config.getAppSecretKey());
}
/**
* 生成随机字符串
*
* @return 随机字符串
*/
private String generateNonceStr() {
return UUID.randomUUID().toString().replace("-", "").substring(0, 32);
}
}
可选实现
OkHttpClient.java - OkHttp 客户端实现
package com.kudian.sdk.http;
import okhttp3.*;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* 基于OkHttp的HTTP客户端实现
*/
public class OkHttpClient implements HttpClient {
private final OkHttpClient client;
public OkHttpClient(int connectTimeout, int readTimeout) {
this.client = new OkHttpClient.Builder()
.connectTimeout(connectTimeout, TimeUnit.MILLISECONDS)
.readTimeout(readTimeout, TimeUnit.MILLISECONDS)
.build();
}
public OkHttpClient() {
this(30000, 30000); // 默认30秒
}
@Override
public String post(String url, Map<String, String> headers, String requestBody) throws Exception {
Request.Builder requestBuilder = new Request.Builder()
.url(url)
.post(RequestBody.create(requestBody, MediaType.parse("application/json; charset=utf-8")));
// 设置请求头
if (headers != null) {
for (Map.Entry<String, String> entry : headers.entrySet()) {
requestBuilder.addHeader(entry.getKey(), entry.getValue());
}
}
Request request = requestBuilder.build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new Exception("请求失败: " + response.code());
}
return response.body().string();
}
}
}
使用示例
完整示例:创建支付订单
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.kudian.sdk.KudianClient;
import com.kudian.sdk.http.OkHttpClient;
import com.kudian.sdk.model.ClientConfig;
import java.util.HashMap;
import java.util.Map;
public class PayOrderExample {
public static void main(String[] args) {
// 1. 创建配置
ClientConfig config = ClientConfig.builder()
.gatewayUrl("https://pay.test.kudianvip.com")
.mchId("your_mch_id")
.appId("your_app_id")
.appPrivateKey("MIIEvQIBADANBgkqhkiG9w0BAQE...") // Base64格式的私钥
.apiPublicKey("MIGfMA0GCSqGSIb3DQEBAQUAA...") // Base64格式的公钥
.appSecretKey("your_secret_key_32_bytes") // 32字节的AES密钥
.build();
// 2. 创建客户端
KudianClient client = new KudianClient(config, new OkHttpClient());
try {
// 3. 构造业务参数
Map<String, Object> bizParams = new HashMap<>();
bizParams.put("order_no", "ORDER" + System.currentTimeMillis());
bizParams.put("amount", 10000);
bizParams.put("currency", "CNY");
bizParams.put("subject", "测试商品");
bizParams.put("notify_url", "https://your-domain.com/notify");
// 4. 发送请求
String result = client.post("/api/pay/create", bizParams);
// 5. 解析响应
ObjectMapper mapper = new ObjectMapper();
Map<String, Object> data = mapper.readValue(result, new TypeReference<Map<String, Object>>(){});
System.out.println("支付订单创建成功!");
System.out.println("订单号: " + data.get("order_no"));
System.out.println("支付URL: " + data.get("pay_url"));
} catch (Exception e) {
e.printStackTrace();
System.err.println("订单创建失败: " + e.getMessage());
}
}
}
回调通知处理示例
import org.springframework.web.bind.annotation.*;
import com.kudian.sdk.KudianClient;
import com.kudian.sdk.http.OkHttpClient;
import com.kudian.sdk.model.ClientConfig;
import java.util.Map;
@RestController
@RequestMapping("/notify")
public class PayNotifyController {
private final KudianClient client;
public PayNotifyController() {
ClientConfig config = ClientConfig.builder()
.mchId("your_mch_id")
.appId("your_app_id")
.appPrivateKey("your_app_private_key")
.appSecretKey("your_secret_key")
.apiPublicKey("your_api_public_key")
.build();
this.client = new KudianClient(config, new OkHttpClient());
}
@PostMapping("/pay")
public String handlePayNotify(@RequestBody Map<String, String> params) {
try {
// 1. 验证签名
boolean isValid = client.verifyNotify(params);
if (!isValid) {
System.err.println("签名验证失败");
return "FAIL";
}
// 2. 解密业务数据
String bizData = client.decryptNotify(params.get("result"));
System.out.println("收到支付回调: " + bizData);
// 3. 解析业务数据并处理
// TODO: 根据业务逻辑处理支付结果
// 更新订单状态、发货等
// 4. 返回成功
return "SUCCESS";
} catch (Exception e) {
e.printStackTrace();
return "FAIL";
}
}
}
异常处理
SDK可能抛出的异常:
| 异常类型 | 说明 | 处理建议 |
|---|---|---|
| IllegalArgumentException | 配置参数错误 | 检查配置参数是否完整和正确 |
| Exception | 签名验签失败 | 检查密钥是否正确 |
| Exception | 加解密失败 | 检查appSecretKey是否正确 |
| Exception | HTTP请求失败 | 检查网络连接和网关地址 |
常见问题
1. 如何获取密钥?
- app_private_key: 商户自行生成RSA密钥对,将公钥上传到商户后台
- api_public_key: 从商户后台获取平台公钥
- secret_key: 从商户后台获取或设置
2. 如何生成RSA密钥对?
可以使用OpenSSL命令生成:
# 生成私钥
openssl genrsa -out app_private_key.pem 2048
# 提取公钥
openssl rsa -in app_private_key.pem -pubout -out app_public_key.pem
# 转换为PKCS8格式(Java需要)
openssl pkcs8 -topk8 -inform PEM -in app_private_key.pem -outform PEM -nocrypt -out app_private_key_pkcs8.pem
# 转换为Base64格式
cat app_private_key_pkcs8.pem | base64 -w 0
cat app_public_key.pem | base64 -w 0
3. 如何使用自定义HTTP客户端?
SDK支持自定义HTTP客户端实现,只需实现 HttpClient 接口:
// 使用内置的OkHttpClient
KudianClient client1 = new KudianClient(config, new OkHttpClient());
// 自定义实现
HttpClient customClient = new HttpClient() {
@Override
public String post(String url, Map<String, String> headers, String requestBody) throws Exception {
// 自定义HTTP客户端实现
// 例如使用其他HTTP库或添加特殊逻辑
}
};
KudianClient client2 = new KudianClient(config, customClient);
4. 日志配置
SDK使用Slf4j记录日志,可以在配置文件中设置日志级别:
<!-- log4j2.xml -->
<Logger name="com.kudian.sdk" level="DEBUG"/>
5. Maven 依赖
<dependencies>
<!-- HTTP客户端(OkHttp) -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>
<!-- JSON处理(Jackson) -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
</dependency>
<!-- Apache Commons(工具类) -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
</dependencies>