完成API签名工具的基础结构,包含Cargo配置、README文档及核心库实现,支持多种签名算法和命令行工具功能。

This commit is contained in:
SF-bytebytebrew
2025-05-21 14:00:33 +08:00
parent f94df2bdbb
commit b92d406bac
4 changed files with 791 additions and 0 deletions

20
rust/Cargo.toml Normal file
View File

@@ -0,0 +1,20 @@
[package]
name = "api-signer"
version = "0.1.0"
edition = "2021"
authors = ["Sound Force <info@example.com>"]
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"] }

231
rust/README.md Normal file
View File

@@ -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<dyn std::error::Error>> {
// 获取签名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(
&params,
"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",
&params,
"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

373
rust/src/lib.rs Normal file
View File

@@ -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<Sha256>;
/// 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<Self, SignError> {
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<SignOptions>) -> 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<String, String>,
access_key_id: &str,
secret_key: &str,
channel_id: &str,
) -> HashMap<String, String> {
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<String, String>,
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, String>) -> 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<String, String>,
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, String>) -> 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<String> = 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<String, String>,
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::<i64>()
.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"),
}
}

167
rust/src/main.rs Normal file
View File

@@ -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<String>,
/// 访问密钥ID默认从环境变量ACCESS_KEY_ID读取
#[arg(short = 'k', long)]
access_key_id: Option<String>,
/// 密钥默认从环境变量SECRET_KEY读取
#[arg(short = 's', long)]
secret_key: Option<String>,
/// 合作渠道方ID默认从环境变量CHANNEL_ID读取
#[arg(short = 'c', long)]
channel_id: Option<String>,
/// 签名仅verify模式需要
#[arg(short = 'g', long)]
signature: Option<String>,
}
fn main() -> Result<(), Box<dyn Error>> {
// 加载环境变量
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 &params {
println!(" {}: {}", key, value);
}
match args.action.as_str() {
"sign_url" => {
// 签名URL
let signed_url =
signer.sign_url(&args.url, &params, &access_key_id, &secret_key, &channel_id);
println!("\n签名后的URL:");
println!("{}", signed_url);
}
"sign_params" => {
// 获取签名后的参数
let signed_params =
signer.sign_request(&params, &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(&params, &secret_key, 300000) {
Ok(()) => println!("\n签名验证结果: 验证成功"),
Err(err) => println!("\n签名验证结果: 验证失败 - {}", err),
}
}
_ => {
return Err(format!("未知操作类型: {}", args.action).into());
}
}
// 演示不同算法的签名结果
demonstrate_algorithms(&params, &access_key_id, &secret_key);
Ok(())
}
/// 演示不同算法的签名结果
fn demonstrate_algorithms(params: &HashMap<String, String>, 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);
}
}