A Complete Guide to Backend Access Control: RBAC, Dynamic Routing, and Data Permissions

The article dissects backend permission control by explaining RBAC fundamentals, illustrating how dynamic routing improves UI experience while backend authorization enforces security, detailing data‑scope enforcement, role inheritance limits, session handling, revocation mechanisms, and practical implementation steps with code examples and testing guidelines.

Tech Ocean
Tech Ocean
Tech Ocean
A Complete Guide to Backend Access Control: RBAC, Dynamic Routing, and Data Permissions
Menu hiding is not sufficient; true permission control prevents users from seeing or modifying data they are not authorized for.

Typical admin panels start with a simple rule—admins see all menus, regular staff see a few. As business logic grows, problems appear: a salesperson can change an order ID in the URL to view another salesperson's order, a regular employee can call an approval API directly, and a revoked admin can continue acting with an old token.

These issues stem from controlling only what is displayed on the page without answering four essential questions:

Who are you?

What actions can you perform?

Which data can you operate on?

Under what conditions is the operation allowed?

The author recommends viewing backend permission as five layers: RBAC as the foundation, dynamic routing for front‑end experience, backend authentication for API protection, data‑scope rules, and business‑condition checks.

1. What RBAC Actually Governs

RBAC (Role‑Based Access Control) follows the classic user → role → permission chain. Example:

张三 → 销售经理 → 查看订单、创建订单、导出订单
李四 → 财务专员 → 查看账单、确认收款

Benefits include easy role‑based changes when personnel shift. The formal model (NIST RBAC FAQ) also includes sessions, role hierarchies, and separation of duties.

RBAC mainly solves the first layer (functional permission). The other layers—menu visibility, data range, business state, amount limits, device risk, tenant isolation—should not be hard‑coded into role names.

Functional permission : can a specific action be executed? Example: order:create Menu permission : which UI entries are shown? Example: Order Management, Finance Center

Data permission : which records are accessible? Example: only self, own department, specific region

Conditional permission : is the current state allowed? Example: order must be PENDING and amount below limit

Tenant boundary : which organization owns the data? Example: users of Company A cannot access Company B

2. Maintainable Database Design

A minimal schema uses five tables:

sys_user            // 用户
sys_role            // 角色
sys_permission      // 权限
sys_user_role       // 用户‑角色关联
sys_role_permission // 角色‑权限关联

The core query chain is:

user_id → sys_user_role → sys_role_permission → permission_code

Permission codes follow a resource+action pattern, e.g.:

order:list
order:detail
order:create
order:update
order:approve
order:export

Menus should be stored separately ( sys_menu) and linked to permission codes; a single menu can map to multiple permissions:

订单管理菜单
  ├─ order:list
  ├─ order:detail
  ├─ order:create
  ├─ order:update
  └─ order:export

Role inheritance reduces duplication but deep hierarchies make tracing difficult. Limit inheritance depth and provide an explanation of permission sources (direct, inherited, parent role).

3. Dynamic Routing Shapes UI, Backend Auth Secures It

Front‑end dynamic routing hides unauthorized menus/buttons and shows friendly prompts for illegal URLs, but the decisive check lives on the server.

A complete request flow:

登录成功
  → 后端返回当前用户的菜单与权限码
  → 前端注册动态路由
  → 用户发起请求
  → 后端校验身份、动作权限、租户、数据范围和业务条件
  → 全部通过后才访问数据

Vue example for safely registering routes:

const componentMap = {
  OrderList: () => import('@/views/order/list.vue'),
  OrderDetail: () => import('@/views/order/detail.vue')
}
function registerRoutes(routes) {
  for (const item of routes) {
    const name = item.componentName
    const component = componentMap[name]
    if (!component) continue
    router.addRoute({
      name: item.name,
      path: item.path,
      component,
      meta: { permission: item.permission }
    })
  }
}

After registration, re‑match the current URL if it matches the newly added route:

registerRoutes(await fetchRoutes())
const route = router.currentRoute.value
await router.replace(route.fullPath)

Spring Security configuration shows how to bind method‑level checks with URL patterns:

@Configuration
@EnableMethodSecurity
public class SecurityConfig {
  @Bean
  SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    String approvePath = "/api/orders/*/approve";
    return http.authorizeHttpRequests(auth -> auth
        .requestMatchers("/login", "/actuator/health").permitAll()
        .requestMatchers(HttpMethod.POST, approvePath).hasAuthority("order:approve")
        .requestMatchers("/api/**").authenticated()
        .anyRequest().denyAll()
    ).build();
  }
}

Common pitfalls:

