Consolidate workspace deps and migrate sha1 to sha2
Some checks failed
CI / verify (push) Has been cancelled
Some checks failed
CI / verify (push) Has been cancelled
This commit is contained in:
@@ -5,7 +5,6 @@ use std::{
|
||||
};
|
||||
|
||||
use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier, password_hash::SaltString};
|
||||
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD};
|
||||
use hmac::{Hmac, Mac};
|
||||
use jsonwebtoken::{
|
||||
Algorithm, DecodingKey, EncodingKey, Header, Validation, decode, encode, errors::ErrorKind,
|
||||
@@ -14,7 +13,6 @@ use rand_core::OsRng;
|
||||
use reqwest::{Client, StatusCode};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use sha1::Sha1;
|
||||
use sha2::{Digest, Sha256};
|
||||
use shared_kernel::{new_uuid_simple_string, normalize_optional_string, normalize_required_string};
|
||||
use time::{Duration, OffsetDateTime};
|
||||
@@ -43,7 +41,7 @@ pub const DEFAULT_WECHAT_ACCESS_TOKEN_ENDPOINT: &str =
|
||||
"https://api.weixin.qq.com/sns/oauth2/access_token";
|
||||
pub const DEFAULT_WECHAT_USER_INFO_ENDPOINT: &str = "https://api.weixin.qq.com/sns/userinfo";
|
||||
|
||||
type HmacSha1 = Hmac<Sha1>;
|
||||
type HmacSha256 = Hmac<Sha256>;
|
||||
|
||||
// 鉴权 provider 直接冻结成文档中约定的枚举,避免后续在多个 crate 内重复发明字符串字面量。
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@@ -927,14 +925,6 @@ impl AliyunSmsAuthProvider {
|
||||
query.insert("Action".to_string(), "SendSmsVerifyCode".to_string());
|
||||
query.insert("Format".to_string(), "json".to_string());
|
||||
query.insert("Version".to_string(), "2017-05-25".to_string());
|
||||
query.insert("Timestamp".to_string(), current_aliyun_timestamp());
|
||||
query.insert("SignatureNonce".to_string(), new_uuid_simple_string());
|
||||
query.insert("SignatureMethod".to_string(), "HMAC-SHA1".to_string());
|
||||
query.insert("SignatureVersion".to_string(), "1.0".to_string());
|
||||
query.insert(
|
||||
"AccessKeyId".to_string(),
|
||||
self.config.access_key_id.clone().unwrap_or_default(),
|
||||
);
|
||||
query.insert(
|
||||
"PhoneNumber".to_string(),
|
||||
request.national_phone_number.trim().to_string(),
|
||||
@@ -971,11 +961,12 @@ impl AliyunSmsAuthProvider {
|
||||
if let Some(scheme_name) = self.config.scheme_name.clone() {
|
||||
query.insert("SchemeName".to_string(), scheme_name);
|
||||
}
|
||||
self.sign_query(&mut query)?;
|
||||
let signature_headers = self.build_signature_headers("SendSmsVerifyCode", &query)?;
|
||||
|
||||
let payload = self
|
||||
.client
|
||||
.post(build_aliyun_sms_url(&self.config.endpoint)?)
|
||||
.headers(signature_headers)
|
||||
.form(&query)
|
||||
.send()
|
||||
.await
|
||||
@@ -1053,14 +1044,6 @@ impl AliyunSmsAuthProvider {
|
||||
query.insert("Action".to_string(), "CheckSmsVerifyCode".to_string());
|
||||
query.insert("Format".to_string(), "json".to_string());
|
||||
query.insert("Version".to_string(), "2017-05-25".to_string());
|
||||
query.insert("Timestamp".to_string(), current_aliyun_timestamp());
|
||||
query.insert("SignatureNonce".to_string(), new_uuid_simple_string());
|
||||
query.insert("SignatureMethod".to_string(), "HMAC-SHA1".to_string());
|
||||
query.insert("SignatureVersion".to_string(), "1.0".to_string());
|
||||
query.insert(
|
||||
"AccessKeyId".to_string(),
|
||||
self.config.access_key_id.clone().unwrap_or_default(),
|
||||
);
|
||||
query.insert(
|
||||
"PhoneNumber".to_string(),
|
||||
request.national_phone_number.trim().to_string(),
|
||||
@@ -1080,11 +1063,12 @@ impl AliyunSmsAuthProvider {
|
||||
if let Some(provider_out_id) = request.provider_out_id {
|
||||
query.insert("OutId".to_string(), provider_out_id);
|
||||
}
|
||||
self.sign_query(&mut query)?;
|
||||
let signature_headers = self.build_signature_headers("CheckSmsVerifyCode", &query)?;
|
||||
|
||||
let payload = self
|
||||
.client
|
||||
.post(build_aliyun_sms_url(&self.config.endpoint)?)
|
||||
.headers(signature_headers)
|
||||
.form(&query)
|
||||
.send()
|
||||
.await
|
||||
@@ -1105,24 +1089,48 @@ impl AliyunSmsAuthProvider {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sign_query(&self, query: &mut BTreeMap<String, String>) -> Result<(), SmsProviderError> {
|
||||
fn build_signature_headers(
|
||||
&self,
|
||||
action: &str,
|
||||
form: &BTreeMap<String, String>,
|
||||
) -> Result<reqwest::header::HeaderMap, SmsProviderError> {
|
||||
let access_key_id = self.config.access_key_id.as_deref().ok_or_else(|| {
|
||||
SmsProviderError::InvalidConfig("阿里云短信 AccessKeyId 未配置".to_string())
|
||||
})?;
|
||||
let access_key_secret = self.config.access_key_secret.as_deref().ok_or_else(|| {
|
||||
SmsProviderError::InvalidConfig("阿里云短信 AccessKeySecret 未配置".to_string())
|
||||
})?;
|
||||
let canonicalized = canonicalize_aliyun_rpc_params(query);
|
||||
let string_to_sign = format!(
|
||||
"POST&{}&{}",
|
||||
aliyun_percent_encode("/"),
|
||||
aliyun_percent_encode(&canonicalized)
|
||||
let date = current_aliyun_timestamp();
|
||||
let nonce = new_uuid_simple_string();
|
||||
let payload = build_aliyun_form_body(form);
|
||||
let payload_hash = sha256_hex(payload.as_bytes());
|
||||
let canonical_headers = format!(
|
||||
"host:{}\nx-acs-action:{}\nx-acs-content-sha256:{}\nx-acs-date:{}\nx-acs-signature-nonce:{}\nx-acs-version:2017-05-25\n",
|
||||
self.config.endpoint, action, payload_hash, date, nonce
|
||||
);
|
||||
let mut signer = HmacSha1::new_from_slice(format!("{access_key_secret}&").as_bytes())
|
||||
.map_err(|error| {
|
||||
SmsProviderError::InvalidConfig(format!("初始化短信签名器失败:{error}"))
|
||||
})?;
|
||||
signer.update(string_to_sign.as_bytes());
|
||||
let signature = BASE64_STANDARD.encode(signer.finalize().into_bytes());
|
||||
query.insert("Signature".to_string(), signature);
|
||||
Ok(())
|
||||
let signed_headers =
|
||||
"host;x-acs-action;x-acs-content-sha256;x-acs-date;x-acs-signature-nonce;x-acs-version";
|
||||
let canonical_request = format!(
|
||||
"POST\n/\n\n{}\n{}\n{}",
|
||||
canonical_headers, signed_headers, payload_hash
|
||||
);
|
||||
let string_to_sign = format!(
|
||||
"ACS3-HMAC-SHA256\n{}",
|
||||
sha256_hex(canonical_request.as_bytes())
|
||||
);
|
||||
let signature = hmac_sha256_hex(access_key_secret.as_bytes(), string_to_sign.as_bytes())?;
|
||||
let authorization = format!(
|
||||
"ACS3-HMAC-SHA256 Credential={access_key_id},SignedHeaders={signed_headers},Signature={signature}"
|
||||
);
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
insert_header(&mut headers, "x-acs-action", action)?;
|
||||
insert_header(&mut headers, "x-acs-version", "2017-05-25")?;
|
||||
insert_header(&mut headers, "x-acs-date", &date)?;
|
||||
insert_header(&mut headers, "x-acs-signature-nonce", &nonce)?;
|
||||
insert_header(&mut headers, "x-acs-content-sha256", &payload_hash)?;
|
||||
insert_header(&mut headers, "authorization", &authorization)?;
|
||||
|
||||
Ok(headers)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1453,10 +1461,9 @@ fn current_aliyun_timestamp() -> String {
|
||||
.unwrap_or_else(|_| "1970-01-01T00:00:00Z".to_string())
|
||||
}
|
||||
|
||||
fn canonicalize_aliyun_rpc_params(params: &BTreeMap<String, String>) -> String {
|
||||
fn canonicalize_aliyun_form_params(params: &BTreeMap<String, String>) -> String {
|
||||
params
|
||||
.iter()
|
||||
.filter(|(key, _)| key.as_str() != "Signature")
|
||||
.map(|(key, value)| {
|
||||
format!(
|
||||
"{}={}",
|
||||
@@ -1468,6 +1475,42 @@ fn canonicalize_aliyun_rpc_params(params: &BTreeMap<String, String>) -> String {
|
||||
.join("&")
|
||||
}
|
||||
|
||||
fn build_aliyun_form_body(params: &BTreeMap<String, String>) -> String {
|
||||
serde_urlencoded::to_string(params).unwrap_or_else(|_| canonicalize_aliyun_form_params(params))
|
||||
}
|
||||
|
||||
fn hmac_sha256_hex(key: &[u8], content: &[u8]) -> Result<String, SmsProviderError> {
|
||||
let mut signer = HmacSha256::new_from_slice(key)
|
||||
.map_err(|error| SmsProviderError::InvalidConfig(format!("初始化短信签名器失败:{error}")))?;
|
||||
signer.update(content);
|
||||
Ok(hex_lower(&signer.finalize().into_bytes()))
|
||||
}
|
||||
|
||||
fn sha256_hex(content: &[u8]) -> String {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(content);
|
||||
hex_lower(&hasher.finalize())
|
||||
}
|
||||
|
||||
fn hex_lower(bytes: &[u8]) -> String {
|
||||
bytes
|
||||
.iter()
|
||||
.map(|byte| format!("{byte:02x}"))
|
||||
.collect::<String>()
|
||||
}
|
||||
|
||||
fn insert_header(
|
||||
headers: &mut reqwest::header::HeaderMap,
|
||||
name: &'static str,
|
||||
value: &str,
|
||||
) -> Result<(), SmsProviderError> {
|
||||
let value = reqwest::header::HeaderValue::from_str(value).map_err(|error| {
|
||||
SmsProviderError::InvalidConfig(format!("构造阿里云短信签名头失败:{error}"))
|
||||
})?;
|
||||
headers.insert(reqwest::header::HeaderName::from_static(name), value);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn aliyun_percent_encode(value: &str) -> String {
|
||||
urlencoding::encode(value)
|
||||
.into_owned()
|
||||
@@ -2046,7 +2089,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonicalize_aliyun_rpc_params_keeps_sorted_percent_encoded_order() {
|
||||
fn canonicalize_aliyun_form_params_keeps_sorted_percent_encoded_order() {
|
||||
let mut params = BTreeMap::new();
|
||||
params.insert(
|
||||
"TemplateParam".to_string(),
|
||||
@@ -2056,11 +2099,53 @@ mod tests {
|
||||
params.insert("PhoneNumber".to_string(), "13800138000".to_string());
|
||||
|
||||
assert_eq!(
|
||||
canonicalize_aliyun_rpc_params(¶ms),
|
||||
canonicalize_aliyun_form_params(¶ms),
|
||||
"Action=SendSmsVerifyCode&PhoneNumber=13800138000&TemplateParam=%7B%22code%22%3A%22%23%23code%23%23%22%7D"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aliyun_signature_headers_use_acs3_sha256() {
|
||||
let config = SmsAuthConfig::new(
|
||||
SmsAuthProviderKind::Aliyun,
|
||||
DEFAULT_SMS_ENDPOINT.to_string(),
|
||||
Some("test-access-key-id".to_string()),
|
||||
Some("test-access-key-secret".to_string()),
|
||||
"测试签名".to_string(),
|
||||
"SMS_001".to_string(),
|
||||
DEFAULT_SMS_TEMPLATE_PARAM_KEY.to_string(),
|
||||
DEFAULT_SMS_COUNTRY_CODE.to_string(),
|
||||
None,
|
||||
DEFAULT_SMS_CODE_LENGTH,
|
||||
DEFAULT_SMS_CODE_TYPE,
|
||||
DEFAULT_SMS_VALID_TIME_SECONDS,
|
||||
DEFAULT_SMS_INTERVAL_SECONDS,
|
||||
DEFAULT_SMS_DUPLICATE_POLICY,
|
||||
DEFAULT_SMS_CASE_AUTH_POLICY,
|
||||
false,
|
||||
DEFAULT_SMS_MOCK_VERIFY_CODE.to_string(),
|
||||
)
|
||||
.expect("aliyun config should build");
|
||||
let provider = AliyunSmsAuthProvider {
|
||||
client: Client::new(),
|
||||
config,
|
||||
};
|
||||
let headers = provider
|
||||
.build_signature_headers(
|
||||
"SendSmsVerifyCode",
|
||||
&BTreeMap::from([("Action".to_string(), "SendSmsVerifyCode".to_string())]),
|
||||
)
|
||||
.expect("signature headers should build");
|
||||
|
||||
let authorization = headers
|
||||
.get(reqwest::header::AUTHORIZATION)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.expect("authorization header should exist");
|
||||
|
||||
assert!(authorization.starts_with("ACS3-HMAC-SHA256 Credential=test-access-key-id"));
|
||||
assert!(headers.get("x-acs-content-sha256").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aliyun_send_response_deserializes_pascal_case_fields() {
|
||||
let payload = serde_json::from_str::<AliyunSendSmsVerifyCodeResponse>(
|
||||
|
||||
Reference in New Issue
Block a user