跳到主要内容

🦀 Rust

声明

本 SDK 由 AI 生成,仅供参考使用。建议在实际生产环境使用前进行充分测试。

概述

平台 Rust Server SDK 提供了便捷的服务端接入方式,支持签名生成、请求加密、响应验签和解密等核心功能。

功能特性

  • 支持 SHA256withRSA 签名算法
  • 支持 AES 对称加密/解密
  • 灵活的 HTTP 客户端接口设计,支持多种 HTTP 客户端实现
  • 自动处理签名和加密逻辑
  • 完整的响应验签和解密

环境要求

  • Rust 1.70+
  • Cargo

安装

Cargo.toml 依赖

[dependencies]
# HTTP客户端
reqwest = { version = "0.11", features = ["json"] }

# JSON序列化/反序列化
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

# 加密相关
rsa = "0.9"
aes = "0.8"
block-modes = "0.8"
pkcs8 = "0.10"

# Base64编码
base64 = "0.21"

# 随机数生成
rand = "0.8"

# 时间处理
chrono = "0.4"

# 异步运行时
tokio = { version = "1.35", features = ["full"] }

# 日志
log = "0.4"
env_logger = "0.10"

快速开始

1. 创建配置

use kudian_sdk::{KudianClient, ClientConfig};
use kudian_sdk::http::ReqwestClient;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// 创建配置
let config = ClientConfig {
gateway_url: "https://pay.kudianvip.com".to_string(), // 正式环境
// gateway_url: "https://pay.test.kudianvip.com".to_string(), // 测试环境
mch_id: "your_mch_id".to_string(),
app_id: "your_app_id".to_string(),
app_private_key: "your_app_private_key".to_string(), // Base64格式
app_secret_key: "your_secret_key".to_string(), // AES加密密钥
api_public_key: "your_api_public_key".to_string(), // Base64格式
connect_timeout: 30,
read_timeout: 30,
};

// 创建客户端
let http_client = ReqwestClient::new();
let client = KudianClient::new(config, http_client);

Ok(())
}

2. 发起API请求

use serde_json::json;
use std::collections::HashMap;

// 构造业务参数
let mut biz_params = HashMap::new();
biz_params.insert("param1", "value1");
biz_params.insert("param2", "value2");

// 发送请求
let response = client.post("/api/path", &biz_params).await?;

// 解析响应
let api_response: serde_json::Value = serde_json::from_str(&response)?;

if api_response["code"] == 0 {
println!("业务数据: {}", api_response["data"]);
} else {
println!("请求失败: {}", api_response["msg"]);
}

核心 API

KudianClient

主客户端类,提供所有API调用功能。

配置参数

参数类型必填说明
gateway_urlString网关地址,正式环境: https://pay.kudianvip.com,测试环境: https://pay.test.kudianvip.com
mch_idString商户编号
app_idString应用ID
app_private_keyString应用私钥(Base64编码格式),用于请求签名
app_secret_keyStringAES加密密钥,用于请求/响应内容加密
api_public_keyStringAPI平台公钥(Base64编码格式),用于响应验签

主要方法

post(&self, api_path: &str, biz_params: &HashMap) -> Result<String, Box<dyn Error>>

发送POST请求到指定API路径。

参数:

  • api_path: API路径,如 /api/pay/create
  • biz_params: 业务参数HashMap

返回:

  • Result<String, Error>: 自动解密后的业务数据JSON字符串或错误

示例:

let mut params = HashMap::new();
params.insert("order_no".to_string(), "ORDER202401010001".to_string());
params.insert("amount".to_string(), "10000".to_string());
params.insert("subject".to_string(), "测试订单".to_string());

let result = client.post("/api/pay/create", &params).await?;

verify_notify(&self, notify_params: &HashMap) -> Result<bool, Box<dyn Error>>

验证回调通知的签名。

参数:

  • notify_params: 回调参数HashMap

返回:

  • Result<bool, Error>: true-验签成功,false-验签失败或错误

