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 –
JwtAuthenticationTokenFiltervalidates tokens and loads user details - Authority Loading –
AdminUserDetailsconverts database resources into Spring Security granted authorities - Filter Chain Assembly –
SecurityConfigregisters the dynamic security filter when enabled - Metadata Resolution –
DynamicSecurityMetadataSourcemaps request URLs to required permissions using Ant-style patterns - Access Decision –
DynamicAccessDecisionManagercompares required permissions against user authorities - Permission Updates –
UmsResourceControllertriggers 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:
-
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" }' -
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
DynamicSecurityMetadataSourceloads URL patterns and required permissions from theums_resourcetable into a concurrent hash map at runtime viaDynamicSecurityService. - JWT-Based Authentication:
JwtAuthenticationTokenFiltervalidates JSON Web Tokens and populates the security context with authorities formatted asresourceId:resourceName. - Ant-Style URL Matching: The system supports wildcard patterns like
/admin/**throughAntPathMatcher, enabling flexible resource definitions. - Real-Time Updates: CRUD operations on resources via
UmsResourceControllertriggerclearDataSource(), forcing immediate permission map reloads without service restarts. - Explicit Authority Comparison:
DynamicAccessDecisionManagerperforms exact string matching between requiredConfigAttributevalues 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →