第 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()