spring_sa_token/
lib.rs

1//! # spring-sa-token
2//!
3//! Automatic assembly for sa-token-rust.
4//!
5//! ## Storage Backend Selection
6//!
7//! When both `memory` and `with-spring-redis` features are enabled,
8//! `with-spring-redis` takes priority. This allows `--all-features` testing
9//! while maintaining predictable behavior.
10//!
11//! ## Quick Start
12//!
13//! Add to your `Cargo.toml`:
14//!
15//! ```toml
16//! [dependencies]
17//! spring-sa-token = { path = "../spring-sa-token" }  # Default: memory storage
18//! # Or reuse spring-redis connection (recommended for production):
19//! # spring-sa-token = { path = "../spring-sa-token", default-features = false, features = ["with-spring-redis", "with-web"] }
20//! ```
21//!
22//! Configure in `config/app.toml`:
23//!
24//! ```toml
25//! [sa-token]
26//! token_name = "Authorization"
27//! timeout = 86400
28//! auto_renew = true
29//! token_style = "uuid"
30//! is_concurrent = true
31//! ```
32//!
33//! Use in your application:
34//!
35//! ```rust,ignore
36//! use spring::{auto_config, App};
37//! use spring_web::{get, WebConfigurator, WebPlugin};
38//! use spring_sa_token::{SaTokenPlugin, LoginIdExtractor, StpUtil};
39//!
40//! #[auto_config(WebConfigurator)]
41//! #[tokio::main]
42//! async fn main() {
43//!     App::new()
44//!         .add_plugin(SaTokenPlugin)
45//!         .add_plugin(WebPlugin)
46//!         .run()
47//!         .await
48//! }
49//!
50//! #[get("/user/info")]
51//! async fn user_info(LoginIdExtractor(user_id): LoginIdExtractor) -> String {
52//!     format!("Current user: {}", user_id)
53//! }
54//! ```
55
56pub mod config;
57pub mod configurator;
58#[cfg(feature = "with-spring-redis")]
59pub mod storage;
60
61use crate::config::SaTokenConfig;
62use sa_token_adapter::SaStorage;
63use spring::app::AppBuilder;
64use spring::async_trait;
65use spring::config::ConfigRegistry;
66use spring::plugin::ComponentRegistry;
67use spring::plugin::{MutableComponentRegistry, Plugin};
68#[cfg(feature = "with-web")]
69use spring_web::LayerConfigurator;
70use std::sync::Arc;
71
72// ============================================================================
73// Re-exports: Users only need to import spring-sa-token
74// ============================================================================
75
76// Re-export entire crates for full access
77pub use sa_token_adapter;
78pub use sa_token_core;
79pub use sa_token_plugin_axum;
80
81// Re-export config types to root
82pub use crate::config::{CoreConfig, TokenStyle};
83
84// Re-export commonly used types to root for convenience
85pub use sa_token_core::{SaTokenManager, StpUtil};
86pub use sa_token_plugin_axum::{LoginIdExtractor, OptionalSaTokenExtractor, SaTokenExtractor};
87pub use sa_token_plugin_axum::{SaTokenLayer, SaTokenMiddleware, SaTokenState};
88
89// Re-export procedural macros from spring-macros (WebError compatible)
90pub use spring_macros::sa_check_login;
91pub use spring_macros::sa_check_permission;
92pub use spring_macros::sa_check_permissions_and;
93pub use spring_macros::sa_check_permissions_or;
94pub use spring_macros::sa_check_role;
95pub use spring_macros::sa_check_roles_and;
96pub use spring_macros::sa_check_roles_or;
97pub use spring_macros::sa_ignore;
98
99// Re-export storage implementations
100#[cfg(feature = "memory")]
101pub use sa_token_storage_memory::MemoryStorage;
102
103// Re-export configurator types
104pub use crate::configurator::{PathAuthBuilder, SaTokenAuthConfigurator, SaTokenConfigurator};
105
106// Re-export error types
107pub use sa_token_core::error::SaTokenError;
108use sa_token_core::PathAuthConfig;
109
110/// Sa-Token plugin for spring-rs
111///
112/// This plugin initializes the Sa-Token authentication system and registers
113/// `SaTokenState` as a component that can be injected into handlers.
114pub struct SaTokenPlugin;
115
116#[async_trait]
117impl Plugin for SaTokenPlugin {
118    async fn build(&self, app: &mut AppBuilder) {
119        let config = app
120            .get_config::<SaTokenConfig>()
121            .expect("sa-token plugin config load failed");
122
123        tracing::info!("Initializing Sa-Token plugin...");
124
125        let state = Self::create_state(app, config)
126            .await
127            .expect("sa-token state creation failed");
128
129        tracing::debug!(
130            "SaTokenState manager.config.token_name = {}",
131            state.manager.config.token_name
132        );
133
134        tracing::info!("Sa-Token plugin initialized successfully");
135
136        // Register SaTokenState as a component
137        app.add_component(state.clone());
138
139        // Automatically register SaTokenLayer as a router layer
140        // This middleware will extract tokens from requests and validate them
141        #[cfg(feature = "with-web")]
142        {
143            // Get path-based authentication configuration from component
144            if let Some(config) = app.get_component::<PathAuthConfig>() {
145                tracing::info!("Registering SaTokenLayer with path-based authentication");
146                app.add_router_layer(move |router| {
147                    router.layer(SaTokenLayer::with_path_auth(state.clone(), config.clone()))
148                });
149            } else {
150                tracing::info!("Registering SaTokenLayer as router middleware (no path config)");
151                app.add_router_layer(move |router| router.layer(SaTokenLayer::new(state.clone())));
152            }
153        }
154    }
155
156    fn name(&self) -> &str {
157        "spring_sa_token::SaTokenPlugin"
158    }
159
160    #[cfg(feature = "with-spring-redis")]
161    fn dependencies(&self) -> Vec<&str> {
162        vec!["spring_redis::RedisPlugin"]
163    }
164}
165
166impl SaTokenPlugin {
167    /// Create SaTokenState from configuration
168    ///
169    /// Uses SaTokenConfig::builder() from sa-token-core which supports most config fields
170    /// and automatically initializes StpUtil.
171    ///
172    /// Note: The following fields are not supported by the builder (using defaults):
173    /// - is_log, is_read_cookie, is_read_header, is_read_body
174    async fn create_state(app: &AppBuilder, config: SaTokenConfig) -> anyhow::Result<SaTokenState> {
175        // Configure storage based on features
176        let storage = Self::configure_storage(app, &config).await?;
177
178        tracing::debug!(
179            "Sa-Token config: token_name={}, timeout={}, auto_renew={}, is_concurrent={}, is_share={}",
180            config.token_name, config.timeout, config.auto_renew, config.is_concurrent, config.is_share
181        );
182
183        // Use SaTokenConfig::builder() from sa-token-core which supports more config fields
184        // and automatically initializes StpUtil on build()
185        let mut builder = sa_token_core::SaTokenConfig::builder()
186            .storage(storage)
187            .token_name(config.token_name)
188            .timeout(config.timeout)
189            .active_timeout(config.active_timeout)
190            .auto_renew(config.auto_renew)
191            .is_concurrent(config.is_concurrent)
192            .is_share(config.is_share)
193            .token_style(config.token_style.into())
194            .enable_nonce(config.enable_nonce)
195            .nonce_timeout(config.nonce_timeout)
196            .enable_refresh_token(config.enable_refresh_token)
197            .refresh_token_timeout(config.refresh_token_timeout);
198
199        // Set optional fields
200        if let Some(prefix) = config.token_prefix {
201            builder = builder.token_prefix(prefix);
202        }
203        if let Some(key) = config.jwt_secret_key {
204            builder = builder.jwt_secret_key(key);
205        }
206        if let Some(algorithm) = config.jwt_algorithm {
207            builder = builder.jwt_algorithm(algorithm);
208        }
209        if let Some(issuer) = config.jwt_issuer {
210            builder = builder.jwt_issuer(issuer);
211        }
212        if let Some(audience) = config.jwt_audience {
213            builder = builder.jwt_audience(audience);
214        }
215
216        // build() creates SaTokenManager and auto-initializes StpUtil
217        let manager = builder.build();
218
219        // Create SaTokenState from manager
220        Ok(SaTokenState::from_manager(manager))
221    }
222
223    /// Configure storage backend based on features and configuration
224    ///
225    /// Priority:
226    /// 1. spring-redis component (if with-spring-redis feature enabled)
227    /// 2. memory storage (if memory feature enabled)
228    ///
229    /// When both features are enabled, with-spring-redis takes priority.
230    #[allow(unused_variables)]
231    async fn configure_storage(
232        app: &AppBuilder,
233        config: &SaTokenConfig,
234    ) -> anyhow::Result<Arc<dyn SaStorage>> {
235        // Priority 1: Use spring-redis component if available
236        #[cfg(feature = "with-spring-redis")]
237        {
238            if let Some(redis) = app.get_component::<spring_redis::Redis>() {
239                tracing::info!("Using SpringRedisStorage (reusing spring-redis connection)");
240                let storage = storage::SpringRedisStorage::new(redis);
241                Ok(Arc::new(storage))
242            } else {
243                anyhow::bail!(
244                    "Feature 'with-spring-redis' is enabled but RedisPlugin is not added. \
245                     Please add RedisPlugin before SaTokenPlugin."
246                );
247            }
248        }
249
250        // Priority 2: Fall back to memory storage (only when with-spring-redis is not enabled)
251        #[cfg(all(feature = "memory", not(feature = "with-spring-redis")))]
252        {
253            tracing::info!("Using Memory storage");
254            Ok(Arc::new(MemoryStorage::new()))
255        }
256
257        // No storage available
258        #[cfg(not(any(feature = "memory", feature = "with-spring-redis")))]
259        {
260            anyhow::bail!(
261                "No storage backend available. Enable 'memory' or 'with-spring-redis' feature."
262            );
263        }
264    }
265}