Skip to main content

sa_token_core/
sign.rs

1// Author: 金书记 | Author: Jin Shuji
2//! HMAC-SHA256 request signing (timestamp + optional nonce).
3//! HMAC-SHA256 请求签名(时间戳 + 可选 nonce)。
4//!
5//! Shared by SSO HTTP and any open API that wants the same canonical query.
6//! SSO HTTP 与需要同一套规范查询串的开放 API 共用本类型。
7
8use std::collections::BTreeMap;
9use std::sync::Arc;
10use std::time::Duration;
11
12use hmac::{Hmac, KeyInit, Mac};
13use sha2::Sha256;
14
15use crate::dao::SaTokenDao;
16use crate::error::{SaTokenError, SaTokenResult};
17use crate::http_basic::ct_eq;
18
19type HmacSha256 = Hmac<Sha256>;
20
21/// Query/body signing helper.
22/// 查询串/表单体签名。
23#[derive(Clone)]
24pub struct RequestSign {
25    secret: String,
26    window_secs: i64,
27    dao: Option<Arc<SaTokenDao>>,
28}
29
30impl std::fmt::Debug for RequestSign {
31    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32        f.write_str("RequestSign { .. }")
33    }
34}
35
36impl RequestSign {
37    /// Create a signer with secret and timestamp window (seconds).
38    /// 使用密钥与时间窗(秒)创建签名器。
39    pub fn new(secret: impl Into<String>, window_secs: i64) -> Self {
40        Self {
41            secret: secret.into(),
42            window_secs: if window_secs > 0 { window_secs } else { 300 },
43            dao: None,
44        }
45    }
46
47    /// Attach Dao so nonce values can be consumed once.
48    /// 挂载 Dao,使 nonce 只能使用一次。
49    pub fn with_dao(mut self, dao: Arc<SaTokenDao>) -> Self {
50        self.dao = Some(dao);
51        self
52    }
53
54    fn canonical(params: &BTreeMap<String, String>) -> String {
55        params
56            .iter()
57            .filter(|(k, _)| k.as_str() != "sign")
58            .map(|(k, v)| format!("{k}={v}"))
59            .collect::<Vec<_>>()
60            .join("&")
61    }
62
63    /// Hex HMAC-SHA256 over the canonical query (excludes the `sign` field).
64    /// 对规范查询串做 Hex HMAC-SHA256(排除 `sign` 字段)。
65    pub fn sign_params(&self, params: &BTreeMap<String, String>) -> SaTokenResult<String> {
66        if self.secret.is_empty() {
67            return Err(SaTokenError::ConfigError(
68                "request sign secret is empty".into(),
69            ));
70        }
71        let mut mac = HmacSha256::new_from_slice(self.secret.as_bytes())
72            .map_err(|e| SaTokenError::ConfigError(format!("invalid request sign secret: {e}")))?;
73        mac.update(Self::canonical(params).as_bytes());
74        Ok(hex::encode(mac.finalize().into_bytes()))
75    }
76
77    /// Insert `timestamp` + `nonce` then compute `sign`. Caller sends the whole map.
78    /// 写入 `timestamp` 与 `nonce` 再计算 `sign`。调用方发送完整 map。
79    pub fn create_signed(
80        &self,
81        mut params: BTreeMap<String, String>,
82    ) -> SaTokenResult<BTreeMap<String, String>> {
83        let now = chrono::Utc::now().timestamp().to_string();
84        let nonce = crate::token::random_hex(32)?;
85        params.insert("timestamp".into(), now);
86        params.insert("nonce".into(), nonce);
87        let sign = self.sign_params(&params)?;
88        params.insert("sign".into(), sign);
89        Ok(params)
90    }
91
92    /// Verify signature, timestamp window, and optional nonce uniqueness.
93    /// 校验签名、时间窗与可选 nonce 唯一性。
94    pub async fn verify_params(
95        &self,
96        params: &BTreeMap<String, String>,
97        provided_sign: &str,
98    ) -> SaTokenResult<()> {
99        if self.secret.is_empty() {
100            return Err(SaTokenError::ConfigError(
101                "request sign secret is empty".into(),
102            ));
103        }
104        let ts = params
105            .get("timestamp")
106            .and_then(|s| s.parse::<i64>().ok())
107            .ok_or(SaTokenError::SignTimestampExpired)?;
108        let now = chrono::Utc::now().timestamp();
109        if (now - ts).abs() > self.window_secs {
110            return Err(SaTokenError::SignTimestampExpired);
111        }
112        if let (Some(nonce), Some(dao)) = (params.get("nonce"), self.dao.as_ref()) {
113            // Dedicated key space so login nonces and request-sign nonces never collide.
114            // 独立键空间,避免登录 nonce 与请求签名 nonce 互相占位。
115            let nkey = dao.keys().sign_nonce(nonce);
116            let inserted = dao
117                .set_if_absent(
118                    &nkey,
119                    "1",
120                    Some(Duration::from_secs(self.window_secs as u64)),
121                )
122                .await?;
123            if !inserted {
124                return Err(SaTokenError::NonceAlreadyUsed);
125            }
126        }
127        let expected = self.sign_params(params)?;
128        if !ct_eq(expected.as_bytes(), provided_sign.as_bytes()) {
129            return Err(SaTokenError::SignInvalid);
130        }
131        Ok(())
132    }
133}
134
135/// Map generic sign errors onto SSO-facing variants used by existing match arms.
136/// 把通用签名错误映射为 SSO 现有匹配臂使用的变体。
137pub fn map_sign_err_to_sso(err: SaTokenError) -> SaTokenError {
138    match err {
139        SaTokenError::SignInvalid => SaTokenError::SsoSignInvalid,
140        SaTokenError::SignTimestampExpired => SaTokenError::TicketExpired,
141        other => other,
142    }
143}