sa-token-rust 0.1.15

A powerful Rust authentication and authorization framework
Documentation
//! # SSO 单点登录示例 | SSO Single Sign-On Example
//!
//! 本示例演示 sa-token-rust 的完整 SSO 功能
//! This example demonstrates the complete SSO functionality of sa-token-rust
//!
//! ## 示例场景 | Example Scenarios
//!
//! 1. 用户在应用1登录 | User logs in at App1
//! 2. 用户访问应用2(SSO 生效)| User accesses App2 (SSO in action)
//! 3. 用户访问应用3 | User accesses App3
//! 4. 检查 SSO 会话 | Check SSO session
//! 5. 统一登出 | Unified logout
//! 6. 票据过期测试 | Ticket expiration test
//! 7. 跨域支持 | Cross-domain support
//! 8. URL 生成 | URL generation
//! 9. 服务匹配保护 | Service mismatch protection
//! 10. 清理过期票据 | Cleanup expired tickets

use std::sync::Arc;
use sa_token_core::{
    SaTokenConfig, SsoServer, SsoClient, SsoManager, SsoConfig,
};
use sa_token_storage_memory::MemoryStorage;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    println!("=== sa-token SSO Example ===\n");

    // ========================================
    // 初始化 sa-token | Initialize sa-token
    // ========================================
    let storage = Arc::new(MemoryStorage::new());
    
    // 创建 SaTokenManager | Create SaTokenManager
    let manager = SaTokenConfig::builder()
        .storage(storage.clone())
        .timeout(7200)  // 2小时 Token 超时 | 2-hour Token timeout
        .build();

    // ========================================
    // 步骤 1: 创建 SSO 服务端 | Step 1: Create SSO Server
    // ========================================
    println!("Step 1: Create SSO Server");
    let manager = Arc::new(manager);
    
    // 创建 SSO 服务端,设置 5 分钟票据超时 | Create SSO Server with 5-minute ticket timeout
    let sso_server = Arc::new(
        SsoServer::new(manager.clone())
            .with_ticket_timeout(300)  // 300秒 = 5分钟 | 300s = 5min
    );
    println!("SSO Server created with 5-minute ticket timeout\n");

    // ========================================
    // 步骤 2: 创建 SSO 客户端 | Step 2: Create SSO Clients
    // ========================================
    println!("Step 2: Create SSO Clients");
    // 客户端 1 - 应用1 | Client 1 - App1
    let client1 = Arc::new(SsoClient::new(
        manager.clone(),
        "http://sso.example.com/auth".to_string(),  // SSO 服务端地址 | SSO Server URL
        "http://app1.example.com".to_string(),      // 当前应用地址 | Current app URL
    ));
    
    // 客户端 2 - 应用2 | Client 2 - App2
    let client2 = Arc::new(SsoClient::new(
        manager.clone(),
        "http://sso.example.com/auth".to_string(),
        "http://app2.example.com".to_string(),
    ));
    
    // 客户端 3 - 应用3 | Client 3 - App3
    let client3 = Arc::new(SsoClient::new(
        manager.clone(),
        "http://sso.example.com/auth".to_string(),
        "http://app3.example.com".to_string(),
    ));
    println!("Created 3 SSO clients\n");

    // ========================================
    // 步骤 3: SSO 配置 | Step 3: SSO Config
    // ========================================
    println!("Step 3: SSO Config");
    // 使用 Builder 模式配置 SSO | Configure SSO using Builder pattern
    let sso_config = SsoConfig::builder()
        .server_url("http://sso.example.com/auth")  // SSO 服务端 URL | SSO Server URL
        .ticket_timeout(300)                         // 票据超时时间 | Ticket timeout
        .allow_cross_domain(true)                    // 允许跨域 | Allow cross-domain
        .add_allowed_origin("http://app1.example.com".to_string())  // 白名单 | Whitelist
        .add_allowed_origin("http://app2.example.com".to_string())
        .add_allowed_origin("http://app3.example.com".to_string())
        .build();
    
    // 创建 SSO 管理器 | Create SSO Manager
    let sso_manager = SsoManager::new(sso_config)
        .with_server(sso_server.clone())
        .with_client(client1.clone());
    
    println!("SSO Manager configured\n");
    println!("{}", "=".repeat(60));

    // ========================================
    // 场景 1: 用户在应用1登录 | Scenario 1: User logs in at App1
    // ========================================
    println!("\nScenario 1: User logs in at App1");
    println!("{}", "-".repeat(60));
    
    let user_id = "user_123";
    
    // 1. 用户在 SSO Server 登录 | User logs in at SSO Server
    let ticket1 = sso_server.login(
        user_id.to_string(),
        "http://app1.example.com".to_string(),
    ).await?;
    
    println!("Generated ticket for App1:");
    println!("  Ticket ID: {}", ticket1.ticket_id);
    println!("  Service: {}", ticket1.service);
    println!("  Login ID: {}", ticket1.login_id);
    println!("  Expires: {}", ticket1.expire_time);

    // 2. 验证票据 | Validate ticket
    let login_id1 = sso_server.validate_ticket(
        &ticket1.ticket_id,
        "http://app1.example.com",
    ).await?;
    
    println!("Ticket validated successfully: {}", login_id1);
    
    // 3. 创建本地会话 | Create local session
    let token1 = client1.login_by_ticket(login_id1.clone()).await?;
    println!("App1 local session created: {}", token1);

    // ========================================
    // 场景 2: 用户访问应用2(SSO 生效)| Scenario 2: User accesses App2 (SSO in action)
    // ========================================
    println!("\nScenario 2: User accesses App2 (SSO in action)");
    println!("{}", "-".repeat(60));
    
    // 检查用户是否已登录 SSO Server | Check if user is already logged in SSO Server
    let is_logged_in = sso_server.is_logged_in(user_id).await;
    println!("User already logged in SSO Server: {}", is_logged_in);
    
    // 直接创建票据,无需再次登录!| Directly create ticket, no re-login required!
    let ticket2 = sso_server.create_ticket(
        user_id.to_string(),
        "http://app2.example.com".to_string(),
    ).await?;
    
    println!("Generated ticket for App2:");
    println!("  Ticket ID: {}", ticket2.ticket_id);
    
    // 验证票据并创建本地会话 | Validate ticket and create local session
    let login_id2 = sso_server.validate_ticket(
        &ticket2.ticket_id,
        "http://app2.example.com",
    ).await?;
    
    let token2 = client2.login_by_ticket(login_id2.clone()).await?;
    println!("App2 local session created: {}", token2);

    // ========================================
    // 场景 3: 用户访问应用3 | Scenario 3: User accesses App3
    // ========================================
    println!("\nScenario 3: User accesses App3");
    println!("{}", "-".repeat(60));
    
    let ticket3 = sso_server.create_ticket(
        user_id.to_string(),
        "http://app3.example.com".to_string(),
    ).await?;
    
    let login_id3 = sso_server.validate_ticket(
        &ticket3.ticket_id,
        "http://app3.example.com",
    ).await?;
    
    let token3 = client3.login_by_ticket(login_id3.clone()).await?;
    println!("App3 local session created: {}", token3);

    println!("\nScenario 4: Check SSO Session");
    println!("{}", "-".repeat(60));
    
    let active_clients = sso_server.get_active_clients(user_id).await;
    println!("User logged into {} applications:", active_clients.len());
    for (i, client) in active_clients.iter().enumerate() {
        println!("  {}. {}", i + 1, client);
    }

    let session = sso_server.get_session(user_id).await;
    if let Some(s) = session {
        println!("SSO Session details:");
        println!("  Login ID: {}", s.login_id);
        println!("  Active clients: {}", s.clients.len());
        println!("  Created: {}", s.create_time);
        println!("  Last active: {}", s.last_active_time);
    }

    println!("\nScenario 5: Unified Logout");
    println!("{}", "-".repeat(60));
    
    let logged_out_clients = sso_server.logout(user_id).await?;
    println!("User logged out from SSO Server");
    println!("Notifying {} clients to logout:", logged_out_clients.len());
    for (i, client_url) in logged_out_clients.iter().enumerate() {
        println!("  {}. {} - clearing local session", i + 1, client_url);
    }

    client1.handle_logout(user_id).await?;
    client2.handle_logout(user_id).await?;
    client3.handle_logout(user_id).await?;
    println!("All local sessions cleared");

    let is_still_logged_in = sso_server.check_session(user_id).await;
    println!("User still logged in: {}", is_still_logged_in);

    println!("\nScenario 6: Ticket Expiration");
    println!("{}", "-".repeat(60));
    
    let expired_ticket = sso_server.create_ticket(
        "user_456".to_string(),
        "http://app1.example.com".to_string(),
    ).await?;
    
    println!("Created ticket: {}", expired_ticket.ticket_id);
    println!("Ticket is valid: {}", expired_ticket.is_valid());
    
    match sso_server.validate_ticket(
        &expired_ticket.ticket_id,
        "http://app1.example.com",
    ).await {
        Ok(id) => println!("Ticket validated: {}", id),
        Err(e) => println!("Ticket validation failed: {}", e),
    }
    
    match sso_server.validate_ticket(
        &expired_ticket.ticket_id,
        "http://app1.example.com",
    ).await {
        Ok(id) => println!("Second validation: {}", id),
        Err(e) => println!("Second validation failed (ticket already used): {}", e),
    }

    println!("\nScenario 7: Cross-Domain Support");
    println!("{}", "-".repeat(60));
    
    let origins = vec![
        "http://app1.example.com",
        "http://app2.example.com",
        "http://evil.com",
        "http://app3.example.com",
    ];
    
    for origin in origins {
        let allowed = sso_manager.is_allowed_origin(origin);
        println!("Origin '{}': {}", origin, if allowed { "✓ Allowed" } else { "✗ Denied" });
    }

    println!("\nScenario 8: Client URL Generation");
    println!("{}", "-".repeat(60));
    
    println!("Client1 Login URL: {}", client1.get_login_url());
    println!("Client1 Logout URL: {}", client1.get_logout_url());
    println!("Client2 Login URL: {}", client2.get_login_url());
    println!("Client3 Login URL: {}", client3.get_login_url());

    println!("\nScenario 9: Service Mismatch Protection");
    println!("{}", "-".repeat(60));
    
    let ticket_app1 = sso_server.create_ticket(
        "user_789".to_string(),
        "http://app1.example.com".to_string(),
    ).await?;
    
    match sso_server.validate_ticket(
        &ticket_app1.ticket_id,
        "http://app2.example.com",
    ).await {
        Ok(_) => println!("Validation succeeded (unexpected)"),
        Err(e) => println!("Service mismatch detected: {}", e),
    }

    println!("\nScenario 10: Cleanup Expired Tickets");
    println!("{}", "-".repeat(60));
    
    for i in 1..=5 {
        let _ = sso_server.create_ticket(
            format!("temp_user_{}", i),
            "http://app1.example.com".to_string(),
        ).await;
    }
    
    println!("Created 5 temporary tickets");
    sso_server.cleanup_expired_tickets().await;
    println!("Cleanup completed - all valid tickets remain");

    println!("\n{}", "=".repeat(60));
    println!("SSO Example completed successfully!");
    println!("Key Features Demonstrated:");
    println!("  ✓ Single Sign-On across multiple applications");
    println!("  ✓ Ticket-based authentication");
    println!("  ✓ Unified logout");
    println!("  ✓ Cross-domain support with origin validation");
    println!("  ✓ Ticket expiration and reuse prevention");
    println!("  ✓ Service mismatch protection");
    println!("  ✓ Session management");

    Ok(())
}