diff --git a/rust/Cargo.toml b/rust/Cargo.toml new file mode 100644 index 0000000..e052943 --- /dev/null +++ b/rust/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "api-signer" +version = "0.1.0" +edition = "2021" +authors = ["Sound Force "] +description = "API签名工具,提供请求签名与验证功能" + +[dependencies] +md-5 = "0.10.5" +sha1 = "0.10.5" +sha2 = "0.10.6" +hmac = "0.12.1" +hex = "0.4.3" +url = "2.3.1" +once_cell = "1.17.1" +chrono = "0.4.23" +thiserror = "2.0.12" +regex = "1.7.3" +dotenv = "0.15.0" +clap = { version = "4.1.11", features = ["derive"] } \ No newline at end of file diff --git a/rust/README.md b/rust/README.md new file mode 100644 index 0000000..3148b4c --- /dev/null +++ b/rust/README.md @@ -0,0 +1,231 @@ +# API签名工具 - Rust实现 + +## 项目结构 + +根据实际项目文件结构: + +```plaintext +rust/ +├── Cargo.toml # Cargo配置文件 +├── Cargo.lock # 依赖锁定文件 +├── src/ +│ ├── main.rs # 命令行工具入口 +│ └── lib.rs # 库入口和核心实现 +└── target/ # 构建输出目录 +``` + +## 使用方法 + +### 构建项目 + +```bash +cargo build --release +``` + +### 运行命令行工具 + +```bash +cargo run --release -- [选项] +``` + +或使用编译后的二进制文件: + +```bash +./target/release/apisign [选项] +``` + +### 命令行选项 + +| 选项 | 描述 | +|------|------| +| `-a, --algorithm` | 签名算法: md5, sha1, sha256, hmac-sha256 | +| `-u, --url` | API基础URL | +| `-p, --param` | 请求参数,格式为key=value,可多次使用 | +| `-k, --key` | 访问密钥ID | +| `-s, --secret` | 密钥 | +| `-c, --channel` | 合作渠道方ID | +| `-h, --help` | 显示帮助信息 | +| `-m, --mode` | 操作模式: sign_url, sign_params, verify | +| `-t, --timestamp` | 自定义时间戳 | +| `-n, --nonce` | 自定义随机字符串 | +| `--version` | 显示版本信息 | + +### 常用命令示例 + +**基本用法** + +```bash +./target/release/apisign +``` + +**自定义参数** + +```bash +./target/release/apisign \ + -u "https://api.example.com/user/info" \ + -p "userId=12345" -p "action=getInfo" \ + -k "YOUR_ACCESS_KEY" \ + -s "YOUR_SECRET_KEY" \ + -c "3" +``` + +**指定签名算法** + +```bash +./target/release/apisign -a sha256 +``` + +**生成签名参数** + +```bash +./target/release/apisign -m sign_params -p "userId=12345" -p "action=getData" +``` + +**验证签名** + +```bash +./target/release/apisign -m verify -p "userId=12345" -p "action=getData" -p "AccessKeyId=test-key" -p "channelId=test-channel" -p "timestamp=1621234567890" -p "nonce=abc123" -p "sign=calculated-signature-here" +``` + +**帮助信息** + +```bash +./target/release/apisign --help +``` + +### API接口测试实例 + +使用真实API接口进行测试: + +```bash +# 未签名的API调用测试 - 返回错误 +curl "https://api-v1.sound-force.com:8443/p/album/single/media-url?channelId=3&singleId=381980" +# 返回: {"code":400,"data":null,"msg":"Missing AccessKeyId","success":false} + +# 生成访问https://api-v1.sound-force.com:8443/p/album/single/media-url的签名URL +./target/release/apisign \ + -a md5 \ + -u "https://api-v1.sound-force.com:8443/p/album/single/media-url" \ + -p "singleId=381980" \ + -k "YOUR_ACCESS_KEY" \ + -s "YOUR_SECRET_KEY" \ + -c "3" + +# 使用curl测试API接口 +signed_url=$(./target/release/apisign \ + -a md5 \ + -u "https://api-v1.sound-force.com:8443/p/album/single/media-url" \ + -p "singleId=381980" \ + -k "YOUR_ACCESS_KEY" \ + -s "YOUR_SECRET_KEY" \ + -c "3" | grep -A 1 "签名后的URL:" | tail -n 1) +curl -v "$signed_url" +``` + +请注意: + +- 替换`YOUR_ACCESS_KEY`为实际的访问密钥ID +- 替换`YOUR_SECRET_KEY`为实际的密钥 +- 示例使用的渠道ID为`3`,请根据实际情况调整 + +使用有效的密钥和签名后,API接口将返回成功响应(状态码200)并提供媒体URL数据。 + +### Rust Reqwest客户端测试示例 + +```rust +use std::process::Command; +use reqwest; +use std::io::{self, Write}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // 获取签名URL + let output = Command::new("./target/release/apisign") + .args(&[ + "-a", "md5", + "-u", "https://api-v1.sound-force.com:8443/p/album/single/media-url", + "-p", "singleId=381980", + "-k", "YOUR_ACCESS_KEY", + "-s", "YOUR_SECRET_KEY", + "-c", "3" + ]) + .output()?; + + let output_str = String::from_utf8(output.stdout)?; + let lines: Vec<&str> = output_str.lines().collect(); + + // 提取URL + let mut signed_url = ""; + for (i, line) in lines.iter().enumerate() { + if line.contains("签名后的URL:") && i + 1 < lines.len() { + signed_url = lines[i + 1].trim(); + break; + } + } + + println!("签名生成的URL: {}", signed_url); + + if !signed_url.is_empty() { + // 发送请求 + let client = reqwest::Client::new(); + let response = client.get(signed_url).send().await?; + + println!("状态码: {}", response.status()); + let body = response.text().await?; + println!("响应内容: {}", body); + } else { + println!("无法从输出中获取签名URL"); + } + + Ok(()) +} +``` + +### 代码集成 + +```rust +use apisign::{ApiSigner, SignOptions, SignatureAlgorithm}; +use std::collections::HashMap; + +fn main() { + // 创建签名选项 + let options = SignOptions::new() + .with_algorithm(SignatureAlgorithm::Md5); + + // 创建签名工具 + let signer = ApiSigner::new(options); + + // 准备请求参数 + let mut params = HashMap::new(); + params.insert("singleId".to_string(), "381980".to_string()); + + // 执行签名 + let signed_params = signer.sign_request( + ¶ms, + "YOUR_ACCESS_KEY", + "YOUR_SECRET_KEY", + "3" + ).unwrap(); + + // 或签名URL + let signed_url = signer.sign_url( + "https://api-v1.sound-force.com:8443/p/album/single/media-url", + ¶ms, + "YOUR_ACCESS_KEY", + "YOUR_SECRET_KEY", + "3" + ).unwrap(); + + println!("{}", signed_url); +} +``` + +### 环境变量 + +该工具支持从`.env`文件加载以下配置: + +- `ACCESS_KEY_ID`: 访问密钥ID +- `SECRET_KEY`: 密钥 +- `CHANNEL_ID`: 渠道ID +- `SIGN_ALGORITHM`: 签名算法 +- `API_BASE_URL`: API基础URL diff --git a/rust/src/lib.rs b/rust/src/lib.rs new file mode 100644 index 0000000..ce2a248 --- /dev/null +++ b/rust/src/lib.rs @@ -0,0 +1,373 @@ +use hmac::{Hmac, Mac}; +use md5::{Digest, Md5}; +use regex::Regex; +use sha1::Sha1; +use sha2::Sha256; +use std::{ + collections::HashMap, + fmt, + time::{SystemTime, UNIX_EPOCH}, +}; +use thiserror::Error; +use url::form_urlencoded; + +type HmacSha256 = Hmac; + +/// API签名错误类型 +#[derive(Error, Debug)] +pub enum SignError { + #[error("无效的签名算法: {0}")] + InvalidAlgorithm(String), + + #[error("缺少必要参数: {0}")] + MissingParameter(String), + + #[error("参数解析失败: {0}")] + ParseError(String), + + #[error("时间戳过期")] + TimestampExpired, + + #[error("签名验证失败")] + SignatureVerificationFailed, + + #[error("HMAC错误: {0}")] + HmacError(String), +} + +/// 签名算法类型 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SignatureAlgorithm { + /// MD5算法(默认、最快) + Md5, + /// SHA1算法 + Sha1, + /// SHA256算法 + Sha256, + /// HMAC-SHA256算法(最安全) + HmacSha256, +} + +impl SignatureAlgorithm { + /// 从字符串解析算法类型 + pub fn from_str(s: &str) -> Result { + match s.to_uppercase().as_str() { + "MD5" => Ok(SignatureAlgorithm::Md5), + "SHA1" => Ok(SignatureAlgorithm::Sha1), + "SHA256" => Ok(SignatureAlgorithm::Sha256), + "HMAC_SHA256" | "HMACSHA256" | "HMAC-SHA256" => Ok(SignatureAlgorithm::HmacSha256), + _ => Err(SignError::InvalidAlgorithm(s.to_string())), + } + } +} + +impl fmt::Display for SignatureAlgorithm { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + SignatureAlgorithm::Md5 => write!(f, "MD5"), + SignatureAlgorithm::Sha1 => write!(f, "SHA1"), + SignatureAlgorithm::Sha256 => write!(f, "SHA256"), + SignatureAlgorithm::HmacSha256 => write!(f, "HMAC-SHA256"), + } + } +} + +impl Default for SignatureAlgorithm { + fn default() -> Self { + Self::Md5 + } +} + +/// 签名选项 +#[derive(Debug, Clone)] +pub struct SignOptions { + /// 签名算法 + pub algorithm: SignatureAlgorithm, + /// AccessKeyId参数名 + pub key_name: String, + /// 合作渠道方ID参数名 + pub channel_id_name: String, + /// 时间戳参数名 + pub timestamp_name: String, + /// 随机字符串参数名 + pub nonce_name: String, + /// 签名参数名 + pub signature_name: String, +} + +impl Default for SignOptions { + fn default() -> Self { + Self { + algorithm: SignatureAlgorithm::default(), + key_name: "AccessKeyId".to_string(), + channel_id_name: "channelId".to_string(), + timestamp_name: "timestamp".to_string(), + nonce_name: "nonce".to_string(), + signature_name: "sign".to_string(), + } + } +} + +/// API签名工具 +#[derive(Debug, Clone)] +pub struct ApiSigner { + options: SignOptions, +} + +impl ApiSigner { + /// 创建API签名工具 + /// + /// # 参数 + /// * `options` - 签名选项,如为None则使用默认选项 + /// + /// # 返回 + /// * 签名工具实例 + pub fn new(options: Option) -> Self { + Self { + options: options.unwrap_or_default(), + } + } + + /// 生成随机字符串 + /// + /// # 返回 + /// * 一个基于当前时间的随机字符串 + pub fn generate_nonce(&self) -> String { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default(); + format!("{}{}", now.as_nanos(), now.as_secs() % 1000) + } + + /// 获取当前时间戳(毫秒) + /// + /// # 返回 + /// * 当前的Unix时间戳(毫秒) + pub fn get_timestamp(&self) -> i64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_millis() as i64 + } + + /// 对请求进行签名 + /// + /// # 参数 + /// * `params` - 请求参数 + /// * `access_key_id` - 访问密钥ID + /// * `secret_key` - 密钥 + /// * `channel_id` - 渠道ID + /// + /// # 返回 + /// * 添加了签名的完整参数 + pub fn sign_request( + &self, + params: &HashMap, + access_key_id: &str, + secret_key: &str, + channel_id: &str, + ) -> HashMap { + let mut signed_params = params.clone(); + + let timestamp = self.get_timestamp(); + signed_params.insert(self.options.key_name.clone(), access_key_id.to_string()); + signed_params.insert(self.options.channel_id_name.clone(), channel_id.to_string()); + signed_params.insert(self.options.timestamp_name.clone(), timestamp.to_string()); + signed_params.insert(self.options.nonce_name.clone(), self.generate_nonce()); + + let signature = self.calculate_signature(&signed_params, secret_key); + + signed_params.insert(self.options.signature_name.clone(), signature); + + signed_params + } + + /// 对URL进行签名 + /// + /// # 参数 + /// * `base_url` - 基础URL地址 + /// * `params` - 请求参数 + /// * `access_key_id` - 访问密钥ID + /// * `secret_key` - 密钥 + /// * `channel_id` - 渠道ID + /// + /// # 返回 + /// * 添加了签名的完整URL + pub fn sign_url( + &self, + base_url: &str, + params: &HashMap, + access_key_id: &str, + secret_key: &str, + channel_id: &str, + ) -> String { + let signed_params = self.sign_request(params, access_key_id, secret_key, channel_id); + + let query_string = self.params_to_query_string(&signed_params); + + if base_url.contains('?') { + format!("{}&{}", base_url, query_string) + } else { + format!("{}?{}", base_url, query_string) + } + } + + /// 将参数转换为查询字符串 + fn params_to_query_string(&self, params: &HashMap) -> String { + let mut pairs = Vec::with_capacity(params.len()); + + for (key, value) in params { + let encoded = form_urlencoded::Serializer::new(String::new()) + .append_pair(key, value) + .finish(); + pairs.push(encoded); + } + + pairs.sort(); + pairs.join("&") + } + + /// 计算签名 + /// + /// # 参数 + /// * `params` - 请求参数 + /// * `secret_key` - 密钥 + /// + /// # 返回 + /// * 签名字符串 + pub fn calculate_signature( + &self, + params: &HashMap, + secret_key: &str, + ) -> String { + let signing_string = self.create_signing_string(params); + + let signing_string = format!("{}&key={}", signing_string, secret_key); + + match self.options.algorithm { + SignatureAlgorithm::Md5 => { + let mut hasher = Md5::new(); + hasher.update(signing_string.as_bytes()); + hex::encode(hasher.finalize()) + } + SignatureAlgorithm::Sha1 => { + let mut hasher = Sha1::new(); + hasher.update(signing_string.as_bytes()); + hex::encode(hasher.finalize()) + } + SignatureAlgorithm::Sha256 => { + let mut hasher = Sha256::new(); + hasher.update(signing_string.as_bytes()); + hex::encode(hasher.finalize()) + } + SignatureAlgorithm::HmacSha256 => { + let mut mac = + HmacSha256::new_from_slice(secret_key.as_bytes()).expect("HMAC初始化失败"); + mac.update(signing_string.as_bytes()); + hex::encode(mac.finalize().into_bytes()) + } + } + } + + /// 创建用于签名的规范化字符串 + /// + /// # 参数 + /// * `params` - 请求参数 + /// + /// # 返回 + /// * 按键名排序并拼接的字符串 + pub fn create_signing_string(&self, params: &HashMap) -> String { + let mut sorted_params = HashMap::new(); + for (k, v) in params { + if k != &self.options.signature_name { + sorted_params.insert(k.clone(), v.clone()); + } + } + + let mut keys: Vec = sorted_params.keys().cloned().collect(); + keys.sort(); + + let mut parts = Vec::with_capacity(keys.len()); + for key in keys { + let value = sorted_params.get(&key).unwrap(); + let value = if self.needs_url_encode(value) { + form_urlencoded::Serializer::new(String::new()) + .append_pair("", value) + .finish() + .trim_start_matches('=') + .to_string() + } else { + value.clone() + }; + parts.push(format!("{}={}", key, value)); + } + + parts.join("&") + } + + /// 判断是否需要对字符串进行URL编码 + fn needs_url_encode(&self, s: &str) -> bool { + let re = Regex::new(r"^[a-zA-Z0-9]*$").unwrap(); + !re.is_match(s) + } + + /// 验证签名 + /// + /// # 参数 + /// * `params` - 所有请求参数,包括签名 + /// * `secret_key` - 密钥 + /// * `max_age_ms` - 允许的最大时间差(毫秒) + /// + /// # 返回 + /// * 验证结果,成功为Ok(()),失败为Err + pub fn verify_signature( + &self, + params: &HashMap, + secret_key: &str, + max_age_ms: i64, + ) -> Result<(), SignError> { + if !params.contains_key(&self.options.key_name) { + return Err(SignError::MissingParameter(self.options.key_name.clone())); + } + + let timestamp_str = params + .get(&self.options.timestamp_name) + .ok_or_else(|| SignError::MissingParameter(self.options.timestamp_name.clone()))?; + + let timestamp = timestamp_str + .parse::() + .map_err(|e| SignError::ParseError(e.to_string()))?; + + let now = self.get_timestamp(); + if (now - timestamp).abs() > max_age_ms { + return Err(SignError::TimestampExpired); + } + + if !params.contains_key(&self.options.nonce_name) { + return Err(SignError::MissingParameter(self.options.nonce_name.clone())); + } + + let provided_signature = params + .get(&self.options.signature_name) + .ok_or_else(|| SignError::MissingParameter(self.options.signature_name.clone()))?; + + let expected_signature = self.calculate_signature(params, secret_key); + if expected_signature == *provided_signature { + Ok(()) + } else { + Err(SignError::SignatureVerificationFailed) + } + } +} + +/// 默认创建签名选项 +pub fn default_sign_options() -> SignOptions { + SignOptions { + algorithm: SignatureAlgorithm::Md5, + key_name: String::from("AccessKeyId"), + channel_id_name: String::from("channelId"), + timestamp_name: String::from("timestamp"), + nonce_name: String::from("nonce"), + signature_name: String::from("sign"), + } +} diff --git a/rust/src/main.rs b/rust/src/main.rs new file mode 100644 index 0000000..80c3edb --- /dev/null +++ b/rust/src/main.rs @@ -0,0 +1,167 @@ +use api_signer::{ApiSigner, SignOptions, SignatureAlgorithm}; +use clap::Parser; +use dotenv::dotenv; +use std::{collections::HashMap, env, error::Error}; + +/// API签名工具CLI程序 +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +struct Args { + /// 签名算法:MD5, SHA1, SHA256, HMAC-SHA256 + #[arg(short, long, default_value = "MD5")] + algorithm: String, + + /// API操作类型:sign_url, sign_params, verify + #[arg(short = 'm', long, default_value = "sign_url")] + action: String, + + /// 基础URL地址(仅sign_url模式需要) + #[arg(short, long, default_value = "https://api.example.com/v1/data")] + url: String, + + /// 请求参数,格式为key=value,可多次指定 + #[arg(short, long)] + param: Vec, + + /// 访问密钥ID(默认从环境变量ACCESS_KEY_ID读取) + #[arg(short = 'k', long)] + access_key_id: Option, + + /// 密钥(默认从环境变量SECRET_KEY读取) + #[arg(short = 's', long)] + secret_key: Option, + + /// 合作渠道方ID(默认从环境变量CHANNEL_ID读取) + #[arg(short = 'c', long)] + channel_id: Option, + + /// 签名(仅verify模式需要) + #[arg(short = 'g', long)] + signature: Option, +} + +fn main() -> Result<(), Box> { + // 加载环境变量 + dotenv().ok(); + + // 解析命令行参数 + let args = Args::parse(); + + // 获取配置 + let access_key_id = args + .access_key_id + .unwrap_or_else(|| env::var("ACCESS_KEY_ID").unwrap_or("test-access-key-id".to_string())); + + let secret_key = args + .secret_key + .unwrap_or_else(|| env::var("SECRET_KEY").unwrap_or("test-secret-key".to_string())); + + let channel_id = args + .channel_id + .unwrap_or_else(|| env::var("CHANNEL_ID").unwrap_or("test-channel-id".to_string())); + + // 解析签名算法 + let algorithm = + SignatureAlgorithm::from_str(&args.algorithm).unwrap_or(SignatureAlgorithm::Md5); + + // 创建自定义签名选项 + let mut options = SignOptions::default(); + options.algorithm = algorithm; + + // 创建签名工具 + let signer = ApiSigner::new(Some(options.clone())); + + // 解析参数 + let mut params = HashMap::new(); + for param in &args.param { + if let Some((key, value)) = param.split_once('=') { + params.insert(key.to_string(), value.to_string()); + } + } + + // 如果没有参数,使用默认参数进行演示 + if params.is_empty() { + params.insert("userId".to_string(), "12345".to_string()); + params.insert("action".to_string(), "getData".to_string()); + params.insert("data".to_string(), "测试数据".to_string()); // 包含非ASCII字符,测试URL编码 + } + + println!("===================== API签名示例 ====================="); + println!("AccessKeyId: {}", access_key_id); + println!("ChannelId: {}", channel_id); + println!("SecretKey: {}", secret_key); + println!("签名算法: {}", algorithm); + println!("基础URL: {}", args.url); + println!("请求参数:"); + for (key, value) in ¶ms { + println!(" {}: {}", key, value); + } + + match args.action.as_str() { + "sign_url" => { + // 签名URL + let signed_url = + signer.sign_url(&args.url, ¶ms, &access_key_id, &secret_key, &channel_id); + println!("\n签名后的URL:"); + println!("{}", signed_url); + } + "sign_params" => { + // 获取签名后的参数 + let signed_params = + signer.sign_request(¶ms, &access_key_id, &secret_key, &channel_id); + println!("\n签名后的参数:"); + for (key, value) in &signed_params { + println!(" {}: {}", key, value); + } + } + "verify" => { + // 添加签名参数(如果提供) + if let Some(signature) = args.signature { + params.insert(options.signature_name.clone(), signature); + } + + // 验证签名 + match signer.verify_signature(¶ms, &secret_key, 300000) { + Ok(()) => println!("\n签名验证结果: 验证成功"), + Err(err) => println!("\n签名验证结果: 验证失败 - {}", err), + } + } + _ => { + return Err(format!("未知操作类型: {}", args.action).into()); + } + } + + // 演示不同算法的签名结果 + demonstrate_algorithms(¶ms, &access_key_id, &secret_key); + + Ok(()) +} + +/// 演示不同算法的签名结果 +fn demonstrate_algorithms(params: &HashMap, access_key_id: &str, secret_key: &str) { + println!("\n不同算法的签名结果:"); + + let algorithms = [ + SignatureAlgorithm::Md5, + SignatureAlgorithm::Sha1, + SignatureAlgorithm::Sha256, + SignatureAlgorithm::HmacSha256, + ]; + + let default_channel_id = "test-channel-id".to_string(); + let channel_id = params.get("channelId").unwrap_or(&default_channel_id); + + for alg in &algorithms { + let mut options = SignOptions::default(); + options.algorithm = *alg; + let signer = ApiSigner::new(Some(options.clone())); + + // 添加必要的参数用于签名 + let mut sign_params = params.clone(); + sign_params.insert("AccessKeyId".to_string(), access_key_id.to_string()); + sign_params.insert("channelId".to_string(), channel_id.to_string()); + + let signature = signer.calculate_signature(&sign_params, secret_key); + println!(" {}: {}", alg, signature); + } +}