Skip to content

权限控制与越权漏洞

越权漏洞概述

越权漏洞是Web应用中最常见的安全问题之一,指用户能够访问或操作超出其权限范围的资源。根据越权方向的不同,可分为水平越权和垂直越权两种类型。

mermaid
flowchart TB
    subgraph PrivilegeEscalation["越权漏洞分类"]
        direction LR
        subgraph Horizontal["水平越权"]
            H1["同级用户之间"]
            H2["访问他人数据"]
            H3["如:查看其他用户订单"]
        end
        
        subgraph Vertical["垂直越权"]
            V1["跨级别权限"]
            V2["获取更高权限"]
            V3["如:普通用户执行管理操作"]
        end
    end
    
    style Horizontal fill:#e3f2fd,stroke:#1976d2,rx:15
    style Vertical fill:#fff3e0,stroke:#ef6c00,rx:15

水平越权攻击

攻击原理

水平越权是指攻击者访问与自己具有相同权限等级的其他用户的资源。最典型的场景是通过修改请求参数中的资源标识(如用户ID、订单号)来获取他人数据。

攻击场景示例

假设有一个电商平台的订单查询接口:

plain
GET /api/orders/10001

如果系统仅根据URL中的订单号查询数据,攻击者只需修改订单号即可尝试获取其他用户的订单信息:

plain
GET /api/orders/10002
GET /api/orders/10003
...

若订单号使用自增ID生成,攻击者可以轻松遍历获取大量订单数据。

mermaid
sequenceDiagram
    participant Attacker as 攻击者
    participant Server as 服务端
    participant Database as 数据库
    
    Attacker->>Server: GET /api/orders/10001 (自己的订单)
    Server->>Database: 查询订单10001
    Database-->>Server: 返回订单数据
    Server-->>Attacker: 订单详情
    
    Note over Attacker: 发现规律后尝试遍历
    
    Attacker->>Server: GET /api/orders/10002 (他人订单)
    Server->>Database: 查询订单10002
    Database-->>Server: 返回订单数据
    Server-->>Attacker: 他人订单详情 (越权成功)

防护方案

方案一:强制关联用户身份

最有效的防护方式是在查询时强制关联当前用户身份,而非信任前端传递的用户标识:

java
@RestController
@RequestMapping("/api/orders")
public class OrderController {
    
    @Autowired
    private OrderService orderService;
    
    /**
     * 安全的订单查询接口
     * 从Session中获取用户ID,而非请求参数
     */
    @GetMapping("/{orderId}")
    public ResponseEntity<OrderVO> getOrder(
            @PathVariable Long orderId,
            HttpSession session) {
        
        // 从Session获取当前登录用户ID
        Long currentUserId = (Long) session.getAttribute("userId");
        if (currentUserId == null) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
        }
        
        // 查询时强制关联用户ID
        Order order = orderService.findByIdAndUserId(orderId, currentUserId);
        if (order == null) {
            return ResponseEntity.notFound().build();
        }
        
        return ResponseEntity.ok(OrderVO.fromEntity(order));
    }
}

@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
    
    /**
     * 查询订单时必须匹配用户ID
     */
    @Query("SELECT o FROM Order o WHERE o.id = :orderId AND o.userId = :userId")
    Order findByIdAndUserId(@Param("orderId") Long orderId, 
                            @Param("userId") Long userId);
}

方案二:资源标识加密混淆

使用加密或编码技术使资源标识难以被猜测和遍历:

java
@Service
public class IdObfuscationService {
    
    private final Sqids sqids;
    
    public IdObfuscationService() {
        // 使用自定义字符表,确保每个应用实例生成的编码不同
        SqidsOptions options = new SqidsOptions();
        options.setAlphabet("TGEpuRNDVtYvISsh34jz5c1db8eoPin6CJUgQwMAmLK9Farl2fW0OyHxqXkBZ7");
        options.setMinLength(8);
        this.sqids = new Sqids(options);
    }
    
    /**
     * 编码ID,加入时间戳使每次编码结果不同
     */
    public String encode(Long id) {
        long timestamp = System.currentTimeMillis() / 1000;
        return sqids.encode(Arrays.asList(id, timestamp));
    }
    
    /**
     * 解码获取原始ID
     */
    public Long decode(String encodedId) {
        List<Long> decoded = sqids.decode(encodedId);
        if (decoded.isEmpty()) {
            throw new IllegalArgumentException("无效的资源标识");
        }
        return decoded.get(0);
    }
}

使用编码后的标识进行接口访问:

java
@GetMapping("/orders/{encodedOrderId}")
public ResponseEntity<OrderVO> getOrder(
        @PathVariable String encodedOrderId,
        HttpSession session) {
    
    Long orderId = idObfuscationService.decode(encodedOrderId);
    Long currentUserId = (Long) session.getAttribute("userId");
    
    Order order = orderService.findByIdAndUserId(orderId, currentUserId);
    // ... 后续处理
}

方案三:请求频率限制

限制单用户在单位时间内的请求次数,增加遍历攻击成本:

java
@Component
public class RequestRateLimitInterceptor implements HandlerInterceptor {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    private static final int MAX_REQUESTS_PER_MINUTE = 60;
    
    @Override
    public boolean preHandle(HttpServletRequest request, 
                            HttpServletResponse response, 
                            Object handler) {
        
        String userId = getCurrentUserId(request);
        String endpoint = request.getRequestURI();
        String key = String.format("rate_limit:%s:%s", userId, endpoint);
        
        Long count = redisTemplate.opsForValue().increment(key);
        if (count == 1) {
            redisTemplate.expire(key, 1, TimeUnit.MINUTES);
        }
        
        if (count > MAX_REQUESTS_PER_MINUTE) {
            response.setStatus(HttpServletResponse.SC_TOO_MANY_REQUESTS);
            return false;
        }
        
        return true;
    }
}

