1. Introduction
In a microservices and front-end/back-end separation architecture, permission management is one of the fundamental pieces of system security. If every application implements its own authentication logic from scratch, it not only wastes effort by reinventing the wheel but also risks overlooking edge cases, leading to security vulnerabilities. RBAC (Role-Based Access Control) introduces an intermediate layer called “role” to decouple users from permissions, and it is currently the most widely adopted permission model in the industry. This article walks you through the core concepts of RBAC, database table design, and implements a minimal but functional permission center using Spring Boot + JPA.
After reading this article, you will understand the principles of the RBAC model, be able to independently design many-to-many table structures for users, roles, and permissions, and write runnable CRUD and authentication interfaces.
2. RBAC Core Concepts and Model Explanation
2.1 Basic Elements: Users, Roles, and Permissions
The core of the RBAC model consists of three entities: User, Role, and Permission. The user is the subject of operations, the permission is the capability to perform an operation, and the role serves as the bridge between them.
The relationships are as follows:
- User and Role: Many-to-many. A user can have multiple roles (e.g., both an “admin” and a “reviewer”), and a role can be assigned to multiple users.
- Role and Permission: Many-to-many. A role can contain multiple permissions (e.g., “admin” has “read,” “write,” and “delete” permissions), and a permission can belong to multiple roles (e.g., the “read” permission belongs to both “admin” and “regular user”).
This design makes permission changes flexible: when business rules change, you only need to adjust the role-permission associations without modifying each user individually. For example, if a company changes its attendance policy and needs to give “attendance administrators” a new “export report” permission, the administrator simply adds that permission to the “attendance administrator” role, and all users assigned that role automatically gain the new capability.
2.2 Permission Granularity: Operation Level and Resource Level
In practice, permissions often need to distinguish between the object of the operation and the type of operation. We introduce the combination of Resource and Action to define permission granularity.
- Action: Refers to the operation performed on a resource. Common actions include: create, read, update, delete (abbreviated as CRUD). Sometimes business actions like export or approve are added.
- Resource: Refers to the object of the operation, such as order, user, product, report, etc.
Therefore, a complete permission record can be expressed as: “Allow the ‘read’ operation on the ‘order’ resource.” At the database level, we design the permission table as permission(resource, action) or permission(code) like order:read. This combination is clear and extensible; when new resources or actions are added, you simply add new records to the permission table.
The RBAC model is divided into multiple levels. The most basic is RBAC0, which includes the triple of users, roles, and permissions described above. RBAC1 introduces role inheritance (e.g., “super admin” inherits all permissions of “admin”), and RBAC2 adds separation of duty constraints (e.g., a user cannot simultaneously have “cashier” and “accountant” roles). This article focuses on RBAC0, which is the starting point for most business systems.
3. Database Table Design: Users, Roles, Permissions, and Their Relationships
3.1 Core Table Structure
Based on the RBAC0 model, the database requires at least five tables:
user: Stores login accounts, passwords, and personal information.role: Stores role names and descriptions.permission: Stores combinations of resources and actions; typically includesresourceandactioncolumns.user_role: The user-role association table, establishing a many-to-many relationship between users and roles.role_permission: The role-permission association table, establishing a many-to-many relationship between roles and permissions.
3.2 Sample Table Creation SQL (PostgreSQL)
1 | |
3.3 Design Considerations
- Primary Keys and Unique Constraints:
user_roleandrole_permissionuse composite primary keys to ensure unique records and avoid duplicate assignments. Thepermissiontable usesUNIQUE (resource, action)to prevent duplicate permission definitions. - Foreign Keys and Cascade Deletion: The foreign keys in association tables use
ON DELETE CASCADE. When a user or role is deleted, the associated records are automatically removed.
However, do not use ON DELETE CASCADE directly on parent table records in production permission tables; typically, soft deletion is used in business logic, and physical deletion is only used during testing or cleanup.
- Indexes: It is recommended to create separate indexes on foreign key columns in association tables to speed up queries. For example:
1 | |
4. Project Setup and Dependency Configuration (Spring Boot in Practice)
4.1 Maven Dependencies (pom.xml)
Create a Spring Boot project and introduce the following core dependencies:
1 | |
If using MySQL, replace postgresql with mysql-connector-j.
4.2 Configuration File (application.yml)
1 | |
Note:
ddl-auto: updateautomatically updates the table structure based on entity classes, suitable for development and debugging. Disable this in production and use Flyway or Liquibase for database version management.
4.3 Directory Structure
1 | |
5. Entity Classes and Repository Implementation
5.1 Entity Class Code
User Entity:
1 | |
Role Entity:
1 | |
Permission Entity:
1 | |
5.2 Repository Interfaces
1 | |
Explanation: In a bidirectional
@ManyToManyrelationship, you need to specify one side as the owning side (the one that “holds” the foreign key). Here, the owning side for theUser-Rolerelationship isUser(via@JoinTable), whileRoleusesmappedBy. For theRole-Permissionrelationship, the owning side isRole(via@JoinTable). The value ofmappedBymust match the field name in the other entity.
6. Core Business Logic: Role Assignment and Permission Verification
6.1 Role Assignment Service
1 | |
The implementation of RoleService.assignPermissions is similar: use role.getPermissions().addAll(...) and call roleRepository.save(role).
6.2 Permission Verification Service
The core logic of permission verification is to check, via a join query, whether a given user has the permission for a specific resource-action combination.
1 | |
Add a custom query method in UserRepository:
1 | |
This query uses JPA JOIN operations to perform permission checks at the database level, which is efficient. For finer-grained control, you can extend this method, e.g., by adding a resource instance ID (data permission).
7. Exposing Interfaces and Testing (Controller Layer)
7.1 RESTful Interface Design
1 | |
7.2 Unified Response Format
1 | |
7.3 Testing Verification
After starting the Spring Boot project, verify using Postman or curl:
Assign roles to a user:
1
2
3POST /api/users/roles?userId=1
Content-Type: application/json
Body: [1, 2]If user ID is 1 and role IDs are 1 (admin) and 2 (viewer), the user will have both roles.
Check permission:
1
GET /api/users/1/check?resource=order&action=deleteReturns
{"code":200,"message":"success","data":true}, indicating user 1 has permission to delete orders.
Tip: During development, you can enable Swagger (springdoc-openapi) to call interfaces directly from the browser for easy debugging.
8. Advanced Tips: Fine-Grained Permission Control and Cache Optimization
8.1 Data Permission Extensions
The above solution only controls “whether a user can perform an action on a resource” but does not involve specific data rows. For example, a regular salesperson can only view “their own” orders, while a department manager can view “their department’s” orders. This is called data permission or row-level permission.
Extension approach: Add a scope field to the permission table to indicate the scope (e.g., ALL, DEPARTMENT, SELF), and then filter based on the current user context during authorization. For example:
1 | |
8.2 Method-Level Authorization Using AOP
Manually calling hasPermission in every interface method results in repetitive code. You can simplify this by using custom annotations + Spring AOP.
Define the annotation:
1 | |
Intercept in the aspect:
1 | |
Then simply add the annotation on controller methods:
1 | |
8.3 Introducing Redis Cache
Every authorization check requiring a database join query can be inefficient under high concurrency. You can cache the user’s permission set.
1 | |
Modify hasPermission to check the cache:
1 | |
Cache Invalidation Strategy: When a role’s permissions change (e.g., a permission is removed), you need to clear the cache for affected users. You can call redisTemplate.delete(CACHE_KEY_PREFIX + userId) after assignPermissions or removePermissions. For a large number of users, you can use a message queue to perform batch cleanup.
9. Pitfalls and Common Issues
9.1 N+1 Query Problem
@ManyToMany defaults to lazy loading (FetchType.LAZY). If you iterate over a role’s user collection in code, each access triggers an additional SQL query, leading to the N+1 problem. Solutions:
- Use
@EntityGraphto explicitly load associations:1
2
3@EntityGraph(attributePaths = {"roles", "roles.permissions"})
@Query("SELECT u FROM User u WHERE u.id = :id")
Optional<User> findWithRoles(@Param("id") Long id); - Or use
JOIN FETCHin JPQL.
9.2 Circular Dependency and JSON Serialization Infinite Loop
Bidirectional @ManyToMany relationships can cause stack overflow when serialized to JSON due to mutual references. Solutions:
- Add
@JsonIgnoreProperties("users")to therolesfield inUser; - Add
@JsonIgnoreProperties("roles")to theusersfield inRole; - Or use
@JsonIgnoreon the association field on one side to omit it from JSON serialization.
9.3 Immediate Effect of Permission Changes
Cached permission data may become stale. When designing the cache, in addition to setting an expiration time, you should actively invalidate the cache after modifying a role’s permissions. Recommendations:
- In
RoleService‘sassignPermissionsandremovePermissionsmethods, callpermissionCacheService.evictCache(roleId); - If needed, iterate over all users of that role and clear their caches one by one.
9.4 Accidental Cascade Deletion
JPA’s CascadeType.REMOVE will cascade delete records in associated tables. For example, if the Role entity has cascade = CascadeType.REMOVE configured on @ManyToMany, deleting a role will attempt to delete the associated Permission records, which is usually not the intended behavior. Use the database’s own ON DELETE CASCADE only on the foreign keys of the user_role and role_permission association tables, and avoid configuring cascade in the entities as much as possible.
10. Summary and Extensions
This article walked through the design and implementation of a minimal permission center based on the RBAC model, from concepts to code. The key points are summarized as follows:
- Model Value: RBAC decouples users and permissions through roles, reducing the complexity of permission management and supporting flexible batch authorization and revocation.
- Table Structure Design: Five tables (user, role, permission, user-role association, role-permission association) express all semantics of RBAC0; the
permissiontable uses theresource + actioncombination to support fine-grained operation-level control. - Implementation Path: Spring Boot + JPA effectively reduces development costs. Entity classes use
@ManyToManywith@JoinTable, taking care to specify the owning side andmappedBy. Permission checks are done via JOIN queries or caching. - Common Pitfalls: N+1 queries, circular serialization, cache consistency issues, and unintended cascade deletions.
Directions for extension include:
Role Inheritance (RBAC1): Establish hierarchical relationships between roles, where child roles automatically inherit parent role permissions. Add a
parent_idfield to the role table.Separation of Duties (RBAC2): Use constraint rules to prevent a user from having conflicting roles simultaneously (e.g., “cashier” and “accountant”), verifying during role assignment.
Attribute-Based Access Control (ABAC): When permission rules depend on environmental attributes (e.g., IP, time, user context), ABAC is more flexible than RBAC, but the model is more complex.
Integration with Spring Security: Embed the
hasPermissionlogic into aPermissionEvaluator, and use expressions in@PreAuthorizeto further reduce boilerplate code.
Recommended reading: The original “RBAC96 Model” paper and Apache Shiro’s permission design source code will help you understand permission engineering practices in large-scale systems. Subsequent topics could include “Spring Security Integration with RBAC in Practice” and “Comparison of Permission Center Cache Solutions.”
Keywords: RBAC permission model tutorial, build RBAC permission center from scratch, RBAC model design steps, Java implementation of RBAC permission management, many-to-many user-role-permission.
Summary
Through this article, you should now have a deeper understanding of “getting started with the RBAC permission model.” It is recommended to practice more in real projects. If you have any questions, feel free to discuss!