示例:

use actix_web::{web, HttpResponse, post};
use serde_json::Value;

#[post("/notify")]
async fn handle_notify(
body: web::Json<Value>,
client: web::Data<KudianClient>,
) -> HttpResponse {
let mut params = std::collections::HashMap::new();
if let Some(obj) = body.as_object() {
for (k, v) in obj {
if let Some(s) = v.as_str() {
params.insert(k.clone(), s.to_string());
}
}
}

// 验证签名
match client.verify_notify(&params) {
Ok(true) => {},
_ => return HttpResponse::BadRequest().body("FAIL"),
}

// 获取业务数据(已自动解密)
match client.decrypt_notify(&params["result"]) {
Ok(biz_data) => {
println!("回调数据: {}", biz_data);

// 处理业务逻辑...

HttpResponse::Ok().body("SUCCESS")
},
Err(_) => HttpResponse::InternalServerError().body("FAIL"),
}
}

完整代码实现

1. src/config.rs - 配置类

use serde::{Deserialize, Serialize};

/// SDK配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClientConfig {
/// 网关地址
pub gateway_url: String,

/// 商户编号
pub mch_id: String,

/// 应用ID
pub app_id: String,

/// 应用私钥(Base64格式)
pub app_private_key: String,

/// API平台公钥(Base64格式)
pub api_public_key: String,

/// AES加密密钥
pub app_secret_key: String,

/// 连接超时时间(秒),默认30秒
#[serde(default = "default_timeout")]
pub connect_timeout: u64,

/// 读取超时时间(秒),默认30秒
#[serde(default = "default_timeout")]
pub read_timeout: u64,
}

fn default_timeout() -> u64 {
30
}

2. src/crypto.rs - 加密工具类

use aes::Aes256;
use block_modes::block_padding::Pkcs7;
use block_modes::{BlockMode, Ecb};
use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
use rsa::pkcs8::DecodePrivateKey;
use rsa::pkcs8::DecodePublicKey;
use rsa::sha2::Sha256;
use rsa::signature::Signer;
use rsa::signature::Verifier;
use rsa::{PssPublicKey, RsaPrivateKey, RsaPublicKey};
use std::error::Error;

type Aes256Ecb = block_modes::Ecb<aes::Aes256, block_modes::block_padding::Pkcs7>;

/// 加密工具类
pub struct CryptoUtils;

impl CryptoUtils {
/// AES加密
pub fn aes_encrypt(plain_text: &str, key: &str) -> Result<String, Box<dyn Error>> {
// 确保密钥是32字节
let mut key_bytes = key.as_bytes().to_vec();
key_bytes.resize(32, 0u8);

let cipher = Aes256Ecb::new_from_slices(&key_bytes, &[])?;
let mut buf = vec![0u8; plain_text.len() + 16];
buf[..plain_text.len()].copy_from_slice(plain_text.as_bytes());
let ciphertext_len = cipher.encrypt_padded_mut::<Pkcs7>(&mut buf, plain_text.len())?.len();

Ok(BASE64.encode(&buf[..ciphertext_len]))
}

/// AES解密
pub fn aes_decrypt(cipher_text: &str, key: &str) -> Result<String, Box<dyn Error>> {
// 确保密钥是32字节
let mut key_bytes = key.as_bytes().to_vec();
key_bytes.resize(32, 0u8);

let cipher = Aes256Ecb::new_from_slices(&key_bytes, &[])?;
let ciphertext = BASE64.decode(cipher_text)?;
let mut buf = ciphertext.clone();
let plaintext_len = cipher.decrypt_padded_mut::<Pkcs7>(&mut buf)?.len();

Ok(String::from_utf8(buf[..plaintext_len].to_vec())?)
}

/// SHA256withRSA签名
pub fn sign_sha256_with_rsa(message: &[u8], private_key_bytes: &[u8]) -> Result<String, Box<dyn Error>> {
let private_key = RsaPrivateKey::from_pkcs8_der(private_key_bytes)?;

let signing_key = rsa::pkcs1v15::SigningKey::<Sha256>::new(private_key);
let signature = signing_key.sign(message);

Ok(BASE64.encode(&signature))
}

/// SHA256withRSA验签
pub fn verify_sha256_with_rsa(
message: &[u8],
public_key_bytes: &[u8],
sign: &str,
) -> Result<bool, Box<dyn Error>> {
let public_key = RsaPublicKey::from_public_key_der(public_key_bytes)?;

let verify_key = rsa::pkcs1v15::VerifyingKey::<Sha256>::new(public_key);
let signature = BASE64.decode(sign)?;

match verify_key.verify(message, &signature) {
Ok(()) => Ok(true),
Err(_) => Ok(false),
}
}
}

