use jsonwebtoken::{decode, DecodingKey, Validation};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Claims {
#[serde(rename = "sub")]
pub login_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub exp: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub iat: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub jti: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub iss: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub aud: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub login_type: Option<String>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub extra: HashMap<String, serde_json::Value>,
}
impl Claims {
pub fn user_id(&self) -> Option<i64> {
self.login_id.parse::<i64>().ok()
}
pub fn tenant_id(&self) -> Option<i64> {
self.extra
.get("tenant_id")
.and_then(|v| v.as_str())
.and_then(|s| s.parse::<i64>().ok())
}
pub fn username(&self) -> String {
self.extra
.get("username")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string()
}
pub fn token_type(&self) -> String {
self.extra
.get("token_type")
.and_then(|v| v.as_str())
.unwrap_or("access")
.to_string()
}
}
pub struct JwtService {
decoding_key: DecodingKey,
}
impl JwtService {
pub fn from_env() -> Result<Self, String> {
let secret = std::env::var("APP__JWT__SECRET")
.map_err(|_| "环境变量 APP__JWT__SECRET 未设置".to_string())?;
Ok(Self {
decoding_key: DecodingKey::from_secret(secret.as_bytes()),
})
}
pub fn new(secret: &str) -> Self {
Self {
decoding_key: DecodingKey::from_secret(secret.as_bytes()),
}
}
pub fn verify_token(&self, token: &str) -> Result<Claims, String> {
let token = token.trim();
let token = if token.starts_with("Bearer ") {
&token[7..]
} else {
token
};
let mut validation = Validation::default();
validation.leeway = 300;
let token_data = decode::<Claims>(token, &self.decoding_key, &validation)
.map_err(|e| format!("JWT 验证失败: {}", e))?;
Ok(token_data.claims)
}
}