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
145type LogoutCallback = Arc<dyn Fn(&str) -> bool + Send + Sync>;
146
147/// SSO 票据结构 | SSO Ticket Structure
148///
149/// 票据是一个短期、一次性使用的认证令牌
150/// A ticket is a short-lived, one-time use authentication token
151#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct SsoTicket {
153 /// 票据唯一标识符(UUID)| Unique ticket identifier (UUID)
154 pub ticket_id: String,
155 /// 目标服务 URL | Target service URL
156 pub service: String,
157 /// 用户登录 ID | User login ID
158 pub login_id: String,
159 /// 票据创建时间 | Ticket creation time
160 pub create_time: DateTime<Utc>,
161 /// 票据过期时间 | Ticket expiration time
162 pub expire_time: DateTime<Utc>,
163 /// 是否已使用(一次性使用)| Whether used (one-time use)
164 pub used: bool,
165}
166
167impl SsoTicket {
168 /// 创建新票据 | Create a new ticket
169 ///
170 /// # 参数 | Parameters
171 /// * `login_id` - 用户登录 ID | User login ID
172 /// * `service` - 目标服务 URL | Target service URL
173 /// * `timeout_seconds` - 票据有效期(秒)| Ticket validity period (seconds)
174 pub fn new(login_id: String, service: String, timeout_seconds: i64) -> Self {
175 let now = Utc::now();
176 Self {
177 ticket_id: uuid::Uuid::new_v4().to_string(),
178 service,
179 login_id,
180 create_time: now,
181 expire_time: now + ChronoDuration::seconds(timeout_seconds),
182 used: false,
183 }
184 }
185
186 /// 检查票据是否过期 | Check if ticket is expired
187 pub fn is_expired(&self) -> bool {
188 Utc::now() > self.expire_time
189 }
190
191 /// 检查票据是否有效(未使用且未过期)| Check if ticket is valid (not used and not expired)
192 pub fn is_valid(&self) -> bool {
193 !self.used && !self.is_expired()
194 }
195}
196
197/// SSO 全局会话 | SSO Global Session
198///
199/// 跟踪用户在所有应用中的登录状态
200/// Tracks user's login status across all applications
201#[derive(Debug, Clone, Serialize, Deserialize)]
202pub struct SsoSession {
203 /// 用户登录 ID | User login ID
204 pub login_id: String,
205 /// 已登录的客户端列表 | List of logged-in clients
206 pub clients: Vec<String>,
207 /// 会话创建时间 | Session creation time
208 pub create_time: DateTime<Utc>,
209 /// 最后活动时间 | Last activity time
210 pub last_active_time: DateTime<Utc>,
211}
212
213impl SsoSession {
214 /// 创建新会话 | Create a new session
215 pub fn new(login_id: String) -> Self {
216 let now = Utc::now();
217 Self {
218 login_id,
219 clients: Vec::new(),
220 create_time: now,
221 last_active_time: now,
222 }
223 }
224
225 /// 添加客户端到会话 | Add client to session
226 ///
227 /// 如果客户端不在列表中,则添加
228 /// Adds client if not already in the list
229 pub fn add_client(&mut self, service: String) {
230 if !self.clients.contains(&service) {
231 self.clients.push(service);
232 }
233 self.last_active_time = Utc::now();
234 }
235
236 /// 从会话中移除客户端 | Remove client from session
237 pub fn remove_client(&mut self, service: &str) {
238 self.clients.retain(|c| c != service);
239 self.last_active_time = Utc::now();
240 }
241}
242
243/// SSO 票据校验结果(对齐 Java checkTicket 返回)
244#[derive(Debug, Clone, Serialize, Deserialize)]
245pub struct CheckTicketResult {
246 pub login_id: String,
247 /// 剩余有效时间(秒)
248 pub remain_seconds: i64,
249}
250
251/// SSO 服务端 | SSO Server
252///
253/// 中央认证服务,负责票据生成、验证和会话管理
254/// Central authentication service responsible for ticket generation, validation, and session management
255pub struct SsoServer {
256 manager: Arc<SaTokenManager>,
257 tickets: Arc<RwLock<HashMap<String, SsoTicket>>>,
258 sessions: Arc<RwLock<HashMap<String, SsoSession>>>,
259 ticket_timeout: i64,
260 allow_cross_domain: bool,
261 allowed_origins: Vec<String>,
262}
263
264impl SsoServer {
265 /// 创建新的 SSO 服务端 | Create a new SSO Server
266 ///
267 /// # 参数 | Parameters
268 /// * `manager` - SaTokenManager 实例 | SaTokenManager instance
269 pub fn new(manager: Arc<SaTokenManager>) -> Self {
270 Self {
271 manager,
272 tickets: Arc::new(RwLock::new(HashMap::new())),
273 sessions: Arc::new(RwLock::new(HashMap::new())),
274 ticket_timeout: 300,
275 allow_cross_domain: true,
276 allowed_origins: vec!["*".to_string()],
277 }
278 }
279
280 /// 注入 SSO 配置(跨域白名单等)
281 pub fn with_config(mut self, config: &SsoConfig) -> Self {
282 self.ticket_timeout = config.ticket_timeout;
283 self.allow_cross_domain = config.allow_cross_domain;
284 self.allowed_origins = config.allowed_origins.clone();
285 self
286 }
287
288 /// 校验 Origin / 服务 URL 是否在白名单内
289 pub fn is_allowed_origin(&self, origin: &str) -> bool {
290 if !self.allow_cross_domain {
291 return false;
292 }
293 self.allowed_origins.contains(&"*".to_string())
294 || self.allowed_origins.iter().any(|allowed| {
295 origin == allowed || origin.starts_with(allowed)
296 })
297 }
298
299 fn validate_service_access(&self, service: &str) -> SaTokenResult<()> {
300 if !self.is_allowed_origin(service) {
301 return Err(SaTokenError::ServiceMismatch);
302 }
303 Ok(())
304 }
305
306 /// 设置票据超时时间 | Set ticket timeout
307 ///
308 /// # 参数 | Parameters
309 /// * `timeout` - 超时时间(秒)| Timeout in seconds
310 pub fn with_ticket_timeout(mut self, timeout: i64) -> Self {
311 self.ticket_timeout = timeout;
312 self
313 }
314
315 /// 检查用户是否已登录 | Check if user is logged in
316 ///
317 /// 通过检查 SSO 会话是否存在来判断
318 /// Determined by checking if SSO session exists
319 pub async fn is_logged_in(&self, login_id: &str) -> bool {
320 let sessions = self.sessions.read().await;
321 let has_session = sessions.contains_key(login_id);
322 drop(sessions);
323
324 // 如果会话存在,进一步验证 Token 是否有效
325 if has_session {
326 let key = self.manager.config.make_key(
327 "login:token:",
328 &self.manager.account_ns("sso", login_id),
329 );
330 matches!(self.manager.storage.get(&key).await, Ok(Some(_)))
331 } else {
332 false
333 }
334 }
335
336 /// 创建票据 | Create ticket
337 ///
338 /// 为已登录用户创建访问特定服务的票据
339 /// Creates a ticket for logged-in user to access specific service
340 ///
341 /// # 参数 | Parameters
342 /// * `login_id` - 用户登录 ID | User login ID
343 /// * `service` - 目标服务 URL | Target service URL
344 ///
345 /// # 返回 | Returns
346 /// 新创建的票据 | Newly created ticket
347 pub async fn create_ticket(&self, login_id: String, service: String) -> SaTokenResult<SsoTicket> {
348 self.validate_service_access(&service)?;
349 // 生成票据 | Generate ticket
350 let ticket = SsoTicket::new(login_id.clone(), service.clone(), self.ticket_timeout);
351
352 // 存储票据 | Store ticket
353 let mut tickets = self.tickets.write().await;
354 tickets.insert(ticket.ticket_id.clone(), ticket.clone());
355
356 // 更新会话,添加客户端 | Update session, add client
357 let mut sessions = self.sessions.write().await;
358 sessions.entry(login_id.clone())
359 .or_insert_with(|| SsoSession::new(login_id))
360 .add_client(service);
361
362 Ok(ticket)
363 }
364
365 /// 验证票据 | Validate ticket
366 ///
367 /// 验证票据的有效性并将其标记为已使用(一次性使用)
368 /// Validates ticket and marks it as used (one-time use)
369 ///
370 /// # 参数 | Parameters
371 /// * `ticket_id` - 票据 ID | Ticket ID
372 /// * `service` - 请求的服务 URL | Requested service URL
373 ///
374 /// # 返回 | Returns
375 /// 用户登录 ID | User login ID
376 ///
377 /// # 错误 | Errors
378 /// * `InvalidTicket` - 票据不存在 | Ticket not found
379 /// * `TicketExpired` - 票据已过期或已使用 | Ticket expired or used
380 /// * `ServiceMismatch` - 服务 URL 不匹配 | Service URL mismatch
381 pub async fn validate_ticket(&self, ticket_id: &str, service: &str) -> SaTokenResult<String> {
382 self.validate_service_access(service)?;
383 let mut tickets = self.tickets.write().await;
384
385 // 1. 检查票据是否存在 | Check if ticket exists
386 let ticket = tickets.get_mut(ticket_id)
387 .ok_or(SaTokenError::InvalidTicket)?;
388
389 // 2. 验证票据有效性(未过期、未使用)| Validate ticket (not expired, not used)
390 if !ticket.is_valid() {
391 return Err(SaTokenError::TicketExpired);
392 }
393
394 // 3. 验证服务 URL 匹配 | Verify service URL matches
395 if ticket.service != service {
396 return Err(SaTokenError::ServiceMismatch);
397 }
398
399 // 4. 标记票据为已使用(一次性使用)| Mark ticket as used (one-time use)
400 ticket.used = true;
401 let login_id = ticket.login_id.clone();
402
403 Ok(login_id)
404 }
405
406 /// 检查票据(不消费,返回 login_id 与剩余有效期)
407 pub async fn check_ticket(
408 &self,
409 ticket_id: &str,
410 service: &str,
411 ) -> SaTokenResult<CheckTicketResult> {
412 self.validate_service_access(service)?;
413 let tickets = self.tickets.read().await;
414 let ticket = tickets.get(ticket_id).ok_or(SaTokenError::InvalidTicket)?;
415
416 if !ticket.is_valid() {
417 return Err(SaTokenError::TicketExpired);
418 }
419 if ticket.service != service {
420 return Err(SaTokenError::ServiceMismatch);
421 }
422
423 let remain = ticket.expire_time.signed_duration_since(Utc::now());
424 Ok(CheckTicketResult {
425 login_id: ticket.login_id.clone(),
426 remain_seconds: remain.num_seconds().max(0),
427 })
428 }
429
430 /// 为 SLO 统一登出生成各客户端回调 URL
431 pub fn build_slo_logout_urls(client_urls: &[String]) -> Vec<String> {
432 client_urls
433 .iter()
434 .map(|client| {
435 let base = client.trim_end_matches('/');
436 format!("{}/sso/logout?slo=1&service={}", base, urlencoding::encode(client))
437 })
438 .collect()
439 }
440
441 /// 统一登出并返回 SLO 回调 URL 列表
442 pub async fn logout_with_slo(&self, login_id: &str) -> SaTokenResult<Vec<String>> {
443 let clients = self.logout(login_id).await?;
444 Ok(Self::build_slo_logout_urls(&clients))
445 }
446
447 /// 用户登录 | User login
448 ///
449 /// 完整的登录流程:创建 Token、会话和票据
450 /// Complete login flow: create Token, session, and ticket
451 ///
452 /// # 参数 | Parameters
453 /// * `login_id` - 用户登录 ID | User login ID
454 /// * `service` - 目标服务 URL | Target service URL
455 ///
456 /// # 返回 | Returns
457 /// 生成的票据 | Generated ticket
458 pub async fn login(&self, login_id: String, service: String) -> SaTokenResult<SsoTicket> {
459 // 使用 login_with_options 创建 SSO 类型的 Token
460 let _token = self.manager.login_with_options(
461 &login_id,
462 Some("sso".to_string()), // 设置 login_type 为 "sso"
463 None,
464 Some(serde_json::json!({
465 "sso_mode": true,
466 "service": service.clone()
467 })),
468 None,
469 None,
470 ).await?;
471
472 // 更新会话
473 let mut sessions = self.sessions.write().await;
474 sessions.entry(login_id.clone())
475 .or_insert_with(|| SsoSession::new(login_id.clone()))
476 .add_client(service.clone());
477
478 drop(sessions);
479
480 // 创建并返回票据
481 self.create_ticket(login_id, service).await
482 }
483
484 /// 统一登出 | Unified logout
485 ///
486 /// 从 SSO 服务端登出,并返回需要通知的客户端列表
487 /// Logout from SSO Server and return list of clients to notify
488 ///
489 /// # 参数 | Parameters
490 /// * `login_id` - 用户登录 ID | User login ID
491 ///
492 /// # 返回 | Returns
493 /// 需要清除会话的客户端 URL 列表 | List of client URLs to clear sessions
494 pub async fn logout(&self, login_id: &str) -> SaTokenResult<Vec<String>> {
495 // 1. 获取并删除 SSO 会话 | Get and remove SSO session
496 let mut sessions = self.sessions.write().await;
497 let session = sessions.remove(login_id);
498
499 // 2. 提取客户端列表 | Extract client list
500 let clients = session.map(|s| s.clients).unwrap_or_default();
501
502 drop(sessions);
503
504 // 3. 从 Token 管理器中登出(登出所有类型的 Token)| Logout from Token manager (all token types)
505 // 3.1 登出 SSO 服务端 Token
506 let sso_key = self.manager.config.make_key(
507 "login:token:",
508 &self.manager.account_ns("sso", login_id),
509 );
510 let _ = self.manager.storage.delete(&sso_key).await;
511
512 // 3.2 登出默认类型 Token
513 self.manager.logout_by_login_id(login_id).await?;
514
515 // 4. 返回客户端列表供通知 | Return client list for notification
516 Ok(clients)
517 }
518
519 /// 获取 SSO 会话 | Get SSO session
520 ///
521 /// # 参数 | Parameters
522 /// * `login_id` - 用户登录 ID | User login ID
523 ///
524 /// # 返回 | Returns
525 /// SSO 会话信息(如果存在)| SSO session info (if exists)
526 pub async fn get_session(&self, login_id: &str) -> Option<SsoSession> {
527 let sessions = self.sessions.read().await;
528 sessions.get(login_id).cloned()
529 }
530
531 /// 检查会话是否存在 | Check if session exists
532 ///
533 /// # 参数 | Parameters
534 /// * `login_id` - 用户登录 ID | User login ID
535 ///
536 /// # 返回 | Returns
537 /// 会话是否存在 | Whether session exists
538 pub async fn check_session(&self, login_id: &str) -> bool {
539 let sessions = self.sessions.read().await;
540 sessions.contains_key(login_id)
541 }
542
543 /// 清理过期票据 | Cleanup expired tickets
544 ///
545 /// 删除所有过期或已使用的票据
546 /// Removes all expired or used tickets
547 pub async fn cleanup_expired_tickets(&self) {
548 let mut tickets = self.tickets.write().await;
549 tickets.retain(|_, ticket| ticket.is_valid());
550 }
551
552 /// 获取活跃客户端列表 | Get active clients list
553 ///
554 /// # 参数 | Parameters
555 /// * `login_id` - 用户登录 ID | User login ID
556 ///
557 /// # 返回 | Returns
558 /// 客户端 URL 列表 | List of client URLs
559 pub async fn get_active_clients(&self, login_id: &str) -> Vec<String> {
560 let sessions = self.sessions.read().await;
561 sessions.get(login_id)
562 .map(|s| s.clients.clone())
563 .unwrap_or_default()
564 }
565}
566
567/// SSO 客户端 | SSO Client
568///
569/// 每个应用作为 SSO 客户端,处理本地会话和票据验证
570/// Each application acts as SSO Client, handling local sessions and ticket validation
571pub struct SsoClient {
572 /// Token 管理器 | Token manager
573 manager: Arc<SaTokenManager>,
574 /// SSO 服务端 URL | SSO Server URL
575 server_url: String,
576 /// 当前服务 URL | Current service URL
577 service_url: String,
578 /// 登出回调函数 | Logout callback function
579 logout_callback: Option<LogoutCallback>,
580}
581
582impl SsoClient {
583 /// 创建新的 SSO 客户端 | Create a new SSO Client
584 ///
585 /// # 参数 | Parameters
586 /// * `manager` - SaTokenManager 实例 | SaTokenManager instance
587 /// * `server_url` - SSO 服务端 URL | SSO Server URL
588 /// * `service_url` - 当前服务 URL | Current service URL
589 pub fn new(
590 manager: Arc<SaTokenManager>,
591 server_url: String,
592 service_url: String,
593 ) -> Self {
594 Self {
595 manager,
596 server_url,
597 service_url,
598 logout_callback: None,
599 }
600 }
601
602 /// 设置登出回调函数 | Set logout callback
603 ///
604 /// # 参数 | Parameters
605 /// * `callback` - 登出时执行的回调函数 | Callback function to execute on logout
606 pub fn with_logout_callback<F>(mut self, callback: F) -> Self
607 where
608 F: Fn(&str) -> bool + Send + Sync + 'static,
609 {
610 self.logout_callback = Some(Arc::new(callback));
611 self
612 }
613
614 /// 生成登录 URL | Generate login URL
615 ///
616 /// # 返回 | Returns
617 /// SSO 服务端登录 URL,包含当前服务的回调地址
618 /// SSO Server login URL with current service callback
619 pub fn get_login_url(&self) -> String {
620 format!("{}?service={}", self.server_url, urlencoding::encode(&self.service_url))
621 }
622
623 /// 生成登出 URL | Generate logout URL
624 ///
625 /// # 返回 | Returns
626 /// SSO 服务端登出 URL,包含当前服务的回调地址
627 /// SSO Server logout URL with current service callback
628 pub fn get_logout_url(&self) -> String {
629 format!("{}/logout?service={}", self.server_url, urlencoding::encode(&self.service_url))
630 }
631
632 /// 检查本地是否已登录 | Check if locally logged in
633 ///
634 /// # 参数 | Parameters
635 /// * `login_id` - 用户登录 ID | User login ID
636 ///
637 /// # 返回 | Returns
638 /// 是否已登录 | Whether logged in
639 pub async fn check_local_login(&self, login_id: &str) -> bool {
640 // 检查 SSO 客户端类型的登录
641 let key = self.manager.config.make_key(
642 "login:token:",
643 &self.manager.account_ns("sso_client", login_id),
644 );
645 match self.manager.storage.get(&key).await {
646 Ok(Some(_)) => true,
647 _ => {
648 // 兼容旧的无类型登录
649 let key_default = self.manager.config.make_key(
650 "login:token:",
651 &self.manager.account_ns("default", login_id),
652 );
653 matches!(self.manager.storage.get(&key_default).await, Ok(Some(_)))
654 }
655 }
656 }
657
658 /// 处理票据(验证票据合法性)| Process ticket (validate ticket)
659 ///
660 /// # 参数 | Parameters
661 /// * `ticket` - 票据 ID | Ticket ID
662 /// * `service` - 服务 URL | Service URL
663 ///
664 /// # 返回 | Returns
665 /// 处理后的票据信息 | Processed ticket info
666 ///
667 /// # 错误 | Errors
668 /// * `ServiceMismatch` - 服务 URL 不匹配 | Service URL mismatch
669 pub async fn process_ticket(&self, ticket: &str, service: &str) -> SaTokenResult<String> {
670 // 验证服务 URL 是否匹配
671 if service != self.service_url {
672 return Err(SaTokenError::ServiceMismatch);
673 }
674
675 Ok(ticket.to_string())
676 }
677
678 /// 通过票据登录(客户端本地登录)| Login by ticket (client-side local login)
679 ///
680 /// # 参数 | Parameters
681 /// * `login_id` - 用户登录 ID | User login ID
682 ///
683 /// # 返回 | Returns
684 /// 生成的本地 Token | Generated local token
685 pub async fn login_by_ticket(&self, login_id: String) -> SaTokenResult<String> {
686 // 使用 login_with_options 创建客户端 Token,标记为 SSO 客户端登录
687 let token = self.manager.login_with_options(
688 &login_id,
689 Some("sso_client".to_string()), // 标记为 SSO 客户端
690 None,
691 Some(serde_json::json!({
692 "sso_client": true,
693 "service_url": self.service_url.clone()
694 })),
695 None,
696 None,
697 ).await?;
698 Ok(token.to_string())
699 }
700
701 /// 处理登出(客户端)| Handle logout (client-side)
702 ///
703 /// # 参数 | Parameters
704 /// * `login_id` - 用户登录 ID | User login ID
705 pub async fn handle_logout(&self, login_id: &str) -> SaTokenResult<()> {
706 // 1. 执行登出回调 | Execute logout callback
707 if let Some(callback) = &self.logout_callback {
708 callback(login_id);
709 }
710
711 // 2. 登出 SSO 客户端类型的 Token | Logout SSO client token
712 let sso_client_key = self.manager.config.make_key(
713 "login:token:",
714 &self.manager.account_ns("sso_client", login_id),
715 );
716 let _ = self.manager.storage.delete(&sso_client_key).await;
717
718 // 3. 登出默认类型的 Token(兼容)| Logout default token (compatibility)
719 self.manager.logout_by_login_id(login_id).await?;
720
721 Ok(())
722 }
723
724 /// 获取 SSO 服务端 URL | Get SSO Server URL
725 pub fn server_url(&self) -> &str {
726 &self.server_url
727 }
728
729 /// 获取当前服务 URL | Get current service URL
730 pub fn service_url(&self) -> &str {
731 &self.service_url
732 }
733}
734
735#[derive(Debug, Clone, Serialize, Deserialize)]
736pub struct SsoConfig {
737 pub server_url: String,
738 pub ticket_timeout: i64,
739 pub allow_cross_domain: bool,
740 pub allowed_origins: Vec<String>,
741}
742
743impl Default for SsoConfig {
744 fn default() -> Self {
745 Self {
746 server_url: "http://localhost:8080/sso".to_string(),
747 ticket_timeout: 300,
748 allow_cross_domain: true,
749 allowed_origins: vec!["*".to_string()],
750 }
751 }
752}
753
754impl SsoConfig {
755 pub fn builder() -> SsoConfigBuilder {
756 SsoConfigBuilder::default()
757 }
758}
759
760#[derive(Default)]
761pub struct SsoConfigBuilder {
762 config: SsoConfig,
763}
764
765impl SsoConfigBuilder {
766 pub fn server_url(mut self, url: impl Into<String>) -> Self {
767 self.config.server_url = url.into();
768 self
769 }
770
771 pub fn ticket_timeout(mut self, timeout: i64) -> Self {
772 self.config.ticket_timeout = timeout;
773 self
774 }
775
776 pub fn allow_cross_domain(mut self, allow: bool) -> Self {
777 self.config.allow_cross_domain = allow;
778 self
779 }
780
781 pub fn allowed_origins(mut self, origins: Vec<String>) -> Self {
782 self.config.allowed_origins = origins;
783 self
784 }
785
786 pub fn add_allowed_origin(mut self, origin: String) -> Self {
787 if self.config.allowed_origins == vec!["*".to_string()] {
788 self.config.allowed_origins = vec![origin];
789 } else {
790 self.config.allowed_origins.push(origin);
791 }
792 self
793 }
794
795 pub fn build(self) -> SsoConfig {
796 self.config
797 }
798}
799
800pub struct SsoManager {
801 server: Option<Arc<SsoServer>>,
802 client: Option<Arc<SsoClient>>,
803 config: SsoConfig,
804}
805
806impl SsoManager {
807 pub fn new(config: SsoConfig) -> Self {
808 Self {
809 server: None,
810 client: None,
811 config,
812 }
813 }
814
815 pub fn with_server(mut self, server: Arc<SsoServer>) -> Self {
816 self.server = Some(server);
817 self
818 }
819
820 pub fn with_client(mut self, client: Arc<SsoClient>) -> Self {
821 self.client = Some(client);
822 self
823 }
824
825 pub fn server(&self) -> Option<&Arc<SsoServer>> {
826 self.server.as_ref()
827 }
828
829 pub fn client(&self) -> Option<&Arc<SsoClient>> {
830 self.client.as_ref()
831 }
832
833 pub fn config(&self) -> &SsoConfig {
834 &self.config
835 }
836
837 pub fn is_allowed_origin(&self, origin: &str) -> bool {
838 if !self.config.allow_cross_domain {
839 return false;
840 }
841
842 self.config.allowed_origins.contains(&"*".to_string()) ||
843 self.config.allowed_origins.contains(&origin.to_string())
844 }
845}
846