3. src/sign.rs - 签名工具类

use std::collections::HashMap;

/// 签名工具类
pub struct SignUtils;

impl SignUtils {
/// 生成签名字符串
/// 将参数按ASCII码排序后拼接成 key1=value1&key2=value2 格式
pub fn make_sign_string(params: &HashMap<String, String>) -> String {
let mut keys: Vec<String> = params
.iter()
.filter(|(k, v)| *k != "sign" && !v.is_empty())
.map(|(k, _)| k.clone())
.collect();

keys.sort();

let pairs: Vec<String> = keys
.iter()
.map(|k| format!("{}={}", k, params.get(k).unwrap()))
.collect();

pairs.join("&")
}
}

4. src/http_client.rs - HTTP客户端接口

use std::collections::HashMap;
use std::error::Error;

/// HTTP客户端接口
pub trait HttpClient: Send + Sync {
/// 发送POST请求
fn post(
&self,
url: &str,
headers: &HashMap<String, String>,
request_body: &str,
) -> Pin<Box<dyn Future<Output = Result<String, Box<dyn Error>>> + Send>>;
}

5. src/reqwest_client.rs - Reqwest客户端实现

use reqwest::Client;
use std::collections::HashMap;
use std::error::Error;
use std::time::Duration;
use crate::http_client::HttpClient;

/// 基于Reqwest的HTTP客户端实现
pub struct ReqwestClient {
client: Client,
}

impl ReqwestClient {
/// 创建默认客户端
pub fn new() -> Self {
let client = Client::builder()
.timeout(Duration::from_secs(30))
.build()
.unwrap();

Self { client }
}

/// 创建带超时的客户端
pub fn with_timeout(connect_timeout: u64, read_timeout: u64) -> Self {
let client = Client::builder()
.connect_timeout(Duration::from_secs(connect_timeout))
.timeout(Duration::from_secs(read_timeout))
.build()
.unwrap();

Self { client }
}
}

impl Default for ReqwestClient {
fn default() -> Self {
Self::new()
}
}

#[async_trait::async_trait]
impl HttpClient for ReqwestClient {
async fn post(
&self,
url: &str,
headers: &HashMap<String, String>,
request_body: &str,
) -> Result<String, Box<dyn Error>> {
let mut request = self.client.post(url);

for (k, v) in headers {
request = request.header(k, v);
}

let response = request.body(request_body.to_string()).send().await?;

if !response.status().is_success() {
return Err(format!("请求失败: {}", response.status()).into());
}

let body = response.text().await?;
Ok(body)
}
}

6. src/client.rs - 主客户端类

use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
use chrono::Utc;
use log::{debug, error, info};
use rand::Rng;
use serde_json::Value;
use std::collections::HashMap;
use std::error::Error;
use std::future::Future;

use crate::config::ClientConfig;
use crate::crypto::CryptoUtils;
use crate::http_client::HttpClient;
use crate::sign::SignUtils;

/// 酷点支付SDK主客户端
pub struct KudianClient<T: HttpClient> {
config: ClientConfig,
http_client: T,
crypto_utils: CryptoUtils,
sign_utils: SignUtils,
}

