下载工作台
Spring Boot Web 开发

RBAC 与对象级权限

试读上半部分 · 解锁后可读全文

第 16 章 · RBAC、@PreAuthorize 细粒度与对象级权限

本章目标:在 shop-spring-demo 实现 RBAC(角色-权限)数据模型;使用 @PreAuthorize / @PostAuthorize 做方法级授权;通过 PermissionEvaluator 或自定义 @authz 实现对象级权限(仅本人订单、仅负责店铺商品);与 ch15 JWT 角色 Claim 衔接;对比 URL 级、方法级、数据级三道防线。

学时建议:5~6 小时(含 2 小时权限模型跟练)

前置ch06 Spring Security;ch15 JWT 与 roles Claim;ch13 DTO/VO。


16.1 权限模型演进

URL 级(permitAll / authenticated)
        │
        ▼
方法级(@PreAuthorize hasRole)
        │
        ▼
数据级(订单 ownerId == 当前用户)
层级示例不足
URL/api/admin/** → ADMIN无法区分「删他人订单」
方法@PreAuthorize("hasRole('ADMIN')")同角色用户权限相同
对象order.userId == principal.id需查库或 SpEL 服务

shop-spring-demo 目标:

角色能力
ROLE_USER浏览商品、下订单、查自己的订单
ROLE_ADMIN商品 CRUD、查全部订单、用户管理
ROLE_MERCHANT(选修)仅管理自己店铺下的商品
虚构项目 shop-spring-demo;权限表为教学简化版,勿照搬真实生产 RBAC 全量设计。

16.2 RBAC 表结构

┌─────────┐     ┌─────────────┐     ┌────────────┐
│  User   │────►│ user_role   │────►│   Role     │
└─────────┘     └─────────────┘     └─────┬──────┘
                                          │
                                          ▼
                                   ┌──────────────┐
                                   │ role_permission│
                                   └──────┬───────┘
                                          ▼
                                   ┌────────────┐
                                   │ Permission │
                                   └────────────┘

实体简例:

@Entity
public class Role {
    @Id @GeneratedValue
    private Long id;
    @Column(unique = true)
    private String code;  // ADMIN, USER, MERCHANT

    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(name = "role_permission")
    private Set<Permission> permissions = new HashSet<>();
}

@Entity
public class Permission {
    @Id @GeneratedValue
    private Long id;
    private String code;  // product:write, order:read:own
}

UserDetailsService 加载时把 permissions 转为 GrantedAuthority

authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getCode()));
role.getPermissions().forEach(p ->
    authorities.add(new SimpleGrantedAuthority(p.getCode())));

JWT ch15 的 roles Claim 可同步写入 permissions 列表(选修,注意 Token 体积)。


16.3 启用方法级安全

@Configuration
@EnableMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig {}
注解时机典型用途
@PreAuthorize方法执行前角色、权限、SpEL
@PostAuthorize方法执行后根据返回值决定(如返回的 Order 是否属本人)
@PreFilter / @PostFilter集合过滤列表只留有权看的项

16.4 @PreAuthorize 常用表达式

@PreAuthorize("hasRole('ADMIN')")
public Result<ProductDetailVO> createProduct(...) { ... }

@PreAuthorize("hasAuthority('product:write')")
public Result<?> updateProduct(...) { ... }

@PreAuthorize("hasRole('ADMIN') or hasAuthority('order:read:all')")
public Result<PageResult<OrderVO>> listAllOrders(...) { ... }

@PreAuthorize("authentication.name == #username")
public Result<UserProfileVO> getProfile(@PathVariable String username) { ... }

内置表达式:

表达式含义
hasRole('ADMIN')角色(自动加 ROLE_ 前缀)
hasAuthority('product:write')精确权限码
hasAnyRole('USER','ADMIN')任一角色
isAuthenticated()已登录
#id方法参数
authentication.principal当前用户

16.5 对象级权限:订单仅本人可见

方案 A:Service 内显式校验

public OrderDetailVO getOrder(Long orderId, Long currentUserId, boolean isAdmin) {
    Order order = orderRepo.findById(orderId)
            .orElseThrow(() -> new NotFoundException("订单不存在"));
    if (!isAdmin && !order.getUserId().equals(currentUserId)) {
        throw new AccessDeniedException("无权查看该订单");
    }
    return orderMapper.toDetailVO(order);
}

清晰但权限逻辑分散。

方案 B:@PreAuthorize 调用 Bean(推荐)

OrderSecurityService.java

@Service("orderAuthz")
public class OrderSecurityService {

    private final OrderRepository orderRepo;

    public boolean canRead(Long orderId, Authentication auth) {
        if (auth == null || !auth.isAuthenticated()) return false;
        if (auth.getAuthorities().stream().anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"))) {
            return true;
        }
        Long userId = ((ShopUserPrincipal) auth.getPrincipal()).getId();
        return orderRepo.findById(orderId)
                .map(o -> o.getUserId().equals(userId))
                .orElse(false);
    }

    public boolean canCancel(Long orderId, Authentication auth) {
        if (!canRead(orderId, auth)) return false;
        return orderRepo.findById(orderId)
                .map(o -> o.getStatus() == OrderStatus.PENDING_PAYMENT)
                .orElse(false);
    }
}

Controller:

@GetMapping("/api/orders/{id}")
@PreAuthorize("@orderAuthz.canRead(#id, authentication)")
public Result<OrderDetailVO> get(@PathVariable Long id) {
    return Result.ok(orderService.getDetail(id));
}

@PostMapping("/api/orders/{id}/cancel")
@PreAuthorize("@orderAuthz.canCancel(#id, authentication)")
public Result<Void> cancel(@PathVariable Long id) {
    orderService.cancel(id);
    return Result.ok();
}
请求 GET /api/orders/42
        │
        ▼
@PreAuthorize → orderAuthz.canRead(42, auth)
        │
   ┌────┴────┐
   │ ADMIN?  │──是──► 通过
   └────┬────┘
        │ 否
        ▼
 order.userId == principal.id ?
        │
   否 ──► 403 Access Denied
   是 ──► 执行 get()

以下内容需解锁后阅读

试读已结束。解锁本章 ¥5.00,或开通年度会员畅读全部教程。
年度会员 ¥199.00/年; 小紫 AI 工作台有效会员 ¥99.00/年

正文仅在服务端鉴权后下发,未付费无法获取下半部分内容。