fix(auth): 修复 Token 刷新并发竞态条件
使用原子 CAS(UPDATE WHERE token_hash = ? AND revoked_at IS NULL) 替代先查后改的非原子操作,防止同一 refresh token 被并发使用两次。 新增 TokenService::validate_and_revoke_atomic 方法,将 JWT 解码、 哈希匹配和 token 撤销合并为单次数据库操作。
This commit is contained in:
@@ -189,12 +189,9 @@ impl AuthService {
|
||||
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, claims.sub, db).await?;
|
||||
// Atomically validate and revoke the old refresh token (prevents TOCTOU race)
|
||||
let claims =
|
||||
TokenService::validate_and_revoke_atomic(refresh_token_str, db, jwt.secret).await?;
|
||||
|
||||
// Fetch fresh roles and permissions
|
||||
let roles: Vec<String> = TokenService::get_user_roles(claims.sub, claims.tid, db).await?;
|
||||
|
||||
@@ -175,6 +175,45 @@ impl TokenService {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Atomically validate and revoke a refresh token by hash.
|
||||
/// This prevents TOCTOU race conditions during concurrent refresh requests.
|
||||
/// Returns the decoded claims on success, or TokenRevoked if already consumed.
|
||||
pub async fn validate_and_revoke_atomic(
|
||||
token: &str,
|
||||
db: &DatabaseConnection,
|
||||
secret: &str,
|
||||
) -> AuthResult<Claims> {
|
||||
let claims = Self::decode_token(token, secret)?;
|
||||
if claims.token_type != "refresh" {
|
||||
return Err(AuthError::Validation("不是 refresh token".to_string()));
|
||||
}
|
||||
|
||||
let hash = sha256_hex(token);
|
||||
let now = Utc::now();
|
||||
let result = user_token::Entity::update_many()
|
||||
.col_expr(
|
||||
user_token::Column::RevokedAt,
|
||||
sea_orm::sea_query::Expr::value(Some(now.naive_utc())),
|
||||
)
|
||||
.col_expr(
|
||||
user_token::Column::UpdatedAt,
|
||||
sea_orm::sea_query::Expr::value(now.naive_utc()),
|
||||
)
|
||||
.filter(user_token::Column::TokenHash.eq(&hash))
|
||||
.filter(user_token::Column::UserId.eq(claims.sub))
|
||||
.filter(user_token::Column::TenantId.eq(claims.tid))
|
||||
.filter(user_token::Column::RevokedAt.is_null())
|
||||
.exec(db)
|
||||
.await
|
||||
.map_err(|e| AuthError::Validation(e.to_string()))?;
|
||||
|
||||
if result.rows_affected == 0 {
|
||||
return Err(AuthError::TokenRevoked);
|
||||
}
|
||||
|
||||
Ok(claims)
|
||||
}
|
||||
|
||||
/// Revoke all non-revoked refresh tokens for a given user within a tenant.
|
||||
pub async fn revoke_all_user_tokens(
|
||||
user_id: Uuid,
|
||||
|
||||
Reference in New Issue
Block a user