impl<T: HttpClient> KudianClient<T> {
/// 创建客户端
pub fn new(config: ClientConfig, http_client: T) -> Self {
Self {
config,
http_client,
crypto_utils: CryptoUtils,
sign_utils: SignUtils,
}
}

/// 发送POST请求
pub async fn post(
&self,
api_path: &str,
biz_params: &HashMap<String, String>,
) -> Result<String, Box<dyn Error>> {
// 1. 构造完整请求URL
let url = format!("{}{}", self.config.gateway_url, api_path);

// 2. 将业务参数转换为JSON并加密
let biz_json = serde_json::to_string(biz_params)?;
let encrypted_content = CryptoUtils::aes_encrypt(&biz_json, &self.config.app_secret_key)?;

// 3. 构造公共参数
let mut public_params = HashMap::new();
public_params.insert("mch_id".to_string(), self.config.mch_id.clone());
public_params.insert("app_id".to_string(), self.config.app_id.clone());
public_params.insert("timestamp".to_string(), Utc::now().timestamp_millis().to_string());
public_params.insert("nonce_str".to_string(), self.generate_nonce_str());
public_params.insert("sign_type".to_string(), "SHA".to_string());
public_params.insert("content".to_string(), encrypted_content);
public_params.insert("version".to_string(), "2.0".to_string());

// 4. 生成签名
let sign_string = SignUtils::make_sign_string(&public_params);
let private_key_bytes = BASE64.decode(&self.config.app_private_key)?;
let sign = CryptoUtils::sign_sha256_with_rsa(sign_string.as_bytes(), &private_key_bytes)?;
public_params.insert("sign".to_string(), sign);

// 5. 发送请求
let request_body = serde_json::to_string(&public_params)?;
info!("请求URL: {}", url);
info!("请求参数: {}", request_body);

let mut headers = HashMap::new();
headers.insert("Content-Type".to_string(), "application/json".to_string());

let response_body = self.http_client.post(&url, &headers, &request_body).await?;
info!("响应结果: {}", response_body);

// 6. 解析响应并验签
let response: Value = serde_json::from_str(&response_body)?;
let code = response["code"].as_i64().unwrap_or(0);

if code == 0 {
// 成功响应,验证签名
let response_sign = response["sign"].as_str().unwrap_or("");
let mut verify_params = HashMap::new();

if let Some(obj) = response.as_object() {
for (k, v) in obj {
// 排除sign字段
if k != "sign" {
if let Some(s) = v.as_str() {
verify_params.insert(k.clone(), s.to_string());
}
}
}
}

let verify_string = SignUtils::make_sign_string(&verify_params);
let public_key_bytes = self.config.api_public_key.as_bytes();
let verify_result = CryptoUtils::verify_sha256_with_rsa(
verify_string.as_bytes(),
public_key_bytes,
response_sign,
)?;

if !verify_result {
return Err("响应签名验证失败".into());
}

// 解密业务数据
let encrypted_result = response["result"].as_str().unwrap_or("");
let decrypted_data = CryptoUtils::aes_decrypt(encrypted_result, &self.config.app_secret_key)?;
info!("解密后的业务数据: {}", decrypted_data);

Ok(decrypted_data)
} else {
// 失败响应
let msg = response["msg"].as_str().unwrap_or("");
Err(format!("请求失败: code={}, msg={}", code, msg).into())
}
}

/// 验证回调通知签名
pub fn verify_notify(&self, notify_params: &HashMap<String, String>) -> Result<bool, Box<dyn Error>> {
let sign = match notify_params.get("sign") {
Some(s) if !s.is_empty() => s,
_ => return Ok(false),
};

let verify_params = notify_params.clone();
let verify_string = SignUtils::make_sign_string(&verify_params);
let public_key_bytes = self.config.api_public_key.as_bytes();

CryptoUtils::verify_sha256_with_rsa(verify_string.as_bytes(), public_key_bytes, sign)
}

/// 解密回调通知的业务数据
pub fn decrypt_notify(&self, encrypted_result: &str) -> Result<String, Box<dyn Error>> {
CryptoUtils::aes_decrypt(encrypted_result, &self.config.app_secret_key)
}

/// 生成随机字符串
fn generate_nonce_str(&self) -> String {
let mut rng = rand::thread_rng();
let nonce: u64 = rng.gen();
format!("{:032x}", nonce)
}
}