垂直越权攻击

攻击原理

垂直越权是指低权限用户通过某些手段执行了高权限用户才能进行的操作。例如普通用户执行管理员功能、一般员工访问高级管理接口等。

攻击场景示例

系统后台提供了用户管理接口,正常情况下只有管理员可以访问:

plain
POST /admin/users/delete
Content-Type: application/json

{"userId": 12345}

如果后端仅依赖前端隐藏管理入口而未进行权限校验,攻击者可以直接构造请求调用管理接口:

mermaid
sequenceDiagram
    participant Admin as 管理员
    participant User as 普通用户
    participant Server as 服务端
    
    Admin->>Server: POST /admin/users/delete (正常操作)
    Server-->>Admin: 删除成功
    
    Note over User: 发现管理接口
    
    User->>Server: POST /admin/users/delete (越权尝试)
    
    alt 存在漏洞
        Server-->>User: 删除成功 (垂直越权)
    else 有权限校验
        Server-->>User: 403 Forbidden
    end

防护方案

基于RBAC的权限控制

实现完善的基于角色的访问控制(Role-Based Access Control):

java
/**
 * 权限注解
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequireRole {
    String[] value();
}

/**
 * 权限校验拦截器
 */
@Component
public class RbacInterceptor implements HandlerInterceptor {
    
    @Autowired
    private UserService userService;
    
    @Override
    public boolean preHandle(HttpServletRequest request, 
                            HttpServletResponse response, 
                            Object handler) throws Exception {
        
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        RequireRole requireRole = handlerMethod.getMethodAnnotation(RequireRole.class);
        
        if (requireRole == null) {
            requireRole = handlerMethod.getBeanType().getAnnotation(RequireRole.class);
        }
        
        if (requireRole == null) {
            return true;
        }
        
        // 获取当前用户角色
        Long userId = (Long) request.getSession().getAttribute("userId");
        Set<String> userRoles = userService.getUserRoles(userId);
        
        // 检查是否拥有所需角色
        for (String role : requireRole.value()) {
            if (userRoles.contains(role)) {
                return true;
            }
        }
        
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        response.getWriter().write("{\"error\": \"权限不足\"}");
        return false;
    }
}

在Controller中使用权限注解:

java
@RestController
@RequestMapping("/admin/users")
@RequireRole("ADMIN")  // 类级别权限要求
public class AdminUserController {
    
    @PostMapping("/delete")
    @RequireRole({"ADMIN", "SUPER_ADMIN"})  // 方法级别权限要求
    public ResponseEntity<Void> deleteUser(@RequestBody DeleteUserRequest request) {
        userService.deleteUser(request.getUserId());
        return ResponseEntity.ok().build();
    }
    
    @GetMapping("/list")
    @RequireRole("ADMIN")
    public ResponseEntity<List<UserVO>> listUsers() {
        return ResponseEntity.ok(userService.getAllUsers());
    }
}

权限数据模型设计

mermaid
erDiagram
    USER ||--o{ USER_ROLE : has
    ROLE ||--o{ USER_ROLE : belongs_to
    ROLE ||--o{ ROLE_PERMISSION : has
    PERMISSION ||--o{ ROLE_PERMISSION : belongs_to
    
    USER {
        Long id PK
        String username
        String password
        Boolean enabled
    }
    
    ROLE {
        Long id PK
        String name
        String description
    }
    
    PERMISSION {
        Long id PK
        String code
        String name
        String resource
        String action
    }
    
    USER_ROLE {
        Long user_id FK
        Long role_id FK
    }
    
    ROLE_PERMISSION {
        Long role_id FK
        Long permission_id FK
    }

最小权限原则

配置权限时应遵循最小权限原则,只授予用户完成工作所需的最低权限:

java
@Configuration
public class RbacConfiguration {
    
    @Bean
    public RoleHierarchy roleHierarchy() {
        RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
        // 定义角色层级关系
        hierarchy.setHierarchy(
            "ROLE_SUPER_ADMIN > ROLE_ADMIN\n" +
            "ROLE_ADMIN > ROLE_MANAGER\n" +
            "ROLE_MANAGER > ROLE_USER"
        );
        return hierarchy;
    }
}

综合防护最佳实践

防护策略对比

越权类型核心问题关键防护措施
水平越权资源归属校验缺失强制关联用户身份、ID混淆加密
垂直越权权限校验不完善RBAC权限控制、接口级鉴权

安全检查清单

mermaid
flowchart TB
    subgraph Checklist["越权漏洞检查清单"]
        direction TB
        C1["1. 所有接口是否都有权限校验?"]
        C2["2. 资源查询是否关联了用户身份?"]
        C3["3. 资源标识是否容易被猜测?"]
        C4["4. 是否有请求频率限制?"]
        C5["5. 敏感操作是否有二次验证?"]
        C6["6. 权限变更是否有审计日志?"]
        
        C1 --> C2 --> C3 --> C4 --> C5 --> C6
    end
    
    style Checklist fill:#e8f5e9,stroke:#4caf50,rx:15

安全编码规范

  1. 永远不信任客户端数据:用户身份从服务端Session获取,不从请求参数读取
  2. 实施纵深防御:多层权限校验,即使一层被绕过也有其他层保护
  3. 使用声明式权限控制:通过注解或配置统一管理权限,避免分散的硬编码
  4. 记录安全审计日志:所有敏感操作都要留痕,便于事后追溯
  5. 定期安全测试:使用自动化工具和人工渗透测试发现潜在漏洞

更新: 2025-12-04 17:36:44
原文: https://www.yuque.com/u22210564/zoxfmt/doc-20-03

Java 后端面试知识库