sa_token_core/
sso.rs

1//! # SSO 单点登录模块 | SSO Single Sign-On Module
2//!
3//! 提供完整的单点登录功能实现,支持票据认证和统一登出。
4//! Provides complete Single Sign-On functionality with ticket-based authentication and unified logout.
5//!
6//! ## 代码流程逻辑 | Code Flow Logic
7//!
8//! ### 1. 核心组件 | Core Components
9//!
10//! ```text
11//! SsoServer(SSO 服务端)
12//!   ├── 票据管理 | Ticket Management
13//!   │   ├── 生成票据 create_ticket()
14//!   │   ├── 验证票据 validate_ticket()
15//!   │   └── 清理过期票据 cleanup_expired_tickets()
16//!   ├── 会话管理 | Session Management
17//!   │   ├── 创建会话 login()
18//!   │   ├── 获取会话 get_session()
19//!   │   └── 删除会话 logout()
20//!   └── 客户端追踪 | Client Tracking
21//!       └── 获取活跃客户端 get_active_clients()
22//!
23//! SsoClient(SSO 客户端)
24//!   ├── URL 生成 | URL Generation
25//!   │   ├── 登录 URL get_login_url()
26//!   │   └── 登出 URL get_logout_url()
27//!   ├── 本地会话 | Local Session
28//!   │   ├── 检查登录 check_local_login()
29//!   │   └── 票据登录 login_by_ticket()
30//!   └── 登出处理 | Logout Handling
31//!       └── 处理登出 handle_logout()
32//! ```
33//!
34//! ### 2. 登录流程 | Login Flow
35//!
36//! ```text
37//! 步骤 1: 用户访问应用 → 重定向到 SSO Server
38//! Step 1: User accesses app → Redirect to SSO Server
39//!
40//! 步骤 2: SSO Server 验证凭证
41//! Step 2: SSO Server validates credentials
42//!   └─> login(login_id, service) 
43//!       ├─> 创建 Token
44//!       ├─> 创建或更新 SsoSession
45//!       └─> 生成 SsoTicket
46//!
47//! 步骤 3: 客户端应用验证票据
48//! Step 3: Client app validates ticket
49//!   └─> validate_ticket(ticket_id, service)
50//!       ├─> 检查票据存在
51//!       ├─> 验证票据有效性(未过期、未使用)
52//!       ├─> 验证服务 URL 匹配
53//!       ├─> 标记票据为已使用
54//!       └─> 返回 login_id
55//!
56//! 步骤 4: 创建本地会话
57//! Step 4: Create local session
58//!   └─> client.login_by_ticket(login_id)
59//!       └─> manager.login(login_id) → 创建本地 Token
60//! ```
61//!
62//! ### 3. SSO 无缝登录流程 | SSO Seamless Login Flow
63//!
64//! ```text
65//! 用户已在应用1登录,访问应用2:
66//! User logged in App1, accessing App2:
67//!
68//! 应用2 → SSO Server: 请求认证
69//! App2 → SSO Server: Request authentication
70//!   └─> is_logged_in(login_id) → true
71//!       └─> create_ticket(login_id, app2_url)
72//!           └─> 直接返回票据(无需再次登录)
73//!               Return ticket (no re-login required)
74//!
75//! 应用2 → 验证票据 → 创建本地会话 → 访问授权
76//! App2 → Validate ticket → Create local session → Access granted
77//! ```
78//!
79//! ### 4. 统一登出流程 | Unified Logout Flow
80//!
81//! ```text
82//! 用户从任一应用登出:
83//! User logs out from any app:
84//!
85//! logout(login_id)
86//!   ├─> 获取 SsoSession
87//!   ├─> 获取所有已登录客户端列表
88//!   ├─> 删除 SsoSession
89//!   ├─> 删除用户的所有 Token
90//!   └─> 返回客户端列表
91//!
92//! 通知所有客户端:
93//! Notify all clients:
94//!   └─> for each client_url
95//!       └─> client.handle_logout(login_id)
96//!           └─> 清除本地会话 | Clear local session
97//! ```
98//!
99//! ### 5. 票据生命周期 | Ticket Lifecycle
100//!
101//! ```text
102//! 创建 | Create: ticket.create_time = now
103//!   └─> 设置过期时间 | Set expiration: expire_time = now + timeout
104//!   └─> 状态 | Status: used = false
105//!
106//! 验证 | Validate:
107//!   ├─> 检查过期 | Check expiration: now > expire_time?
108//!   ├─> 检查使用状态 | Check usage: used == true?
109//!   └─> 验证服务 | Verify service: service == expected?
110//!
111//! 使用 | Use: 验证成功后 | After validation
112//!   └─> ticket.used = true(一次性使用 | One-time use)
113//!
114//! 清理 | Cleanup: cleanup_expired_tickets()
115//!   └─> 删除所有过期或已使用的票据
116//!       Remove all expired or used tickets
117//! ```
118//!
119//! ### 6. 安全机制 | Security Mechanisms
120//!
121//! ```text
122//! 1. 票据一次性使用 | One-time ticket usage
123//!    └─> validate_ticket() 后立即设置 used = true
124//!
125//! 2. 服务 URL 匹配 | Service URL matching
126//!    └─> ticket.service 必须与请求的 service 完全匹配
127//!
128//! 3. 票据过期 | Ticket expiration
129//!    └─> 默认 5 分钟过期,可配置
130//!
131//! 4. 跨域保护 | Cross-domain protection
132//!    └─> SsoConfig.allowed_origins 白名单机制
133//!
134//! 5. UUID 票据 ID | UUID ticket ID
135//!    └─> 使用 UUID 防止票据 ID 被猜测
136//! ```
137
138use std::sync::Arc;
139use std::collections::HashMap;
140use chrono::{DateTime, Utc, Duration as ChronoDuration};
141use serde::{Serialize, Deserialize};
142use tokio::sync::RwLock;
143use crate::{SaTokenError, SaTokenResult, SaTokenManager};
144
145/// SSO 票据结构 | SSO Ticket Structure
146///
147/// 票据是一个短期、一次性使用的认证令牌
148/// A ticket is a short-lived, one-time use authentication token
149#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct SsoTicket {
151    /// 票据唯一标识符(UUID)| Unique ticket identifier (UUID)
152    pub ticket_id: String,
153    /// 目标服务 URL | Target service URL
154    pub service: String,
155    /// 用户登录 ID | User login ID
156    pub login_id: String,
157    /// 票据创建时间 | Ticket creation time
158    pub create_time: DateTime<Utc>,
159    /// 票据过期时间 | Ticket expiration time
160    pub expire_time: DateTime<Utc>,
161    /// 是否已使用(一次性使用)| Whether used (one-time use)
162    pub used: bool,
163}
164
165impl SsoTicket {
166    /// 创建新票据 | Create a new ticket
167    ///
168    /// # 参数 | Parameters
169    /// * `login_id` - 用户登录 ID | User login ID
170    /// * `service` - 目标服务 URL | Target service URL
171    /// * `timeout_seconds` - 票据有效期(秒)| Ticket validity period (seconds)
172    pub fn new(login_id: String, service: String, timeout_seconds: i64) -> Self {
173        let now = Utc::now();
174        Self {
175            ticket_id: uuid::Uuid::new_v4().to_string(),
176            service,
177            login_id,
178            create_time: now,
179            expire_time: now + ChronoDuration::seconds(timeout_seconds),
180            used: false,
181        }
182    }
183
184    /// 检查票据是否过期 | Check if ticket is expired
185    pub fn is_expired(&self) -> bool {
186        Utc::now() > self.expire_time
187    }
188
189    /// 检查票据是否有效(未使用且未过期)| Check if ticket is valid (not used and not expired)
190    pub fn is_valid(&self) -> bool {
191        !self.used && !self.is_expired()
192    }
193}
194
195/// SSO 全局会话 | SSO Global Session
196///
197/// 跟踪用户在所有应用中的登录状态
198/// Tracks user's login status across all applications
199#[derive(Debug, Clone, Serialize, Deserialize)]
200pub struct SsoSession {
201    /// 用户登录 ID | User login ID
202    pub login_id: String,
203    /// 已登录的客户端列表 | List of logged-in clients
204    pub clients: Vec<String>,
205    /// 会话创建时间 | Session creation time
206    pub create_time: DateTime<Utc>,
207    /// 最后活动时间 | Last activity time
208    pub last_active_time: DateTime<Utc>,
209}
210
211impl SsoSession {
212    /// 创建新会话 | Create a new session
213    pub fn new(login_id: String) -> Self {
214        let now = Utc::now();
215        Self {
216            login_id,
217            clients: Vec::new(),
218            create_time: now,
219            last_active_time: now,
220        }
221    }
222
223    /// 添加客户端到会话 | Add client to session
224    ///
225    /// 如果客户端不在列表中,则添加
226    /// Adds client if not already in the list
227    pub fn add_client(&mut self, service: String) {
228        if !self.clients.contains(&service) {
229            self.clients.push(service);
230        }
231        self.last_active_time = Utc::now();
232    }
233
234    /// 从会话中移除客户端 | Remove client from session
235    pub fn remove_client(&mut self, service: &str) {
236        self.clients.retain(|c| c != service);
237        self.last_active_time = Utc::now();
238    }
239}
240
241/// SSO 服务端 | SSO Server
242///
243/// 中央认证服务,负责票据生成、验证和会话管理
244/// Central authentication service responsible for ticket generation, validation, and session management
245pub struct SsoServer {
246    manager: Arc<SaTokenManager>,
247    tickets: Arc<RwLock<HashMap<String, SsoTicket>>>,
248    sessions: Arc<RwLock<HashMap<String, SsoSession>>>,
249    ticket_timeout: i64,
250}
251
252impl SsoServer {
253    /// 创建新的 SSO 服务端 | Create a new SSO Server
254    ///
255    /// # 参数 | Parameters
256    /// * `manager` - SaTokenManager 实例 | SaTokenManager instance
257    pub fn new(manager: Arc<SaTokenManager>) -> Self {
258        Self {
259            manager,
260            tickets: Arc::new(RwLock::new(HashMap::new())),
261            sessions: Arc::new(RwLock::new(HashMap::new())),
262            ticket_timeout: 300, // 默认 5 分钟 | Default 5 minutes
263        }
264    }
265
266    /// 设置票据超时时间 | Set ticket timeout
267    ///
268    /// # 参数 | Parameters
269    /// * `timeout` - 超时时间(秒)| Timeout in seconds
270    pub fn with_ticket_timeout(mut self, timeout: i64) -> Self {
271        self.ticket_timeout = timeout;
272        self
273    }
274
275    /// 检查用户是否已登录 | Check if user is logged in
276    ///
277    /// 通过检查 SSO 会话是否存在来判断
278    /// Determined by checking if SSO session exists
279    pub async fn is_logged_in(&self, login_id: &str) -> bool {
280        let sessions = self.sessions.read().await;
281        let has_session = sessions.contains_key(login_id);
282        drop(sessions);
283        
284        // 如果会话存在,进一步验证 Token 是否有效
285        if has_session {
286            let key = format!("sa:login:token:{}:sso", login_id);
287            matches!(self.manager.storage.get(&key).await, Ok(Some(_)))
288        } else {
289            false
290        }
291    }
292
293    /// 创建票据 | Create ticket
294    ///
295    /// 为已登录用户创建访问特定服务的票据
296    /// Creates a ticket for logged-in user to access specific service
297    ///
298    /// # 参数 | Parameters
299    /// * `login_id` - 用户登录 ID | User login ID
300    /// * `service` - 目标服务 URL | Target service URL
301    ///
302    /// # 返回 | Returns
303    /// 新创建的票据 | Newly created ticket
304    pub async fn create_ticket(&self, login_id: String, service: String) -> SaTokenResult<SsoTicket> {
305        // 生成票据 | Generate ticket
306        let ticket = SsoTicket::new(login_id.clone(), service.clone(), self.ticket_timeout);
307        
308        // 存储票据 | Store ticket
309        let mut tickets = self.tickets.write().await;
310        tickets.insert(ticket.ticket_id.clone(), ticket.clone());
311
312        // 更新会话,添加客户端 | Update session, add client
313        let mut sessions = self.sessions.write().await;
314        sessions.entry(login_id.clone())
315            .or_insert_with(|| SsoSession::new(login_id))
316            .add_client(service);
317
318        Ok(ticket)
319    }
320
321    /// 验证票据 | Validate ticket
322    ///
323    /// 验证票据的有效性并将其标记为已使用(一次性使用)
324    /// Validates ticket and marks it as used (one-time use)
325    ///
326    /// # 参数 | Parameters
327    /// * `ticket_id` - 票据 ID | Ticket ID
328    /// * `service` - 请求的服务 URL | Requested service URL
329    ///
330    /// # 返回 | Returns
331    /// 用户登录 ID | User login ID
332    ///
333    /// # 错误 | Errors
334    /// * `InvalidTicket` - 票据不存在 | Ticket not found
335    /// * `TicketExpired` - 票据已过期或已使用 | Ticket expired or used
336    /// * `ServiceMismatch` - 服务 URL 不匹配 | Service URL mismatch
337    pub async fn validate_ticket(&self, ticket_id: &str, service: &str) -> SaTokenResult<String> {
338        let mut tickets = self.tickets.write().await;
339        
340        // 1. 检查票据是否存在 | Check if ticket exists
341        let ticket = tickets.get_mut(ticket_id)
342            .ok_or(SaTokenError::InvalidTicket)?;
343
344        // 2. 验证票据有效性(未过期、未使用)| Validate ticket (not expired, not used)
345        if !ticket.is_valid() {
346            return Err(SaTokenError::TicketExpired);
347        }
348
349        // 3. 验证服务 URL 匹配 | Verify service URL matches
350        if ticket.service != service {
351            return Err(SaTokenError::ServiceMismatch);
352        }
353
354        // 4. 标记票据为已使用(一次性使用)| Mark ticket as used (one-time use)
355        ticket.used = true;
356        let login_id = ticket.login_id.clone();
357
358        Ok(login_id)
359    }
360
361    /// 用户登录 | User login
362    ///
363    /// 完整的登录流程:创建 Token、会话和票据
364    /// Complete login flow: create Token, session, and ticket
365    ///
366    /// # 参数 | Parameters
367    /// * `login_id` - 用户登录 ID | User login ID
368    /// * `service` - 目标服务 URL | Target service URL
369    ///
370    /// # 返回 | Returns
371    /// 生成的票据 | Generated ticket
372    pub async fn login(&self, login_id: String, service: String) -> SaTokenResult<SsoTicket> {
373        // 使用 login_with_options 创建 SSO 类型的 Token
374        let _token = self.manager.login_with_options(
375            &login_id,
376            Some("sso".to_string()), // 设置 login_type 为 "sso"
377            None,
378            Some(serde_json::json!({
379                "sso_mode": true,
380                "service": service.clone()
381            })),
382            None,
383            None,
384        ).await?;
385        
386        // 更新会话
387        let mut sessions = self.sessions.write().await;
388        sessions.entry(login_id.clone())
389            .or_insert_with(|| SsoSession::new(login_id.clone()))
390            .add_client(service.clone());
391
392        drop(sessions);
393
394        // 创建并返回票据
395        self.create_ticket(login_id, service).await
396    }
397
398    /// 统一登出 | Unified logout
399    ///
400    /// 从 SSO 服务端登出,并返回需要通知的客户端列表
401    /// Logout from SSO Server and return list of clients to notify
402    ///
403    /// # 参数 | Parameters
404    /// * `login_id` - 用户登录 ID | User login ID
405    ///
406    /// # 返回 | Returns
407    /// 需要清除会话的客户端 URL 列表 | List of client URLs to clear sessions
408    pub async fn logout(&self, login_id: &str) -> SaTokenResult<Vec<String>> {
409        // 1. 获取并删除 SSO 会话 | Get and remove SSO session
410        let mut sessions = self.sessions.write().await;
411        let session = sessions.remove(login_id);
412        
413        // 2. 提取客户端列表 | Extract client list
414        let clients = session.map(|s| s.clients).unwrap_or_default();
415
416        drop(sessions);
417
418        // 3. 从 Token 管理器中登出(登出所有类型的 Token)| Logout from Token manager (all token types)
419        // 3.1 登出 SSO 服务端 Token
420        let sso_key = format!("sa:login:token:{}:sso", login_id);
421        let _ = self.manager.storage.delete(&sso_key).await;
422        
423        // 3.2 登出默认类型 Token
424        self.manager.logout_by_login_id(login_id).await?;
425
426        // 4. 返回客户端列表供通知 | Return client list for notification
427        Ok(clients)
428    }
429
430    /// 获取 SSO 会话 | Get SSO session
431    ///
432    /// # 参数 | Parameters
433    /// * `login_id` - 用户登录 ID | User login ID
434    ///
435    /// # 返回 | Returns
436    /// SSO 会话信息(如果存在)| SSO session info (if exists)
437    pub async fn get_session(&self, login_id: &str) -> Option<SsoSession> {
438        let sessions = self.sessions.read().await;
439        sessions.get(login_id).cloned()
440    }
441
442    /// 检查会话是否存在 | Check if session exists
443    ///
444    /// # 参数 | Parameters
445    /// * `login_id` - 用户登录 ID | User login ID
446    ///
447    /// # 返回 | Returns
448    /// 会话是否存在 | Whether session exists
449    pub async fn check_session(&self, login_id: &str) -> bool {
450        let sessions = self.sessions.read().await;
451        sessions.contains_key(login_id)
452    }
453
454    /// 清理过期票据 | Cleanup expired tickets
455    ///
456    /// 删除所有过期或已使用的票据
457    /// Removes all expired or used tickets
458    pub async fn cleanup_expired_tickets(&self) {
459        let mut tickets = self.tickets.write().await;
460        tickets.retain(|_, ticket| ticket.is_valid());
461    }
462
463    /// 获取活跃客户端列表 | Get active clients list
464    ///
465    /// # 参数 | Parameters
466    /// * `login_id` - 用户登录 ID | User login ID
467    ///
468    /// # 返回 | Returns
469    /// 客户端 URL 列表 | List of client URLs
470    pub async fn get_active_clients(&self, login_id: &str) -> Vec<String> {
471        let sessions = self.sessions.read().await;
472        sessions.get(login_id)
473            .map(|s| s.clients.clone())
474            .unwrap_or_default()
475    }
476}
477
478/// SSO 客户端 | SSO Client
479///
480/// 每个应用作为 SSO 客户端,处理本地会话和票据验证
481/// Each application acts as SSO Client, handling local sessions and ticket validation
482pub struct SsoClient {
483    /// Token 管理器 | Token manager
484    manager: Arc<SaTokenManager>,
485    /// SSO 服务端 URL | SSO Server URL
486    server_url: String,
487    /// 当前服务 URL | Current service URL
488    service_url: String,
489    /// 登出回调函数 | Logout callback function
490    logout_callback: Option<Arc<dyn Fn(&str) -> bool + Send + Sync>>,
491}
492
493impl SsoClient {
494    /// 创建新的 SSO 客户端 | Create a new SSO Client
495    ///
496    /// # 参数 | Parameters
497    /// * `manager` - SaTokenManager 实例 | SaTokenManager instance
498    /// * `server_url` - SSO 服务端 URL | SSO Server URL
499    /// * `service_url` - 当前服务 URL | Current service URL
500    pub fn new(
501        manager: Arc<SaTokenManager>,
502        server_url: String,
503        service_url: String,
504    ) -> Self {
505        Self {
506            manager,
507            server_url,
508            service_url,
509            logout_callback: None,
510        }
511    }
512
513    /// 设置登出回调函数 | Set logout callback
514    ///
515    /// # 参数 | Parameters
516    /// * `callback` - 登出时执行的回调函数 | Callback function to execute on logout
517    pub fn with_logout_callback<F>(mut self, callback: F) -> Self
518    where
519        F: Fn(&str) -> bool + Send + Sync + 'static,
520    {
521        self.logout_callback = Some(Arc::new(callback));
522        self
523    }
524
525    /// 生成登录 URL | Generate login URL
526    ///
527    /// # 返回 | Returns
528    /// SSO 服务端登录 URL,包含当前服务的回调地址
529    /// SSO Server login URL with current service callback
530    pub fn get_login_url(&self) -> String {
531        format!("{}?service={}", self.server_url, urlencoding::encode(&self.service_url))
532    }
533
534    /// 生成登出 URL | Generate logout URL
535    ///
536    /// # 返回 | Returns
537    /// SSO 服务端登出 URL,包含当前服务的回调地址
538    /// SSO Server logout URL with current service callback
539    pub fn get_logout_url(&self) -> String {
540        format!("{}/logout?service={}", self.server_url, urlencoding::encode(&self.service_url))
541    }
542
543    /// 检查本地是否已登录 | Check if locally logged in
544    ///
545    /// # 参数 | Parameters
546    /// * `login_id` - 用户登录 ID | User login ID
547    ///
548    /// # 返回 | Returns
549    /// 是否已登录 | Whether logged in
550    pub async fn check_local_login(&self, login_id: &str) -> bool {
551        // 检查 SSO 客户端类型的登录
552        let key = format!("sa:login:token:{}:sso_client", login_id);
553        match self.manager.storage.get(&key).await {
554            Ok(Some(_)) => true,
555            _ => {
556                // 兼容旧的无类型登录
557                let key_default = format!("sa:login:token:{}", login_id);
558                matches!(self.manager.storage.get(&key_default).await, Ok(Some(_)))
559            }
560        }
561    }
562
563    /// 处理票据(验证票据合法性)| Process ticket (validate ticket)
564    ///
565    /// # 参数 | Parameters
566    /// * `ticket` - 票据 ID | Ticket ID
567    /// * `service` - 服务 URL | Service URL
568    ///
569    /// # 返回 | Returns
570    /// 处理后的票据信息 | Processed ticket info
571    ///
572    /// # 错误 | Errors
573    /// * `ServiceMismatch` - 服务 URL 不匹配 | Service URL mismatch
574    pub async fn process_ticket(&self, ticket: &str, service: &str) -> SaTokenResult<String> {
575        // 验证服务 URL 是否匹配
576        if service != self.service_url {
577            return Err(SaTokenError::ServiceMismatch);
578        }
579
580        Ok(ticket.to_string())
581    }
582
583    /// 通过票据登录(客户端本地登录)| Login by ticket (client-side local login)
584    ///
585    /// # 参数 | Parameters
586    /// * `login_id` - 用户登录 ID | User login ID
587    ///
588    /// # 返回 | Returns
589    /// 生成的本地 Token | Generated local token
590    pub async fn login_by_ticket(&self, login_id: String) -> SaTokenResult<String> {
591        // 使用 login_with_options 创建客户端 Token,标记为 SSO 客户端登录
592        let token = self.manager.login_with_options(
593            &login_id,
594            Some("sso_client".to_string()), // 标记为 SSO 客户端
595            None,
596            Some(serde_json::json!({
597                "sso_client": true,
598                "service_url": self.service_url.clone()
599            })),
600            None,
601            None,
602        ).await?;
603        Ok(token.to_string())
604    }
605
606    /// 处理登出(客户端)| Handle logout (client-side)
607    ///
608    /// # 参数 | Parameters
609    /// * `login_id` - 用户登录 ID | User login ID
610    pub async fn handle_logout(&self, login_id: &str) -> SaTokenResult<()> {
611        // 1. 执行登出回调 | Execute logout callback
612        if let Some(callback) = &self.logout_callback {
613            callback(login_id);
614        }
615        
616        // 2. 登出 SSO 客户端类型的 Token | Logout SSO client token
617        let sso_client_key = format!("sa:login:token:{}:sso_client", login_id);
618        let _ = self.manager.storage.delete(&sso_client_key).await;
619        
620        // 3. 登出默认类型的 Token(兼容)| Logout default token (compatibility)
621        self.manager.logout_by_login_id(login_id).await?;
622        
623        Ok(())
624    }
625
626    /// 获取 SSO 服务端 URL | Get SSO Server URL
627    pub fn server_url(&self) -> &str {
628        &self.server_url
629    }
630
631    /// 获取当前服务 URL | Get current service URL
632    pub fn service_url(&self) -> &str {
633        &self.service_url
634    }
635}
636
637#[derive(Debug, Clone, Serialize, Deserialize)]
638pub struct SsoConfig {
639    pub server_url: String,
640    pub ticket_timeout: i64,
641    pub allow_cross_domain: bool,
642    pub allowed_origins: Vec<String>,
643}
644
645impl Default for SsoConfig {
646    fn default() -> Self {
647        Self {
648            server_url: "http://localhost:8080/sso".to_string(),
649            ticket_timeout: 300,
650            allow_cross_domain: true,
651            allowed_origins: vec!["*".to_string()],
652        }
653    }
654}
655
656impl SsoConfig {
657    pub fn builder() -> SsoConfigBuilder {
658        SsoConfigBuilder::default()
659    }
660}
661
662#[derive(Default)]
663pub struct SsoConfigBuilder {
664    config: SsoConfig,
665}
666
667impl SsoConfigBuilder {
668    pub fn server_url(mut self, url: impl Into<String>) -> Self {
669        self.config.server_url = url.into();
670        self
671    }
672
673    pub fn ticket_timeout(mut self, timeout: i64) -> Self {
674        self.config.ticket_timeout = timeout;
675        self
676    }
677
678    pub fn allow_cross_domain(mut self, allow: bool) -> Self {
679        self.config.allow_cross_domain = allow;
680        self
681    }
682
683    pub fn allowed_origins(mut self, origins: Vec<String>) -> Self {
684        self.config.allowed_origins = origins;
685        self
686    }
687
688    pub fn add_allowed_origin(mut self, origin: String) -> Self {
689        if self.config.allowed_origins == vec!["*".to_string()] {
690            self.config.allowed_origins = vec![origin];
691        } else {
692            self.config.allowed_origins.push(origin);
693        }
694        self
695    }
696
697    pub fn build(self) -> SsoConfig {
698        self.config
699    }
700}
701
702pub struct SsoManager {
703    server: Option<Arc<SsoServer>>,
704    client: Option<Arc<SsoClient>>,
705    config: SsoConfig,
706}
707
708impl SsoManager {
709    pub fn new(config: SsoConfig) -> Self {
710        Self {
711            server: None,
712            client: None,
713            config,
714        }
715    }
716
717    pub fn with_server(mut self, server: Arc<SsoServer>) -> Self {
718        self.server = Some(server);
719        self
720    }
721
722    pub fn with_client(mut self, client: Arc<SsoClient>) -> Self {
723        self.client = Some(client);
724        self
725    }
726
727    pub fn server(&self) -> Option<&Arc<SsoServer>> {
728        self.server.as_ref()
729    }
730
731    pub fn client(&self) -> Option<&Arc<SsoClient>> {
732        self.client.as_ref()
733    }
734
735    pub fn config(&self) -> &SsoConfig {
736        &self.config
737    }
738
739    pub fn is_allowed_origin(&self, origin: &str) -> bool {
740        if !self.config.allow_cross_domain {
741            return false;
742        }
743
744        self.config.allowed_origins.contains(&"*".to_string()) ||
745        self.config.allowed_origins.contains(&origin.to_string())
746    }
747}
748