use async_trait::async_trait;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use std::sync::RwLock;
use std::time::Duration;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum SaTokenEventType {
Login,
Logout,
KickOut,
RenewTimeout,
Replaced,
Banned,
Unbanned,
OpenSafe,
CloseSafe,
SafeVerify,
GrantChanged,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum DispatchMode {
#[default]
Sequential,
Concurrent,
Detached,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EventBusConfig {
pub dispatch_mode: DispatchMode,
pub listener_timeout: Option<Duration>,
}
impl Default for EventBusConfig {
fn default() -> Self {
Self {
dispatch_mode: DispatchMode::Sequential,
listener_timeout: Some(Duration::from_secs(5)),
}
}
}
impl EventBusConfig {
pub fn no_timeout() -> Self {
Self {
listener_timeout: None,
..Default::default()
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SaTokenEvent {
pub event_type: SaTokenEventType,
pub login_id: String,
pub token: String,
pub login_type: String,
pub timestamp: DateTime<Utc>,
pub extra: Option<serde_json::Value>,
}
impl SaTokenEvent {
pub fn login(login_id: impl Into<String>, token: impl Into<String>) -> Self {
Self {
event_type: SaTokenEventType::Login,
login_id: login_id.into(),
token: token.into(),
login_type: "default".to_string(),
timestamp: Utc::now(),
extra: None,
}
}
pub fn logout(login_id: impl Into<String>, token: impl Into<String>) -> Self {
Self {
event_type: SaTokenEventType::Logout,
login_id: login_id.into(),
token: token.into(),
login_type: "default".to_string(),
timestamp: Utc::now(),
extra: None,
}
}
pub fn kick_out(login_id: impl Into<String>, token: impl Into<String>) -> Self {
Self {
event_type: SaTokenEventType::KickOut,
login_id: login_id.into(),
token: token.into(),
login_type: "default".to_string(),
timestamp: Utc::now(),
extra: None,
}
}
pub fn renew_timeout(
login_id: impl Into<String>,
token: impl Into<String>,
timeout_seconds: i64,
) -> Self {
Self {
event_type: SaTokenEventType::RenewTimeout,
login_id: login_id.into(),
token: token.into(),
login_type: "default".to_string(),
timestamp: Utc::now(),
extra: Some(serde_json::json!({ "timeout_seconds": timeout_seconds })),
}
}
pub fn replaced(login_id: impl Into<String>, token: impl Into<String>) -> Self {
Self {
event_type: SaTokenEventType::Replaced,
login_id: login_id.into(),
token: token.into(),
login_type: "default".to_string(),
timestamp: Utc::now(),
extra: None,
}
}
pub fn banned(login_id: impl Into<String>, service: impl Into<String>, level: i32) -> Self {
Self {
event_type: SaTokenEventType::Banned,
login_id: login_id.into(),
token: String::new(),
login_type: "default".to_string(),
timestamp: Utc::now(),
extra: Some(serde_json::json!({ "service": service.into(), "level": level })),
}
}
pub fn unbanned(login_id: impl Into<String>, service: impl Into<String>) -> Self {
Self {
event_type: SaTokenEventType::Unbanned,
login_id: login_id.into(),
token: String::new(),
login_type: "default".to_string(),
timestamp: Utc::now(),
extra: Some(serde_json::json!({ "service": service.into() })),
}
}
pub fn open_safe(token: impl Into<String>, service: impl Into<String>) -> Self {
let svc = service.into();
Self {
event_type: SaTokenEventType::OpenSafe,
login_id: String::new(),
token: token.into(),
login_type: "default".to_string(),
timestamp: Utc::now(),
extra: Some(serde_json::json!({ "service": svc })),
}
}
pub fn close_safe(token: impl Into<String>, service: impl Into<String>) -> Self {
let svc = service.into();
Self {
event_type: SaTokenEventType::CloseSafe,
login_id: String::new(),
token: token.into(),
login_type: "default".to_string(),
timestamp: Utc::now(),
extra: Some(serde_json::json!({ "service": svc })),
}
}
pub fn safe_verify(token: impl Into<String>, service: impl Into<String>) -> Self {
let svc = service.into();
Self {
event_type: SaTokenEventType::SafeVerify,
login_id: String::new(),
token: token.into(),
login_type: "default".to_string(),
timestamp: Utc::now(),
extra: Some(serde_json::json!({ "service": svc })),
}
}
pub fn grant_changed(login_id: impl Into<String>, login_type: impl Into<String>) -> Self {
Self {
event_type: SaTokenEventType::GrantChanged,
login_id: login_id.into(),
token: String::new(),
login_type: login_type.into(),
timestamp: Utc::now(),
extra: None,
}
}
pub fn with_login_type(mut self, login_type: impl Into<String>) -> Self {
self.login_type = login_type.into();
self
}
pub fn with_extra(mut self, extra: serde_json::Value) -> Self {
self.extra = Some(extra);
self
}
}
#[async_trait]
pub trait SaTokenListener: Send + Sync {
async fn on_login(&self, login_id: &str, token: &str, login_type: &str) {
let _ = (login_id, token, login_type);
}
async fn on_logout(&self, login_id: &str, token: &str, login_type: &str) {
let _ = (login_id, token, login_type);
}
async fn on_kick_out(&self, login_id: &str, token: &str, login_type: &str) {
let _ = (login_id, token, login_type);
}
async fn on_renew_timeout(
&self,
login_id: &str,
token: &str,
login_type: &str,
timeout_seconds: i64,
) {
let _ = (login_id, token, login_type, timeout_seconds);
}
async fn on_replaced(&self, login_id: &str, token: &str, login_type: &str) {
let _ = (login_id, token, login_type);
}
async fn on_banned(&self, login_id: &str, login_type: &str) {
let _ = (login_id, login_type);
}
async fn on_unbanned(&self, login_id: &str, service: &str, login_type: &str) {
let _ = (login_id, service, login_type);
}
async fn on_open_safe(&self, token: &str, service: &str) {
let _ = (token, service);
}
async fn on_close_safe(&self, token: &str, service: &str) {
let _ = (token, service);
}
async fn on_safe_verify(&self, token: &str, service: &str) {
let _ = (token, service);
}
async fn on_grant_changed(&self, login_id: &str, login_type: &str) {
let _ = (login_id, login_type);
}
async fn on_event(&self, event: &SaTokenEvent) {
let _ = event;
}
}
type ListenerList = Arc<Vec<Arc<dyn SaTokenListener>>>;
#[derive(Clone)]
pub struct SaTokenEventBus {
listeners: Arc<RwLock<ListenerList>>,
config: EventBusConfig,
}
impl std::fmt::Debug for SaTokenEventBus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("SaTokenEventBus { .. }")
}
}
impl SaTokenEventBus {
pub fn new() -> Self {
Self::with_config(EventBusConfig::default())
}
pub fn with_config(config: EventBusConfig) -> Self {
Self {
listeners: Arc::new(RwLock::new(Arc::new(Vec::new()))),
config,
}
}
pub fn config(&self) -> &EventBusConfig {
&self.config
}
fn read_guard(&self) -> std::sync::RwLockReadGuard<'_, Arc<Vec<Arc<dyn SaTokenListener>>>> {
self.listeners.read().unwrap_or_else(|poisoned| {
tracing::warn!("EventBus RwLock poisoned, recovering");
poisoned.into_inner()
})
}
fn write_guard(&self) -> std::sync::RwLockWriteGuard<'_, Arc<Vec<Arc<dyn SaTokenListener>>>> {
self.listeners.write().unwrap_or_else(|poisoned| {
tracing::warn!("EventBus RwLock poisoned during write, recovering");
poisoned.into_inner()
})
}
fn snapshot(&self) -> Arc<Vec<Arc<dyn SaTokenListener>>> {
Arc::clone(&*self.read_guard())
}
pub fn register(&self, listener: Arc<dyn SaTokenListener>) {
let mut guard = self.write_guard();
let mut next = Vec::with_capacity(guard.len() + 1);
next.extend(guard.iter().cloned());
next.push(listener);
*guard = Arc::new(next);
}
pub async fn register_async(&self, listener: Arc<dyn SaTokenListener>) {
self.register(listener);
}
pub fn clear(&self) {
*self.write_guard() = Arc::new(Vec::new());
}
pub fn listener_count(&self) -> usize {
self.read_guard().len()
}
pub async fn publish(&self, event: SaTokenEvent) {
match self.config.dispatch_mode {
DispatchMode::Sequential => {
self.dispatch_sequential(event).await;
}
DispatchMode::Concurrent => {
self.dispatch_concurrent(event).await;
}
DispatchMode::Detached => {
let bus = self.clone();
tokio::spawn(async move {
bus.dispatch_sequential(event).await;
});
}
}
}
async fn dispatch_sequential(&self, event: SaTokenEvent) {
let listeners = self.snapshot();
let timeout = self.config.listener_timeout;
for listener in listeners.iter() {
Self::invoke_listener_safe(Arc::clone(listener), &event, timeout).await;
}
}
async fn dispatch_concurrent(&self, event: SaTokenEvent) {
let listeners = self.snapshot();
let timeout = self.config.listener_timeout;
let mut handles = Vec::with_capacity(listeners.len());
for listener in listeners.iter() {
let listener = Arc::clone(listener);
let ev = event.clone();
let handle = tokio::spawn(async move {
Self::invoke_listener_safe(listener, &ev, timeout).await;
});
handles.push(handle);
}
for (idx, handle) in handles.into_iter().enumerate() {
if let Err(e) = handle.await {
if e.is_panic() {
tracing::warn!(
listener_idx = idx,
"listener task panicked in concurrent mode"
);
} else {
tracing::warn!(listener_idx = idx, "listener task cancelled");
}
}
}
}
async fn invoke_listener_safe(
listener: Arc<dyn SaTokenListener>,
event: &SaTokenEvent,
timeout: Option<Duration>,
) {
let event_owned = event.clone();
let handle = tokio::spawn(async move {
let fut = Self::dispatch_to_listener(&listener, &event_owned);
match timeout {
Some(d) => match tokio::time::timeout(d, fut).await {
Ok(()) => Ok(()),
Err(_elapsed) => Err("timeout"),
},
None => {
fut.await;
Ok(())
}
}
});
match handle.await {
Ok(Ok(())) => {}
Ok(Err("timeout")) => {
tracing::warn!(
event_type = ?event.event_type,
"listener timed out during event dispatch"
);
}
Ok(Err(_)) => {}
Err(e) if e.is_panic() => {
tracing::warn!(
event_type = ?event.event_type,
"listener panicked during event dispatch"
);
}
Err(e) => {
tracing::warn!("listener task cancelled: {:?}", e);
}
}
}
async fn dispatch_to_listener(listener: &Arc<dyn SaTokenListener>, event: &SaTokenEvent) {
listener.on_event(event).await;
match event.event_type {
SaTokenEventType::Login => {
listener
.on_login(&event.login_id, &event.token, &event.login_type)
.await;
}
SaTokenEventType::Logout => {
listener
.on_logout(&event.login_id, &event.token, &event.login_type)
.await;
}
SaTokenEventType::KickOut => {
listener
.on_kick_out(&event.login_id, &event.token, &event.login_type)
.await;
}
SaTokenEventType::RenewTimeout => {
let timeout_seconds = event
.extra
.as_ref()
.and_then(|v| v.get("timeout_seconds"))
.and_then(|v| v.as_i64())
.unwrap_or(0);
listener
.on_renew_timeout(
&event.login_id,
&event.token,
&event.login_type,
timeout_seconds,
)
.await;
}
SaTokenEventType::Replaced => {
listener
.on_replaced(&event.login_id, &event.token, &event.login_type)
.await;
}
SaTokenEventType::Banned => {
listener.on_banned(&event.login_id, &event.login_type).await;
}
SaTokenEventType::Unbanned => {
let service = event
.extra
.as_ref()
.and_then(|v| v.get("service"))
.and_then(|v| v.as_str())
.unwrap_or("");
listener
.on_unbanned(&event.login_id, service, &event.login_type)
.await;
}
SaTokenEventType::OpenSafe => {
let service = event
.extra
.as_ref()
.and_then(|v| v.get("service"))
.and_then(|v| v.as_str())
.unwrap_or(&event.login_type);
listener.on_open_safe(&event.token, service).await;
}
SaTokenEventType::CloseSafe => {
let service = event
.extra
.as_ref()
.and_then(|v| v.get("service"))
.and_then(|v| v.as_str())
.unwrap_or(&event.login_type);
listener.on_close_safe(&event.token, service).await;
}
SaTokenEventType::SafeVerify => {
let service = event
.extra
.as_ref()
.and_then(|v| v.get("service"))
.and_then(|v| v.as_str())
.unwrap_or("");
listener.on_safe_verify(&event.token, service).await;
}
SaTokenEventType::GrantChanged => {
listener
.on_grant_changed(&event.login_id, &event.login_type)
.await;
}
}
}
}
impl Default for SaTokenEventBus {
fn default() -> Self {
Self::new()
}
}
pub struct LoggingListener;
#[async_trait]
impl SaTokenListener for LoggingListener {
async fn on_login(&self, login_id: &str, token: &str, login_type: &str) {
tracing::info!(
login_id = %login_id,
token = %token,
login_type = %login_type,
"用户登录"
);
}
async fn on_logout(&self, login_id: &str, token: &str, login_type: &str) {
tracing::info!(
login_id = %login_id,
token = %token,
login_type = %login_type,
"用户登出"
);
}
async fn on_kick_out(&self, login_id: &str, token: &str, login_type: &str) {
tracing::warn!(
login_id = %login_id,
token = %token,
login_type = %login_type,
"用户被踢出下线"
);
}
async fn on_renew_timeout(
&self,
login_id: &str,
token: &str,
login_type: &str,
timeout_seconds: i64,
) {
tracing::debug!(
login_id = %login_id,
token = %token,
login_type = %login_type,
timeout_seconds = timeout_seconds,
"Token 续期"
);
}
async fn on_replaced(&self, login_id: &str, token: &str, login_type: &str) {
tracing::warn!(
login_id = %login_id,
token = %token,
login_type = %login_type,
"用户被顶下线"
);
}
async fn on_banned(&self, login_id: &str, login_type: &str) {
tracing::warn!(
login_id = %login_id,
login_type = %login_type,
"用户被封禁"
);
}
async fn on_unbanned(&self, login_id: &str, service: &str, login_type: &str) {
tracing::info!(
login_id = %login_id,
service = %service,
login_type = %login_type,
"用户被解封"
);
}
async fn on_safe_verify(&self, token: &str, service: &str) {
tracing::debug!(
token = %token,
service = %service,
"二级认证校验通过"
);
}
}
impl std::fmt::Debug for LoggingListener {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("LoggingListener { .. }")
}
}
#[cfg(test)]
mod tests {
use super::*;
struct TestListener {
login_count: Arc<RwLock<i32>>,
}
impl TestListener {
fn new() -> Self {
Self {
login_count: Arc::new(RwLock::new(0)),
}
}
}
#[async_trait]
impl SaTokenListener for TestListener {
async fn on_login(&self, _login_id: &str, _token: &str, _login_type: &str) {
let mut count = self.login_count.write().unwrap();
*count += 1;
}
}
#[tokio::test]
async fn test_event_bus() {
let bus = SaTokenEventBus::with_config(EventBusConfig::no_timeout());
let listener = Arc::new(TestListener::new());
let login_count = Arc::clone(&listener.login_count);
bus.register(listener);
let event = SaTokenEvent::login("user_123", "token_abc");
bus.publish(event).await;
let count = login_count.read().unwrap();
assert_eq!(*count, 1);
}
#[test]
fn test_event_creation() {
let event = SaTokenEvent::login("user_123", "token_abc");
assert_eq!(event.event_type, SaTokenEventType::Login);
assert_eq!(event.login_id, "user_123");
assert_eq!(event.token, "token_abc");
}
}