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

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. This filter extracts the JWT from the Authorization header, validates it, and populates the SecurityContextHolder with an Authentication object containing the user's authorities.

// 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 implements the Spring Security UserDetails interface and builds the user's authority list from their assigned resources stored in the ums_resource table.

// 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 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.

// 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.

// 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:

// 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 implements the voting logic that determines whether a request proceeds or receives an AccessDeniedException.

// 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, the system immediately invalidates the cached permission map to enforce the new rules without restarting the application.

// 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:

    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:

    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 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →