# How the Spring Security RBAC Permission System Works in mall-admin

> Discover how the mall-admin Spring Security RBAC permission system dynamically manages user access. Learn how URL permissions are loaded from the database and refreshed without server restarts.

- Repository: [macro/mall](https://github.com/macrozheng/mall)
- Tags: deep-dive
- Published: 2026-02-28

---

**The mall-admin module implements a dynamic Role-Based Access Control (RBAC) system using Spring Security that loads URL-to-permission mappings from the database at runtime, validates them against JWT-derived user authorities, and automatically refreshes when administrators modify permissions without requiring a server restart.**

The macrozheng/mall e-commerce platform uses a sophisticated **Spring Security RBAC permission system** in its mall-admin module to protect management APIs. Unlike static annotation-based security, this implementation stores access rules in the `ums_resource` table and enforces them through a custom filter chain that evaluates permissions dynamically for every request.

## Architecture Overview

The system operates through six coordinated components that form a runtime-configurable security pipeline:

- **JWT Authentication** – `JwtAuthenticationTokenFilter` validates tokens and loads user details
- **Authority Loading** – `AdminUserDetails` converts database resources into Spring Security granted authorities
- **Filter Chain Assembly** – `SecurityConfig` registers the dynamic security filter when enabled
- **Metadata Resolution** – `DynamicSecurityMetadataSource` maps request URLs to required permissions using Ant-style patterns
- **Access Decision** – `DynamicAccessDecisionManager` compares required permissions against user authorities
- **Permission Updates** – `UmsResourceController` triggers cache invalidation when resources change

## JWT Authentication and Authority Loading

### Token Validation and Security Context Population

Every request first passes through `JwtAuthenticationTokenFilter` located in [`mall-security/src/main/java/com/macro/mall/security/component/JwtAuthenticationTokenFilter.java`](https://github.com/macrozheng/mall/blob/main/mall-security/src/main/java/com/macro/mall/security/component/JwtAuthenticationTokenFilter.java). This filter extracts the JWT from the Authorization header, validates it, and populates the `SecurityContextHolder` with an `Authentication` object containing the user's authorities.

```java
// JwtAuthenticationTokenFilter.doFilterInternal(...)
String authHeader = request.getHeader(tokenHeader);
if (authHeader != null && authHeader.startsWith(tokenHead)) {
    String authToken = authHeader.substring(tokenHead.length());
    String username = jwtTokenUtil.getUserNameFromToken(authToken);
    UserDetails userDetails = userDetailsService.loadUserByUsername(username);
    if (jwtTokenUtil.validateToken(authToken, userDetails)) {
        Authentication auth = new UsernamePasswordAuthenticationToken(
                userDetails, null, userDetails.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(auth);
    }
}

```

The filter runs before any URL-based authorization checks, ensuring the authentication object is available for the subsequent RBAC evaluation.

### Authority Construction from Database Resources

The `AdminUserDetails` class in [`mall-admin/src/main/java/com/macro/mall/bo/AdminUserDetails.java`](https://github.com/macrozheng/mall/blob/main/mall-admin/src/main/java/com/macro/mall/bo/AdminUserDetails.java) implements the Spring Security `UserDetails` interface and builds the user's authority list from their assigned resources stored in the `ums_resource` table.

```java
// AdminUserDetails.getAuthorities()
return resourceList.stream()
        .map(r -> new SimpleGrantedAuthority(r.getId() + ":" + r.getName()))
        .collect(Collectors.toList());

```

Each authority follows the format `resourceId:resourceName`, enabling fine-grained permission matching against the database records.

## Dynamic Authorization Filter Chain

### Conditional Filter Registration

The `SecurityConfig` class in [`mall-security/src/main/java/com/macro/mall/security/config/SecurityConfig.java`](https://github.com/macrozheng/mall/blob/main/mall-security/src/main/java/com/macro/mall/security/config/SecurityConfig.java) assembles the security filter chain. It conditionally adds the `DynamicSecurityFilter` only when a `DynamicSecurityService` bean exists, allowing the RBAC system to be enabled or disabled via configuration.

```java
// SecurityConfig.filterChain(...)
http.authorizeRequests()
    .antMatchers(ignoreUrlsConfig.getUrls()).permitAll()
    .anyRequest().authenticated()
    .and()
    .addFilterBefore(jwtAuthenticationTokenFilter,
                     UsernamePasswordAuthenticationFilter.class);
if (dynamicSecurityService != null) {
    http.addFilterBefore(dynamicSecurityFilter,
                         FilterSecurityInterceptor.class);
}

```

### URL-to-Permission Mapping

The `DynamicSecurityMetadataSource` class maintains an in-memory map of URL patterns to required permissions. It loads this data from the database via `DynamicSecurityService` and supports Ant-style path patterns.

```java
// DynamicSecurityMetadataSource.loadDataSource()
configAttributeMap = dynamicSecurityService.loadDataSource();

// MallSecurityConfig implementation in mall-admin/src/main/java/com/macro/mall/config/MallSecurityConfig.java
Map<String, ConfigAttribute> map = new ConcurrentHashMap<>();
for (UmsResource r : resourceService.listAll()) {
    map.put(r.getUrl(),
            new SecurityConfig(r.getId() + ":" + r.getName()));
}

```

When a request arrives, the `getAttributes` method matches the request path against stored patterns:

```java
// DynamicSecurityMetadataSource.getAttributes(...)
String path = URLUtil.getPath(((FilterInvocation) o).getRequestUrl());
for (String pattern : configAttributeMap.keySet()) {
    if (new AntPathMatcher().match(pattern, path)) {
        configAttributes.add(configAttributeMap.get(pattern));
    }
}

```

## Access Decision Logic

The `DynamicAccessDecisionManager` in [`mall-security/src/main/java/com/macro/mall/security/component/DynamicAccessDecisionManager.java`](https://github.com/macrozheng/mall/blob/main/mall-security/src/main/java/com/macro/mall/security/component/DynamicAccessDecisionManager.java) implements the voting logic that determines whether a request proceeds or receives an `AccessDeniedException`.

```java
// DynamicAccessDecisionManager.decide(...)
if (CollUtil.isEmpty(configAttributes)) return; // no config → allow
for (ConfigAttribute ca : configAttributes) {
    String need = ca.getAttribute();
    for (GrantedAuthority ga : authentication.getAuthorities()) {
        if (need.trim().equals(ga.getAuthority())) return; // granted
    }
}
throw new AccessDeniedException("抱歉，您没有访问权限");

```

The manager iterates through required `ConfigAttribute` objects and checks for exact matches against the user's granted authorities. If no match exists, access is denied with a message indicating insufficient permissions.

## Runtime Permission Management

### Cache Invalidation on Resource Changes

When administrators modify permissions through the `UmsResourceController` in [`mall-admin/src/main/java/com/macro/mall/controller/UmsResourceController.java`](https://github.com/macrozheng/mall/blob/main/mall-admin/src/main/java/com/macro/mall/controller/UmsResourceController.java), the system immediately invalidates the cached permission map to enforce the new rules without restarting the application.

```java
// UmsResourceController.create / update / delete
int count = resourceService.create(umsResource);
dynamicSecurityMetadataSource.clearDataSource();

```

The `clearDataSource()` method forces `DynamicSecurityMetadataSource` to reload the URL-to-permission map from the database on the next request, ensuring zero-downtime permission updates.

## Practical Example: Adding and Testing a New Permission

The following workflow demonstrates the dynamic nature of the system:

1.  **Create a new resource** via the API, which clears the security cache:

    ```bash
    curl -X POST http://localhost:8080/resource/create \
         -H "Authorization: Bearer <admin-jwt>" \
         -H "Content-Type: application/json" \
         -d '{
               "name":"订单管理",
               "url":"/order/**",
               "description":"Manage orders"
             }'
    ```

2.  **Access the protected endpoint**. The request succeeds only if the admin's resource list contains the newly created permission:

    ```bash
    curl http://localhost:8080/order/list \
         -H "Authorization: Bearer <admin-jwt>"
    ```

When the admin authenticates again, `AdminUserDetails` generates a new `SimpleGrantedAuthority` such as `42:订单管理`, allowing access to `/order/list` and any sub-paths matching the `/order/**` pattern.

## Summary

-   **Dynamic Metadata Loading**: The `DynamicSecurityMetadataSource` loads URL patterns and required permissions from the `ums_resource` table into a concurrent hash map at runtime via `DynamicSecurityService`.
-   **JWT-Based Authentication**: `JwtAuthenticationTokenFilter` validates JSON Web Tokens and populates the security context with authorities formatted as `resourceId:resourceName`.
-   **Ant-Style URL Matching**: The system supports wildcard patterns like `/admin/**` through `AntPathMatcher`, enabling flexible resource definitions.
-   **Real-Time Updates**: CRUD operations on resources via `UmsResourceController` trigger `clearDataSource()`, forcing immediate permission map reloads without service restarts.
-   **Explicit Authority Comparison**: `DynamicAccessDecisionManager` performs exact string matching between required `ConfigAttribute` values and user authorities, rejecting requests that lack specific permissions.

## Frequently Asked Questions

### How does the system handle public endpoints that don't require authentication?

The `IgnoreUrlsConfig` class in [`mall-security/src/main/java/com/macro/mall/security/config/IgnoreUrlsConfig.java`](https://github.com/macrozheng/mall/blob/main/mall-security/src/main/java/com/macro/mall/security/config/IgnoreUrlsConfig.java) defines a configurable whitelist of URL patterns (such as Swagger documentation and login endpoints) that bypass the dynamic RBAC filter entirely. These paths are permitted through `http.authorizeRequests().antMatchers(ignoreUrlsConfig.getUrls()).permitAll()` in `SecurityConfig`.

### What happens when no permissions are configured for a URL?

If `DynamicSecurityMetadataSource` returns an empty collection of `ConfigAttribute` objects for a request path, the `DynamicAccessDecisionManager` immediately returns without throwing an exception, effectively allowing the request. This behavior ensures that unconfigured endpoints remain accessible rather than being blocked by default.

### Can the permission system work without JWT authentication?

No, the dynamic RBAC filter chain depends on the authentication object populated by `JwtAuthenticationTokenFilter`. Without a valid JWT token and the resulting `Authentication` object in `SecurityContextHolder`, the `DynamicAccessDecisionManager` cannot compare required permissions against user authorities, and the request fails at the authentication layer before reaching the authorization logic.

### How are permission changes propagated to active user sessions?

The system uses an in-memory cache invalidation strategy rather than session-based propagation. When an administrator modifies a resource via `UmsResourceController`, the `clearDataSource()` method empties the cached map in `DynamicSecurityMetadataSource`. Subsequent requests trigger a reload of the complete permission map from the database via `DynamicSecurityService`, affecting all active users immediately without requiring re-login.