7. src/lib.rs - 库入口

pub mod client;
pub mod config;
pub mod crypto;
pub mod http_client;
pub mod reqwest_client;
pub mod sign;

pub use client::KudianClient;
pub use config::ClientConfig;
pub use reqwest_client::ReqwestClient;

可选实现

src/surf_client.rs - Surf 客户端实现

use surf::Client;
use std::collections::HashMap;
use std::error::Error;

/// 基于Surf的HTTP客户端实现
pub struct SurfClient {
client: Client,
}

impl SurfClient {
/// 创建默认客户端
pub fn new() -> Self {
Self {
client: Client::new(),
}
}
}

impl Default for SurfClient {
fn default() -> Self {
Self::new()
}
}

#[async_trait::async_trait]
impl HttpClient for SurfClient {
async fn post(
&self,
url: &str,
headers: &HashMap<String, String>,
request_body: &str,
) -> Result<String, Box<dyn Error>> {
let mut request = self.client.post(url);

for (k, v) in headers {
request = request.header(k, v);
}

let response = request.body_string(request_body.to_string()).await?;

if !response.status().is_success() {
return Err(format!("请求失败: {}", response.status()).into());
}

let body = response.body_string().await?;
Ok(body)
}
}

使用示例

完整示例:创建支付订单

use kudian_sdk::{ClientConfig, KudianClient, ReqwestClient};
use serde_json::Value;
use std::collections::HashMap;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
env_logger::init();

// 1. 创建配置
let config = ClientConfig {
gateway_url: "https://pay.test.kudianvip.com".to_string(),
mch_id: "your_mch_id".to_string(),
app_id: "your_app_id".to_string(),
app_private_key: "MIIEvQIBADANBgkqhkiG9w0BAQE...".to_string(), // Base64格式的私钥
api_public_key: "MIGfMA0GCSqGSIb3DQEBAQUAA...".to_string(), // Base64格式的公钥
app_secret_key: "your_secret_key_32_bytes".to_string(), // 32字节的AES密钥
connect_timeout: 30,
read_timeout: 30,
};

// 2. 创建客户端
let http_client = ReqwestClient::new();
let client = KudianClient::new(config, http_client);

// 3. 构造业务参数
let mut biz_params = HashMap::new();
biz_params.insert("order_no".to_string(), format!("ORDER{}", Utc::now().timestamp_millis()));
biz_params.insert("amount".to_string(), "10000".to_string());
biz_params.insert("currency".to_string(), "CNY".to_string());
biz_params.insert("subject".to_string(), "测试商品".to_string());
biz_params.insert("notify_url".to_string(), "https://your-domain.com/notify".to_string());

// 4. 发送请求
let result = client.post("/api/pay/create", &biz_params).await?;

// 5. 解析响应
let data: Value = serde_json::from_str(&result)?;

println!("支付订单创建成功!");
println!("订单号: {}", data["order_no"]);
println!("支付URL: {}", data["pay_url"]);

Ok(())
}

Actix-web 回调通知处理示例

use actix_web::{web, App, HttpResponse, HttpServer, post};
use kudian_sdk::{ClientConfig, KudianClient, ReqwestClient};
use serde_json::Value;
use std::sync::Arc;

// 创建客户端(全局共享)
async fn create_client() -> Arc<KudianClient<ReqwestClient>> {
let config = ClientConfig {
gateway_url: "https://pay.kudianvip.com".to_string(),
mch_id: "your_mch_id".to_string(),
app_id: "your_app_id".to_string(),
app_private_key: "your_app_private_key".to_string(),
app_secret_key: "your_secret_key".to_string(),
api_public_key: "your_api_public_key".to_string(),
connect_timeout: 30,
read_timeout: 30,
};

let http_client = ReqwestClient::new();
Arc::new(KudianClient::new(config, http_client))
}

