✨ 完成API签名工具的基础结构,包含Cargo配置、README文档及核心库实现,支持多种签名算法和命令行工具功能。
This commit is contained in:
20
rust/Cargo.toml
Normal file
20
rust/Cargo.toml
Normal 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
231
rust/README.md
Normal 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(
|
||||||
|
¶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
|
||||||
373
rust/src/lib.rs
Normal file
373
rust/src/lib.rs
Normal 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
167
rust/src/main.rs
Normal 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 ¶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<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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user