This article introduces a modern permission system design and implementation based on the Rust Axum framework, focusing on how to build a high-performance, maintainable permission control system through declarative APIs, intelligent caching, and middleware architecture.


I. Introduction: Why Design a Unified Permission System?

In modern web application development, permission control is an unavoidable core requirement. As business complexity grows, we face the following challenges:

πŸ” Pain Points of Traditional Permission Control

Scattered Permission Checks: Each API endpoint requires manual permission checking code

// ❌ Traditional approach: Every handler needs repeated permission checks
async fn user_list_handler(current_user: CurrentUser) -> Result<Json<Vec<User>>, AppError> {
    if !current_user.has_permission("system:user:list") {
        return Err(AppError::PermissionDenied);
    }
    // Business logic...
}

Maintenance Difficulties: Permission logic scattered everywhere, hard to manage and debug uniformly

Performance Issues: Every request requires database queries to fetch permission information

Security Risks: Easy to miss permission checks or have inconsistent permission logic

🎯 Our Solution

Design a centralized, declarative, high-performance permission system that implements:


II. Design Goals and Principles

🎯 Core Goals of Permission System

  1. Centralized Permission Declaration: Permission binding completed during route registration phase, clear at a glance
  2. Unified Middleware Validation: All permission validation logic centralized in middleware layer
  3. Automatic User Information Injection: No need for manual user identity handling
  4. Cache Optimization: Avoid repeated queries, support intelligent refresh

βœ… Design Principles

Minimal Coupling: Complete separation of Authentication and Authorization

// Authentication middleware: Only responsible for identity verification
pub async fn auth_middleware(/* ... */) -> Result<Response, AppError>

// Permission middleware: Only responsible for permission checking
async fn permission_middleware(/* ... */) -> Result<Response, AppError>

Extensibility: Support single permission, any permission, all permissions and other combination modes

High Performance: In-memory caching + expiration refresh, minimize database access

Clean and Readable: Developer-friendly declarative API


III. Permission System Architecture Overview

πŸ”„ Complete Request Processing Flow

graph TD
    A[HTTP Request] --> B[Auth Middleware]
    B --> C{JWT Verification}
    C -->|Failed| D[401 Unauthorized]
    C -->|Success| E[Inject CurrentUser]
    E --> F[Permission Middleware]
    F --> G{Permission Cache Check}
    G -->|Cache Hit| H[Permission Validation]
    G -->|Cache Miss| I[Database Query]
    I --> J[Update Cache]
    J --> H
    H -->|Insufficient Permission| K[403 Forbidden]
    H -->|Permission Granted| L[Execute Business Handler]

πŸ—οΈ Module Architecture Design

Module File Responsibility
JWT Authentication core/jwt.rs Token generation, verification, Claims parsing
User Extraction auth/extractor.rs CurrentUser structure definition and extraction logic
Auth Middleware auth/middleware.rs JWT verification, user information injection
Permission Cache auth/permission.rs Permission cache management, permission validation logic
Router Extension common/router_ext.rs Declarative permission binding API

IV. Core Module Details

4.1 JWT Authentication Module

Design Philosophy: JWT only handles identity recognition, does not carry permission information

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Claims {
    pub user_id: i64,      // User unique identifier
    pub username: String,  // Username
    pub exp: usize,        // Expiration time
    pub iat: usize,        // Issued at time
}

Key Features:

pub fn verify_token(token: &str) -> Result<Claims, jsonwebtoken::errors::Error> {
    let validation = Validation::new(Algorithm::HS256);
    let token_data = decode::<Claims>(
        token,
        &DecodingKey::from_secret(JWT_CONFIG.secret.as_bytes()),
        &validation,
    )?;
    Ok(token_data.claims)
}

4.2 User Information Extractor CurrentUser

Design Highlight: Implements Axum’s FromRequestParts, supports dependency injection

πŸ“‹ CurrentUser vs Claims Semantic Differences

Concept Purpose Lifecycle Information Contained
Claims JWT Token payload data Token validity period Basic identity information
CurrentUser Business layer user abstraction Single request Verified user information
// Claims: Raw data after JWT parsing, only used for authentication
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Claims {
    pub user_id: i64,
    pub username: String,
    pub exp: usize,
    pub iat: usize,
}

// CurrentUser: Unified business layer user structure for dependency injection
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CurrentUser {
    pub user_id: i64,
    pub username: String,
}

// 🎯 Key: Implement FromRequestParts for automatic extraction
impl<S> FromRequestParts<S> for CurrentUser
where S: Send + Sync,
{
    type Rejection = AppError;

    fn from_request_parts(/* ... */) -> impl Future<Output = Result<Self, Self::Rejection>> {
        // Get user information from request extensions
        // Data comes from authentication middleware parsing Claims and injecting information
    }
}

Design Principles:

Usage:

// βœ… Extract current user information
async fn user_profile_handler(current_user: CurrentUser) -> Json<UserProfile> {
    // current_user automatically injected, contains user_id and username
}