#[post("/notify/pay")]
async fn handle_pay_notify(
body: web::Json<Value>,
client: web::Data<Arc<KudianClient<ReqwestClient>>>,
) -> HttpResponse {
let mut params = std::collections::HashMap::new();
if let Some(obj) = body.as_object() {
for (k, v) in obj {
if let Some(s) = v.as_str() {
params.insert(k.clone(), s.to_string());
}
}
}

// 1. 验证签名
match client.verify_notify(&params) {
Ok(true) => {},
_ => {
error!("签名验证失败");
return HttpResponse::BadRequest().body("FAIL");
}
}

// 2. 解密业务数据
match client.decrypt_notify(&params["result"]) {
Ok(biz_data) => {
info!("收到支付回调: {}", biz_data);

// 3. 解析业务数据并处理
// TODO: 根据业务逻辑处理支付结果
// 更新订单状态、发货等

// 4. 返回成功
HttpResponse::Ok().body("SUCCESS")
},
Err(e) => {
error!("解密失败: {}", e);
HttpResponse::InternalServerError().body("FAIL")
}
}
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
env_logger::init();

let client = create_client().await;

HttpServer::new(move || {
App::new()
.app_data(web::Data::new(client.clone()))
.service(handle_pay_notify)
})
.bind("0.0.0.0:8080")?
.run()
.await
}

异常处理

SDK可能返回的错误:

错误类型说明处理建议
配置错误配置参数缺失或无效检查配置参数是否完整和正确
加密错误密钥错误或加密算法错误检查密钥格式和长度
签名错误签名验签失败检查密钥是否正确
JSON错误序列化/反序列化失败检查数据格式是否正确
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格式
openssl pkcs8 -topk8 -inform PEM -in app_private_key.pem -outform PEM -nocrypt -out app_private_key_pkcs8.pem

# 转换为Base64格式
base64 -w 0 app_private_key_pkcs8.pem
base64 -w 0 app_public_key.pem

3. 如何使用自定义HTTP客户端?

SDK支持自定义HTTP客户端实现,只需实现 HttpClient trait:

use kudian_sdk::HttpClient;

// 使用内置的ReqwestClient
let client1 = KudianClient::new(config, ReqwestClient::new());

// 自定义实现
struct CustomClient;

#[async_trait::async_trait]
impl HttpClient for CustomClient {
async fn post(
&self,
url: &str,
headers: &HashMap<String, String>,
request_body: &str,
) -> Result<String, Box<dyn Error>> {
// 自定义HTTP客户端实现
// 例如使用其他HTTP库或添加特殊逻辑
Ok("".to_string())
}
}

let client2 = KudianClient::new(config, CustomClient);

4. 日志配置

SDK使用log crate记录日志,可以这样配置:

use env_logger;

#[tokio::main]
async fn main() {
// 初始化日志
env_logger::Builder::from_default_env()
.filter_level(log::LevelFilter::Debug)
.init();

// SDK会使用log crate记录日志
}

5. Cargo.toml 完整依赖

[package]
name = "kudian-sdk"
version = "0.1.0"
edition = "2021"

[dependencies]
# HTTP客户端
reqwest = { version = "0.11", features = ["json"] }
async-trait = "0.1"

# JSON序列化/反序列化
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

# 加密相关
rsa = "0.9"
aes = "0.8"
block-modes = "0.8"
pkcs8 = "0.10"

# Base64编码
base64 = "0.21"

# 随机数生成
rand = "0.8"

# 时间处理
chrono = "0.4"

# 异步运行时
tokio = { version = "1.35", features = ["full"] }

# 日志
log = "0.4"
env_logger = "0.10"

# Actix-web (可选,用于Web框架)
actix-web = { version = "4.4", optional = true }
预约咨询