#![cfg(feature = "server-http")]
use crate::app_system_error;
use crate::tina::constant::Constants;
use crate::tina::core::domain::login_user::LoginUser;
use crate::tina::data::json::JsonToString;
use crate::tina::data::AppResult;
use crate::tina::server::http::request_ext::RequestExt;
use crate::tina::util::client::ClientUtil;
use crate::tina::util::not_empty::INotEmpty;
use crate::tina::util::string::AsStr;
use chrono::Local;
use indexmap::IndexMap;
use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation};
use serde_json::{Number, Value};
use std::borrow::Cow;
use std::marker::PhantomData;
use std::time::Duration;
use tracing::warn;
use uuid::Uuid;
use super::cache::ICacheService;
#[cfg(feature = "redis")]
pub type TokenService = ITokenService<crate::tina::redis::cache::RedisCache>;
#[cfg(not(feature = "redis"))]
pub type TokenService = ITokenService<crate::tina::core::service::cache::NoCacheService>;
pub struct ITokenService<T: ICacheService> {
_phantom: PhantomData<T>,
}
impl<T: ICacheService> Default for ITokenService<T> {
fn default() -> Self {
Self::new()
}
}
impl<T: ICacheService> ITokenService<T> {
#[allow(unused)]
const MILLIS_SECOND: i64 = 1000;
#[allow(unused)]
const MILLIS_MINUTE: i64 = 60 * Self::MILLIS_SECOND;
#[allow(unused)]
const MILLIS_MINUTE_TEN: i64 = 20 * 60 * 1000;
pub fn new() -> Self {
Self {
_phantom: Default::default(),
}
}
#[instrument]
pub async fn get_login_user<Req: RequestExt + ?Sized>(req: &Req) -> AppResult<Option<LoginUser>> {
let token = Self::get_token(req)?;
if token.not_empty() {
return match get_login_user_by_token::<Req, T>(req, token.as_str()).await {
Ok(user) => Ok(user),
Err(err) => {
warn!("获取用户身份信息异常: {}", err);
Ok(None)
}
};
}
Ok(None)
}
#[instrument]
pub async fn set_login_user<Req: RequestExt + ?Sized>(req: &Req, login_user: &mut LoginUser) -> AppResult<()> {
if login_user.token.not_empty() {
Self::refresh_token(req, login_user).await?;
}
Ok(())
}
#[instrument]
pub async fn del_login_user<Req: RequestExt + ?Sized>(req: &Req, token: &str) -> AppResult<()> {
if token.not_empty() {
let user_key = get_token_key(token);
let application = req.get_application()?;
let service = application.get_cache_service::<T>()?;
service.delete_object(&application, user_key.as_str()).await?;
}
Ok(())
}
#[instrument]
pub async fn create_token<Req: RequestExt + ?Sized>(req: &Req, login_user: &mut LoginUser) -> AppResult<String> {
let token = Uuid::new_v4().to_string();
login_user.token = Some(token.clone());
Self::set_user_agent(req, login_user)?;
Self::refresh_token(req, login_user).await?;
let mut claims = IndexMap::<String, Value>::new();
claims.insert(Constants::LOGIN_USER_KEY.to_string(), Value::String(token));
let application = req.get_application()?;
let security_config = application.get_security_config()?;
let secret = security_config.token_secret.as_str();
let token_string = Self::create_token_by_claims(secret, &mut claims)?;
Ok(token_string)
}
#[instrument]
pub async fn verify_token<Req: RequestExt + ?Sized>(req: &Req, login_user: &mut LoginUser) -> AppResult<()> {
let expire_time = login_user.expire_time.unwrap_or_default();
let current_time = Local::now().timestamp_millis();
if expire_time - current_time <= Self::MILLIS_MINUTE_TEN {
Self::refresh_token(req, login_user).await?;
}
Ok(())
}
#[instrument]
pub async fn refresh_token<Req: RequestExt + ?Sized>(req: &Req, login_user: &mut LoginUser) -> AppResult<()> {
let application = req.get_application()?;
let expire_time = application.get_security_config()?.token_expire_time as i64;
let expire_time = expire_time * Self::MILLIS_MINUTE;
let cur_time_millis = Local::now().timestamp_millis();
login_user.login_time = Some(cur_time_millis);
login_user.expire_time = Some(cur_time_millis + expire_time);
let user_key = get_token_key(login_user.token.as_str());
let service = application.get_cache_service::<T>()?;
service.set_cache_object_with_timeout(&application, user_key.as_str(), login_user, Duration::from_millis(expire_time as u64)).await
}
#[instrument]
pub fn set_user_agent<Req: RequestExt + ?Sized>(req: &Req, login_user: &mut LoginUser) -> AppResult<()> {
let ip = req.get_remote_ip_address();
login_user.ipaddr = Some(ip.to_string());
login_user.login_location = Some("XX XX".to_string());
let user_agent = req.get_user_agent();
let user_agent_str = user_agent.as_ref();
if user_agent_str.not_empty() {
login_user.os = Some(ClientUtil::get_user_agent_os(user_agent_str).into_owned());
login_user.device = Some(ClientUtil::get_user_agent_browser(user_agent_str).into_owned());
}
Ok(())
}
#[instrument]
fn create_token_by_claims(secret: &str, claims: &mut IndexMap<String, Value>) -> AppResult<String> {
if !claims.contains_key("exp") {
claims.insert("exp".to_string(), Value::Number(Number::from(Local::now().timestamp_millis())));
}
let token = encode(&Header::new(Algorithm::HS512), &claims, &EncodingKey::from_secret(secret.as_bytes()))
.map_err(|err| app_system_error!(format!("create token failed! reason: {}, claims: {:?}", err, claims)))?;
Ok(token)
}
#[instrument]
fn parse_token(secret: &str, token: &str) -> AppResult<IndexMap<String, Value>> {
let token =
decode::<IndexMap<String, Value>>(token, &DecodingKey::from_secret(secret.as_bytes()), &Validation::new(Algorithm::HS512))
.map_err(|err| app_system_error!(format!("parse token failed: {}, reason: {}", token, err)))?;
Ok(token.claims)
}
#[instrument]
pub fn get_username_from_token(secret: &str, token: &str) -> AppResult<Option<String>> {
let mut claims = Self::parse_token(secret, token)?;
match claims.remove("sub") {
None => Ok(None),
Some(sub) => Ok(Some(sub.to_string_value())),
}
}
#[instrument]
fn get_token<Req: RequestExt + ?Sized>(req: &Req) -> AppResult<Option<String>> {
let token = get_token_value(req)?;
let token = token.as_str();
match token.not_empty() && token.starts_with(Constants::TOKEN_PREFIX) {
true => {
let token = token.replace(Constants::TOKEN_PREFIX, "");
Ok(Some(token))
}
false => Ok(None),
}
}
}
#[instrument]
async fn get_login_user_by_token<Req: RequestExt + ?Sized, T: ICacheService>(req: &Req, token: &str) -> AppResult<Option<LoginUser>> {
let application = req.get_application()?;
let security_config = application.get_security_config()?;
let secret = security_config.token_secret.as_str();
let mut claims = TokenService::parse_token(secret, token)?;
let uuid = claims.remove(Constants::LOGIN_USER_KEY).map(|v| v.to_string_value());
match uuid {
None => Ok(None),
Some(uuid) => {
let user_key = get_token_key(uuid.as_str());
let service = application.get_cache_service::<T>()?;
let user = service.get_cache_object(&application, user_key.as_str()).await?;
Ok(user)
}
}
}
#[instrument]
fn get_token_value<Req: RequestExt + ?Sized>(req: &Req) -> AppResult<Option<Cow<str>>> {
let application = req.get_application()?;
let security_config = application.get_security_config()?;
let header_name = security_config.token_header_name.as_str();
req.get_request_header(header_name)
}
#[instrument]
fn get_token_key(uuid: &str) -> String {
format!("{}{}", Constants::LOGIN_TOKEN_KEY, uuid)
}
#[allow(unused)]
#[cfg(test)]
#[cfg(feature = "redis")]
mod test {
use crate::tina::constant::Constants;
use crate::tina::core::service::token::TokenService;
use crate::tina::redis::cache::RedisCache;
use indexmap::IndexMap;
use serde_json::Value;
use std::error::Error;
#[test]
#[ignore]
fn test_parse() -> Result<(), Box<dyn Error>> {
let token = "eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6IjU4ZmNjZTU3LWMyYzktNDgyMi1iNDQ0LTRmMTc0N2U4YWM5MyJ9.sNfAQ3ur8cb4I695P3OEfvl9voYGGKJicP1VeSrwr5t-YkP7smviPTHvpsg17Tghs7iHgTfmOmaykpuOaVCrnw";
let header = jsonwebtoken::decode_header(token)?;
println!("{:#?}", header);
let map = TokenService::parse_token("abcdefghijklmnopqrstuvwxyz", token)?;
println!("{:#?}", map);
Ok(())
}
#[test]
#[ignore]
fn create_token() -> Result<(), Box<dyn Error>> {
let token = "58fcce57-c2c9-4822-b444-4f1747e8ac93";
let mut claims = IndexMap::<String, Value>::new();
claims.insert(Constants::LOGIN_TOKEN_KEY.to_string(), Value::String(token.to_string()));
let tokens = TokenService::create_token_by_claims("abcdefghijklmnopqrstuvwxyz", &mut claims)?;
println!("{}", tokens);
let map = TokenService::parse_token("abcdefghijklmnopqrstuvwxyz", tokens.as_str())?;
println!("{:#?}", map);
Ok(())
}
}