Enable @EnableMethodSecurity explicitly.

Methods without annotations are unprotected; unknown URLs should be denied by default.

Self‑invocation can bypass Spring AOP proxies.

4. Data Permissions Are the Hardest Layer

Having order:detail only proves the user can perform the "view order" action, not that they can view any order. The query must also enforce data scope such as "owner only" or "department only".

Typical data‑scope categories:

Only self

Own department

Department and sub‑departments

Specific region or project

All data

List query should embed the OR of multiple scopes directly in SQL:

SELECT id, order_no, owner_id, dept_id, status
FROM biz_order
WHERE tenant_id = #{tenantId}
  AND (
    owner_id = #{userId}
    OR dept_id IN (/* allowedDeptIds */)
  )
ORDER BY created_at DESC

If allowedDeptIds is empty, the clause 1 = 0 prevents accidental full‑table scans.

MyBatis can pass a DataScope object to the mapper, or an interceptor can rewrite SQL, but the latter must be thoroughly tested.

Detail queries should embed permission checks in the WHERE clause to avoid a separate existence check:

SELECT id, order_no, status
FROM biz_order
WHERE id = #{orderId}
  AND tenant_id = #{tenantId}
  AND owner_id = #{userId}
LIMIT 1

If no row is returned, respond with a generic "record not found or no permission" message to avoid enumeration.

Write operations must be atomic. Example UPDATE that combines data‑scope, business state, and optimistic lock:

UPDATE biz_order
SET status = 'APPROVED',
    version = version + 1,
    approved_by = #{userId},
    approved_at = CURRENT_TIMESTAMP
WHERE id = #{orderId}
  AND tenant_id = #{tenantId}
  AND dept_id IN (/* allowed departments */)
  AND status = 'PENDING'
  AND version = #{expectedVersion}

If the affected row count is not 1, treat it as a failure (possible stale data, state change, or lock conflict) and log the precise cause internally while keeping the external error vague.

5. Business Rules and Separation of Duties

Some constraints depend on the current object, amount, or time, not just static permissions. Examples:

Only orders in PENDING state can be approved.

Amounts over 100 000 require a higher‑level approver.

Applicants cannot approve their own requests.

After finance confirms receipt, sales cannot modify the amount.

Operations outside working hours need secondary authentication.

These rules belong in backend services. A Spring @PreAuthorize expression can combine action permission and a custom policy bean:

@PreAuthorize("hasAuthority('order:approve') and @orderAuth.canApprove(authentication, #orderId)")
public void approveOrder(Long orderId) { /* ... */ }

Two forms of duty separation:

Static: mutually exclusive roles cannot be assigned to the same user.

Dynamic: roles may coexist but cannot be used simultaneously within the same session or workflow.

6. Making Permission Changes Take Effect Promptly

Revoking rights is harder than granting them because cached permissions, Redis entries, or JWTs may still allow actions.

Robust revocation workflow (all steps in one DB transaction):

1. Update user/role/permission relations
2. Increment authz_version for affected users/roles
3. Write an Outbox event
--- after commit ---
4. Clear local and Redis caches
5. Publish the invalidation event for other nodes (idempotent)
6. Use a short TTL as a safety net

The version number is stored in the session or token; high‑risk endpoints compare the token's version with the current server version and force re‑login if they differ.

JWTs are not a revocation magic. Common strategies:

High‑risk checks use authz_version or server‑side session.

Maintain a short‑lived deny‑list for revoked tokens.

Shorten access‑token lifespan and refresh permissions on each renewal.

7. Practical Rollout Steps

Inventory resources and actions (e.g., order:list, order:create, payment:approve).

Define mandatory boundaries: tenant identification, visible organizations, mutable states, duty‑separation rules, and secondary‑auth requirements.

Implement backend defaults: deny‑by‑default, per‑request authz, data‑scoped queries, and row‑count verification before UI polishing.

Validate with a test matrix covering missing permission, cross‑tenant access, concurrent approvals, self‑approval, stale sessions, and privilege‑escalation attempts.

Audit every permission change: who, when, what was added/removed, version before/after, source, request ID, and result (redact sensitive data).

Final principle (illustrated in the article’s concluding image): "Front‑end permissions make the UI convenient; back‑end permissions make the system trustworthy."

Backend permission control five‑layer defense
Backend permission control five‑layer defense
Complete auth chain from login to data response
Complete auth chain from login to data response
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

access controldynamic routingRBACbackend securitydata permissionsSpring Securityrole inheritance
Tech Ocean
Written by

Tech Ocean

Focused on AI programming, sharing ready-to-use development efficiency solutions.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.