// πŸ”§ Uniformly use CurrentUser to extract user information
async fn get_user_info_handler(
    current_user: CurrentUser,  // Automatically inject current user information
    State(pool): State<PgPool>,
) -> AppResult<Json<ApiResponse<UserInfoResponse>>> {
    let user_info = AuthService::get_user_info(&pool, current_user.user_id, &current_user.username).await?;
    Ok(ApiResponse::success(user_info))
}

4.3 Intelligent Permission Caching Mechanism

Core Design: In-memory cache + expiration refresh + thread safety

/// Permission cache entry with expiration time
#[derive(Debug, Clone)]
pub struct UserPermissionCache {
    pub permissions: HashSet<String>,  // User permission set
    pub cached_at: DateTime<Utc>,      // Cache time
}

/// Global cache manager, thread-safe
pub struct PermissionCacheManager {
    cache: Arc<RwLock<HashMap<i64, UserPermissionCache>>>,
}

Intelligent Refresh Strategy:

pub async fn get_cached_permissions(
    pool: &PgPool,
    user_id: i64,
) -> Result<Option<UserPermissionCache>, ServiceError> {
    if let Some(cache) = PERMISSION_CACHE.get(user_id) {
        if cache.is_expired() {
            // πŸ”„ Cache expired, automatically refresh from database
            let new_cache = Self::load_user_permissions_from_db(pool, user_id).await?;
            return Ok(Some(new_cache));
        }
        return Ok(Some(cache));
    }
    Ok(None)
}

Performance Optimization:


V. Declarative Route Permission Binding Design

5.1 RouterExt Trait Design

Core Innovation: Extend Axum Router to support permission declaration

pub trait RouterExt<S> {
    fn route_with_permission(
        self,
        path: &str,
        method_router: MethodRouter<S>,
        permissions_check: PermissionsCheck,  // 🎯 Core: Permission check configuration
    ) -> Self;
}

Implementation Principle:

impl RouterExt<PgPool> for Router<PgPool> {
    fn route_with_permission(self, path: &str, method_router: MethodRouter<PgPool>, permissions_check: PermissionsCheck) -> Self {
        self.route(
            path,
            method_router.layer(axum::middleware::from_fn(move |req: Request, next: Next| {
                let permissions_check = permissions_check.clone();
                async move { permission_middleware(req, next, permissions_check).await }
            })),
        )
    }
}

5.2 Flexible Permission Check Modes

PermissionsCheck Enum: Support multiple permission combination logic

#[derive(Debug, Clone)]
pub enum PermissionsCheck {
    Single(&'static str),           // Single permission
    Any(Vec<&'static str>),        // Any permission (OR logic)
    All(Vec<&'static str>),        // All permissions (AND logic)
}

Usage Examples:

// πŸ”Ή Single permission check
router.route_with_permission(
    "/system/users",
    get(user_list_handler),
    PermissionsCheck::Single("system:user:list")
)

// πŸ”Ή Any permission check (admin or user admin can access)
router.route_with_permission(
    "/system/users",
    post(create_user_handler),
    PermissionsCheck::Any(vec!["admin:all", "system:user:create"])
)

// πŸ”Ή All permissions check (requires delete permission AND confirm permission)
router.route_with_permission(
    "/system/users/{id}",
    delete(delete_user_handler),
    PermissionsCheck::All(vec!["system:user:delete", "system:confirm"])
)

5.3 Permission Middleware Core Logic

Unified Permission Validation Flow:

async fn permission_middleware(
    request: Request,
    next: Next,
    permissions_check: PermissionsCheck,
) -> Result<Response, AppError> {
    // 1️⃣ Get current user (injected by auth middleware)
    let current_user = request.extensions().get::<CurrentUser>()
        .ok_or(AppError::from(ServiceError::InvalidCredentials))?;

    // 2️⃣ Get database connection pool
    let pool = request.extensions().get::<PgPool>()
        .ok_or(AppError::from(ServiceError::DatabaseQueryFailed))?;

    // 3️⃣ Check permissions (cache first)
    let has_permission = PermissionService::check_permissions(
        &pool,
        current_user.user_id,
        &permissions_check
    ).await?;

    // 4️⃣ Deny access if insufficient permissions
    if !has_permission {
        return Err(AppError::from(ServiceError::PermissionDenied));
    }

    // 5️⃣ Permission granted, continue execution
    Ok(next.run(request).await)
}

VI. Permission Cache and JWT Collaboration Mechanism

6.1 Dual-Layer Cache Design

Cache Type Storage Location Expiration Time Refresh Mechanism Purpose
JWT Token Client 1-2 hours Re-login on expiration Identity authentication
Permission Cache Server memory 1 hour Auto refresh Permission validation

6.2 Cache Collaboration Flow

Cache Permissions on Login:

// Login triggers permission caching
pub async fn login(pool: &PgPool, request: LoginRequest) -> Result<LoginResponse, ServiceError> {
    let user = Self::verify_login(pool, &request.username, &request.password).await?;
    let token = jwt::generate_token(user.id, &user.username)?;

    // Getting user info automatically caches permissions
    let user_info = Self::get_user_info(pool, user.id, &user.username).await?;
    Ok(LoginResponse { token, user_info })
}

// 🎯 Key location for permission caching
pub async fn get_user_info(pool: &PgPool, user_id: i64, username: &str) -> Result<UserInfoResponse, ServiceError> {
    // Query user permissions
    let permissions = UserRepository::get_user_permissions(pool, user_id).await?;

    // 🎯 Cache permissions to memory (1 hour validity)
    PermissionService::cache_user_permissions(user_id, permissions);

    // Return user info...
}

Cache Strategy for Permission Checking:

pub async fn check_permissions(
    pool: &PgPool,
    user_id: i64,
    permissions_check: &PermissionsCheck,
) -> Result<bool, ServiceError> {
    // πŸ” Try to get permissions from cache
    if let Some(cache) = Self::get_cached_permissions(pool, user_id).await? {
        return Ok(permissions_check.check(&cache.permissions));
    }

    // 🚫 Cache miss, require re-authentication
    // Design rationale: This shouldn't happen in normal business flow,
    // if it occurs, it indicates system anomaly, deny access for security
    // Supports logout after user prohibition/deletion
    Err(ServiceError::InvalidCredentials)
}

6.3 Cache Invalidation and Security

Key Scenarios for Active Cache Clearing:

// 1️⃣ Clear permission cache on logout
async fn logout_handler(current_user: CurrentUser) -> AppResult<Json<ApiResponse<()>>> {
    PermissionService::clear_user_cache(current_user.user_id);
    Ok(ApiResponse::success(()))
}

// 2️⃣ Clear cache when deleting user
pub async fn delete_user(pool: &PgPool, id: i64) -> Result<(), ServiceError> {
    let deleted = UserRepository::soft_delete(pool, id).await?;
    if !deleted {
        return Err(ServiceError::NotFound("User not found".to_string()));
    }

    // 🎯 Key: Clear user's permission cache when deleting user
    PermissionService::clear_user_cache(id);
    tracing::info!("Deleted user {} and cleared associated cache", id);

    Ok(())
}

// 3️⃣ Clear cache when user roles change
pub async fn update_user(/* ... */) -> Result<UserResponse, ServiceError> {
    // When updating user roles
    if let Some(role_ids) = request.role_ids {
        UserRepository::set_user_roles(pool, updated_user.id, &role_ids).await?;

        // 🎯 Clear permission cache after role change, force reload
        PermissionService::clear_user_cache(updated_user.id);
        tracing::info!("Updated user {} roles and cleared permission cache", updated_user.id);
    }

    // When user status changes (especially disabling user)
    if let Some(new_status) = request.status {
        if new_status != user.status {
            PermissionService::clear_user_cache(updated_user.id);
            tracing::info!("Updated user {} status and cleared permission cache", updated_user.id);
        }
    }
}

Cache Security Strategy:


VII. Practical Usage Examples

7.1 System Management Module Route Configuration

pub fn system_routes() -> Router<PgPool> {
    Router::new()
        // User management
        .route_with_permission(
            "/users",
            get(user_list_handler),
            PermissionsCheck::Single("system:user:list")
        )
        .route_with_permission(
            "/users",
            post(create_user_handler),
            PermissionsCheck::Single("system:user:create")
        )
        .route_with_permission(
            "/users/{id}",
            put(update_user_handler),
            PermissionsCheck::Single("system:user:update")
        )
        .route_with_permission(
            "/users/{id}",
            delete(delete_user_handler),
            PermissionsCheck::All(vec!["system:user:delete", "system:confirm"])
        )

        // Role management
        .route_with_permission(
            "/roles",
            get(role_list_handler),
            PermissionsCheck::Any(vec!["system:role:list", "admin:all"])
        )
}

7.2 Handler Implementation

// βœ… Handler focuses on business logic, no need to worry about permission checks
async fn user_list_handler(
    current_user: CurrentUser,  // Automatically injected current user
    State(pool): State<PgPool>,
) -> AppResult<Json<ApiResponse<Vec<UserResponse>>>> {
    // Permission already checked at middleware layer, handle business logic directly here
    let users = UserService::list_users(&pool).await?;
    Ok(ApiResponse::success(users))
}

async fn delete_user_handler(
    current_user: CurrentUser,
    State(pool): State<PgPool>,
    Path(user_id): Path<i64>,
) -> AppResult<Json<ApiResponse<()>>> {
    // Permission check: requires system:user:delete AND system:confirm
    // Already declared at route layer, middleware handles automatically
    UserService::delete_user(&pool, user_id).await?;
    Ok(ApiResponse::success(()))
}

VIII. Summary

🎯 Multiple Benefits from Core Design

Declarative Permission Binding Design:

Intelligent Caching Mechanism:

Unified Middleware Processing:

πŸ“Š Overall Effect

Design Feature Development Efficiency Performance Optimization Security Assurance Maintainability Extensibility
Declarative API βœ… - βœ… βœ… βœ…
Intelligent Cache - βœ… βœ… - βœ…
Unified Middleware βœ… - βœ… βœ… -
Modular Design βœ… - - βœ… βœ