- auth_service::refresh() 添加 tenant_id 校验 - user_service get_by_id/update/delete/assign_roles 改为数据库级 tenant_id 过滤 - 限流中间件改为 fail-closed:Redis 不可达时返回 429 而非放行
316 lines
11 KiB
Rust
316 lines
11 KiB
Rust
use chrono::Utc;
|
|
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set};
|
|
use uuid::Uuid;
|
|
|
|
use crate::dto::{LoginResp, RoleResp, UserResp};
|
|
use crate::entity::{role, user, user_credential, user_role};
|
|
use crate::error::AuthError;
|
|
use erp_core::events::EventBus;
|
|
|
|
use crate::error::AuthResult;
|
|
|
|
use super::password;
|
|
use super::token_service::TokenService;
|
|
|
|
/// JWT configuration needed for token signing.
|
|
pub struct JwtConfig<'a> {
|
|
pub secret: &'a str,
|
|
pub access_ttl_secs: i64,
|
|
pub refresh_ttl_secs: i64,
|
|
}
|
|
|
|
/// Authentication service handling login, token refresh, and logout.
|
|
pub struct AuthService;
|
|
|
|
impl AuthService {
|
|
/// Authenticate a user and issue access + refresh tokens.
|
|
///
|
|
/// Steps:
|
|
/// 1. Look up user by tenant + username (soft-delete aware)
|
|
/// 2. Verify user status is "active"
|
|
/// 3. Fetch the stored password credential
|
|
/// 4. Verify password hash
|
|
/// 5. Collect roles and permissions
|
|
/// 6. Sign JWT tokens
|
|
/// 7. Update last_login_at
|
|
/// 8. Publish login event
|
|
pub async fn login(
|
|
tenant_id: Uuid,
|
|
username: &str,
|
|
password_plain: &str,
|
|
db: &sea_orm::DatabaseConnection,
|
|
jwt: &JwtConfig<'_>,
|
|
event_bus: &EventBus,
|
|
) -> AuthResult<LoginResp> {
|
|
// 1. Find user by tenant_id + username
|
|
let user_model = user::Entity::find()
|
|
.filter(user::Column::TenantId.eq(tenant_id))
|
|
.filter(user::Column::Username.eq(username))
|
|
.filter(user::Column::DeletedAt.is_null())
|
|
.one(db)
|
|
.await
|
|
.map_err(|e| AuthError::Validation(e.to_string()))?
|
|
.ok_or(AuthError::InvalidCredentials)?;
|
|
|
|
// 2. Check user status
|
|
if user_model.status != "active" {
|
|
return Err(AuthError::UserDisabled(user_model.status.clone()));
|
|
}
|
|
|
|
// 3. Find password credential
|
|
let cred = user_credential::Entity::find()
|
|
.filter(user_credential::Column::UserId.eq(user_model.id))
|
|
.filter(user_credential::Column::CredentialType.eq("password"))
|
|
.filter(user_credential::Column::DeletedAt.is_null())
|
|
.one(db)
|
|
.await
|
|
.map_err(|e| AuthError::Validation(e.to_string()))?
|
|
.ok_or(AuthError::InvalidCredentials)?;
|
|
|
|
// 4. Verify password
|
|
let stored_hash = cred
|
|
.credential_data
|
|
.as_ref()
|
|
.and_then(|v| v.get("hash").and_then(|h| h.as_str()))
|
|
.ok_or(AuthError::InvalidCredentials)?;
|
|
|
|
if !password::verify_password(password_plain, stored_hash)? {
|
|
return Err(AuthError::InvalidCredentials);
|
|
}
|
|
|
|
// 5. Get roles and permissions
|
|
let roles: Vec<String> = TokenService::get_user_roles(user_model.id, tenant_id, db).await?;
|
|
let permissions = TokenService::get_user_permissions(user_model.id, tenant_id, db).await?;
|
|
|
|
// 6. Sign tokens
|
|
let access_token = TokenService::sign_access_token(
|
|
user_model.id,
|
|
tenant_id,
|
|
roles.clone(),
|
|
permissions,
|
|
jwt.secret,
|
|
jwt.access_ttl_secs,
|
|
)?;
|
|
let (refresh_token, _) = TokenService::sign_refresh_token(
|
|
user_model.id,
|
|
tenant_id,
|
|
db,
|
|
jwt.secret,
|
|
jwt.refresh_ttl_secs,
|
|
)
|
|
.await?;
|
|
|
|
// 7. Update last_login_at
|
|
let mut user_active: user::ActiveModel = user_model.clone().into();
|
|
user_active.last_login_at = Set(Some(Utc::now()));
|
|
user_active.updated_at = Set(Utc::now());
|
|
user_active
|
|
.update(db)
|
|
.await
|
|
.map_err(|e| AuthError::Validation(e.to_string()))?;
|
|
|
|
// 8. Build response
|
|
let role_resps = Self::get_user_role_resps(user_model.id, tenant_id, db).await?;
|
|
let user_resp = UserResp {
|
|
id: user_model.id,
|
|
username: user_model.username.clone(),
|
|
email: user_model.email,
|
|
phone: user_model.phone,
|
|
display_name: user_model.display_name,
|
|
avatar_url: user_model.avatar_url,
|
|
status: user_model.status,
|
|
roles: role_resps,
|
|
version: user_model.version,
|
|
};
|
|
|
|
// 9. Publish event
|
|
event_bus.publish(erp_core::events::DomainEvent::new(
|
|
"user.login",
|
|
tenant_id,
|
|
serde_json::json!({ "user_id": user_model.id, "username": user_model.username }),
|
|
), db).await;
|
|
|
|
Ok(LoginResp {
|
|
access_token,
|
|
refresh_token,
|
|
expires_in: jwt.access_ttl_secs as u64,
|
|
user: user_resp,
|
|
})
|
|
}
|
|
|
|
/// Refresh the token pair: validate the old refresh token, revoke it, issue a new pair.
|
|
pub async fn refresh(
|
|
refresh_token_str: &str,
|
|
db: &sea_orm::DatabaseConnection,
|
|
jwt: &JwtConfig<'_>,
|
|
) -> AuthResult<LoginResp> {
|
|
// Validate existing refresh token
|
|
let (old_token_id, claims) =
|
|
TokenService::validate_refresh_token(refresh_token_str, db, jwt.secret).await?;
|
|
|
|
// Revoke the old token (rotation)
|
|
TokenService::revoke_token(old_token_id, db).await?;
|
|
|
|
// Fetch fresh roles and permissions
|
|
let roles: Vec<String> = TokenService::get_user_roles(claims.sub, claims.tid, db).await?;
|
|
let permissions = TokenService::get_user_permissions(claims.sub, claims.tid, db).await?;
|
|
|
|
// Sign new token pair
|
|
let access_token = TokenService::sign_access_token(
|
|
claims.sub,
|
|
claims.tid,
|
|
roles.clone(),
|
|
permissions,
|
|
jwt.secret,
|
|
jwt.access_ttl_secs,
|
|
)?;
|
|
let (new_refresh_token, _) = TokenService::sign_refresh_token(
|
|
claims.sub,
|
|
claims.tid,
|
|
db,
|
|
jwt.secret,
|
|
jwt.refresh_ttl_secs,
|
|
)
|
|
.await?;
|
|
|
|
// Fetch user for the response
|
|
let user_model = user::Entity::find_by_id(claims.sub)
|
|
.one(db)
|
|
.await
|
|
.map_err(|e| AuthError::Validation(e.to_string()))?
|
|
.ok_or(AuthError::TokenRevoked)?;
|
|
|
|
// 验证用户属于 JWT 中声明的租户
|
|
if user_model.tenant_id != claims.tid {
|
|
tracing::warn!(
|
|
user_id = %claims.sub,
|
|
jwt_tenant = %claims.tid,
|
|
actual_tenant = %user_model.tenant_id,
|
|
"Token tenant_id 与用户实际租户不匹配"
|
|
);
|
|
return Err(AuthError::TokenRevoked);
|
|
}
|
|
|
|
let role_resps = Self::get_user_role_resps(claims.sub, claims.tid, db).await?;
|
|
let user_resp = UserResp {
|
|
id: user_model.id,
|
|
username: user_model.username,
|
|
email: user_model.email,
|
|
phone: user_model.phone,
|
|
display_name: user_model.display_name,
|
|
avatar_url: user_model.avatar_url,
|
|
status: user_model.status,
|
|
roles: role_resps,
|
|
version: user_model.version,
|
|
};
|
|
|
|
Ok(LoginResp {
|
|
access_token,
|
|
refresh_token: new_refresh_token,
|
|
expires_in: jwt.access_ttl_secs as u64,
|
|
user: user_resp,
|
|
})
|
|
}
|
|
|
|
/// Revoke all refresh tokens for a user, effectively logging them out everywhere.
|
|
pub async fn logout(
|
|
user_id: Uuid,
|
|
tenant_id: Uuid,
|
|
db: &sea_orm::DatabaseConnection,
|
|
) -> AuthResult<()> {
|
|
TokenService::revoke_all_user_tokens(user_id, tenant_id, db).await
|
|
}
|
|
|
|
/// Change password for the authenticated user.
|
|
///
|
|
/// Steps:
|
|
/// 1. Verify current password
|
|
/// 2. Hash the new password
|
|
/// 3. Update the credential record
|
|
/// 4. Revoke all existing refresh tokens (force re-login)
|
|
pub async fn change_password(
|
|
user_id: Uuid,
|
|
tenant_id: Uuid,
|
|
current_password: &str,
|
|
new_password: &str,
|
|
db: &sea_orm::DatabaseConnection,
|
|
) -> AuthResult<()> {
|
|
// 1. Find the user's password credential
|
|
let cred = user_credential::Entity::find()
|
|
.filter(user_credential::Column::UserId.eq(user_id))
|
|
.filter(user_credential::Column::TenantId.eq(tenant_id))
|
|
.filter(user_credential::Column::CredentialType.eq("password"))
|
|
.filter(user_credential::Column::DeletedAt.is_null())
|
|
.one(db)
|
|
.await
|
|
.map_err(|e| AuthError::Validation(e.to_string()))?
|
|
.ok_or_else(|| AuthError::Validation("用户凭证不存在".to_string()))?;
|
|
|
|
// 2. Verify current password
|
|
let stored_hash = cred
|
|
.credential_data
|
|
.as_ref()
|
|
.and_then(|v| v.get("hash").and_then(|h| h.as_str()))
|
|
.ok_or_else(|| AuthError::Validation("用户凭证异常".to_string()))?;
|
|
|
|
if !password::verify_password(current_password, stored_hash)? {
|
|
return Err(AuthError::Validation("当前密码不正确".to_string()));
|
|
}
|
|
|
|
// 3. Hash new password and update credential
|
|
let new_hash = password::hash_password(new_password)?;
|
|
let mut cred_active: user_credential::ActiveModel = cred.into();
|
|
cred_active.credential_data = Set(Some(serde_json::json!({ "hash": new_hash })));
|
|
cred_active.updated_at = Set(Utc::now());
|
|
cred_active.version = Set(2);
|
|
cred_active
|
|
.update(db)
|
|
.await
|
|
.map_err(|e| AuthError::Validation(e.to_string()))?;
|
|
|
|
// 4. Revoke all refresh tokens — force re-login on all devices
|
|
TokenService::revoke_all_user_tokens(user_id, tenant_id, db).await?;
|
|
|
|
tracing::info!(user_id = %user_id, "Password changed successfully");
|
|
Ok(())
|
|
}
|
|
|
|
/// Fetch role details for a user, returning RoleResp DTOs.
|
|
async fn get_user_role_resps(
|
|
user_id: Uuid,
|
|
tenant_id: Uuid,
|
|
db: &sea_orm::DatabaseConnection,
|
|
) -> AuthResult<Vec<RoleResp>> {
|
|
let user_roles = user_role::Entity::find()
|
|
.filter(user_role::Column::UserId.eq(user_id))
|
|
.filter(user_role::Column::TenantId.eq(tenant_id))
|
|
.all(db)
|
|
.await
|
|
.map_err(|e| AuthError::Validation(e.to_string()))?;
|
|
|
|
let role_ids: Vec<Uuid> = user_roles.iter().map(|ur| ur.role_id).collect();
|
|
if role_ids.is_empty() {
|
|
return Ok(vec![]);
|
|
}
|
|
|
|
let roles = role::Entity::find()
|
|
.filter(role::Column::Id.is_in(role_ids))
|
|
.filter(role::Column::TenantId.eq(tenant_id))
|
|
.all(db)
|
|
.await
|
|
.map_err(|e| AuthError::Validation(e.to_string()))?;
|
|
|
|
Ok(roles
|
|
.iter()
|
|
.map(|r| RoleResp {
|
|
id: r.id,
|
|
name: r.name.clone(),
|
|
code: r.code.clone(),
|
|
description: r.description.clone(),
|
|
is_system: r.is_system,
|
|
version: r.version,
|
|
})
|
|
.collect())
|
|
}
|
|
}
|