Skip to main content

sa_token_core/sso/
checker.rs

1// Author: 金书记 | Author: Jin Shuji
2//! Ticket consumption backends (local Dao or signed HTTP).
3//! 票据消费后端(本地 Dao 或带签名的 HTTP)。
4
5use async_trait::async_trait;
6
7use crate::error::SaTokenResult;
8use crate::sso::ticket_store::SsoTicketStore;
9
10/// Consumes an SSO ticket and returns login_id.
11/// 消费 SSO 票据并返回 login_id。
12#[async_trait]
13pub trait TicketChecker: Send + Sync {
14    /// Check and consume a ticket for the given service.
15    /// 为给定服务校验并消费票据。
16    async fn check_and_consume(&self, ticket_id: &str, service: &str) -> SaTokenResult<String>;
17}
18
19/// Same storage as the SSO server (recommended in one cluster).
20/// 与 SSO 服务端共享存储(同集群推荐)。
21pub struct LocalTicketChecker {
22    /// Shared ticket store.
23    /// 共享票据存储。
24    pub store: SsoTicketStore,
25}
26
27impl std::fmt::Debug for LocalTicketChecker {
28    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29        f.write_str("LocalTicketChecker { .. }")
30    }
31}
32
33#[async_trait]
34impl TicketChecker for LocalTicketChecker {
35    async fn check_and_consume(&self, ticket_id: &str, service: &str) -> SaTokenResult<String> {
36        self.store.consume(ticket_id, service).await
37    }
38}
39
40#[cfg(feature = "sso-http")]
41use std::collections::BTreeMap;
42
43#[cfg(feature = "sso-http")]
44use serde::Deserialize;
45#[cfg(feature = "sso-http")]
46use uuid::Uuid;
47
48#[cfg(feature = "sso-http")]
49use crate::error::SaTokenError;
50#[cfg(feature = "sso-http")]
51use crate::sso::sign::RequestSign;
52
53/// Signed HTTP remote ticket checker (compiled only with `sso-http`).
54/// 带签名的远程 HTTP 验票器(仅 `sso-http` 时编译)。
55#[cfg(feature = "sso-http")]
56pub struct HttpTicketChecker {
57    /// Remote check endpoint URL.
58    /// 远程验票端点 URL。
59    pub check_url: String,
60    /// Shared request signer.
61    /// 共享请求签名器。
62    pub sign: RequestSign,
63    /// Expected service URL.
64    /// 期望的服务 URL。
65    pub service: String,
66    client: reqwest::Client,
67}
68
69#[cfg(feature = "sso-http")]
70impl HttpTicketChecker {
71    /// Create a remote checker.
72    /// 创建远程验票器。
73    pub fn new(
74        check_url: impl Into<String>,
75        sign: RequestSign,
76        service: impl Into<String>,
77    ) -> Self {
78        Self {
79            check_url: check_url.into(),
80            sign,
81            service: service.into(),
82            client: reqwest::Client::new(),
83        }
84    }
85}
86
87#[cfg(feature = "sso-http")]
88#[derive(Deserialize)]
89struct CheckTicketHttpBody {
90    login_id: String,
91    timestamp: String,
92    nonce: String,
93    sign: String,
94}
95
96#[cfg(feature = "sso-http")]
97#[async_trait]
98impl TicketChecker for HttpTicketChecker {
99    async fn check_and_consume(&self, ticket_id: &str, service: &str) -> SaTokenResult<String> {
100        if service != self.service {
101            return Err(SaTokenError::ServiceMismatch);
102        }
103        let mut params = BTreeMap::new();
104        params.insert("ticket".into(), ticket_id.to_string());
105        params.insert("service".into(), service.to_string());
106        params.insert(
107            "timestamp".into(),
108            chrono::Utc::now().timestamp().to_string(),
109        );
110        params.insert("nonce".into(), Uuid::new_v4().simple().to_string());
111        let sign = self.sign.sign_params(&params)?;
112        params.insert("sign".into(), sign);
113        let qs: String = params
114            .iter()
115            .map(|(k, v)| format!("{}={}", urlencoding::encode(k), urlencoding::encode(v)))
116            .collect::<Vec<_>>()
117            .join("&");
118        let url = format!("{}?{}", self.check_url, qs);
119        let body = self
120            .client
121            .get(&url)
122            .send()
123            .await
124            .map_err(|e| SaTokenError::StorageError(e.to_string()))?
125            .text()
126            .await
127            .map_err(|e| SaTokenError::StorageError(e.to_string()))?;
128        let parsed: CheckTicketHttpBody =
129            serde_json::from_str(&body).map_err(|_| SaTokenError::InvalidTicket)?;
130        let mut verify = BTreeMap::new();
131        verify.insert("login_id".into(), parsed.login_id.clone());
132        verify.insert("timestamp".into(), parsed.timestamp.clone());
133        verify.insert("nonce".into(), parsed.nonce.clone());
134        self.sign
135            .verify_params(&verify, &parsed.sign)
136            .await
137            .map_err(crate::sso::sign::map_sign_err_to_sso)?;
138        Ok(parsed.login_id)
139    }
140}