1use 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#[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 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 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 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 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(¶ms)?;
88 params.insert("sign".into(), sign);
89 Ok(params)
90 }
91
92 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 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
135pub 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}