Spring Boot应用多租户场景下如何优雅实现资源访问权限校验?
Spring Boot 层级资源域权限控制优雅实现方案
方案1:Spring Security 方法级注解 + SpEL 表达式(最常用)
利用Spring Security原生的方法权限校验能力,把权限逻辑从业务代码中解耦:
- 首先开启方法级安全配置,Spring Security 6+ 版本在配置类添加注解:
@EnableMethodSecurity public class SecurityConfig { // 其他安全配置 }
- 把你的
PermissionManager注册为Spring Bean,在校验注解中直接通过SpEL调用。比如Workspace的操作方法直接加注解:
// 按ID操作场景,直接传ID校验不需要提前查询实体 @PreAuthorize("@permissionManager.checkWorkspaceAccess(authentication.principal, #workspaceId)") public void deleteWorkspace(Long workspaceId) { // 业务逻辑 } // 入参是实体的场景 @PreAuthorize("@permissionManager.checkWorkspaceAccess(authentication.principal, #workspace)") public void updateWorkspace(Workspace workspace) { // 业务逻辑 }
- WorkItem的校验同理,注解直接绑定到对应Service方法上,无需在业务代码中手动调用校验逻辑。
方案2:策略模式重构PermissionManager,解决类臃肿问题
如果资源类型持续增加,不要把所有校验逻辑都堆在PermissionManager里,用策略模式拆分实现:
- 定义通用校验接口:
public interface ResourcePermissionChecker<T> { /** * 校验用户是否有权限操作资源 * @return 有权限返回true,否则返回false */ boolean check(User currentUser, T resource); /** * 返回当前校验器支持的资源类型 */ Class<T> getSupportedResourceType(); }
- 每个资源类型单独实现校验器:
// Workspace校验器 @Component public class WorkspacePermissionChecker implements ResourcePermissionChecker<Workspace> { @Override public boolean check(User currentUser, Workspace workspace) { return Objects.equals(currentUser.getId(), workspace.getOwner().getId()); } @Override public Class<Workspace> getSupportedResourceType() { return Workspace.class; } } // WorkItem校验器 @Component public class WorkItemPermissionChecker implements ResourcePermissionChecker<WorkItem> { @Override public boolean check(User currentUser, WorkItem workItem) { return Objects.equals(currentUser.getId(), workItem.getWorkspace().getOwner().getId()); } @Override public Class<WorkItem> getSupportedResourceType() { return WorkItem.class; } }
- PermissionManager只做校验器分发,无需关心具体校验逻辑:
@Component public class PermissionManager { private final Map<Class<?>, ResourcePermissionChecker<?>> checkerMap; // 自动注入所有实现了ResourcePermissionChecker的Bean public PermissionManager(List<ResourcePermissionChecker<?>> checkers) { this.checkerMap = checkers.stream() .collect(Collectors.toMap(ResourcePermissionChecker::getSupportedResourceType, Function.identity())); } @SuppressWarnings("unchecked") public <T> boolean checkAccess(User currentUser, T resource) { ResourcePermissionChecker<T> checker = (ResourcePermissionChecker<T>) checkerMap.get(resource.getClass()); if (checker == null) { // 无对应校验器可根据业务规则默认放行/拒绝 throw new IllegalArgumentException("未找到资源类型对应的权限校验器:" + resource.getClass().getName()); } return checker.check(currentUser, resource); } }
后续新增资源类型只要新增对应ResourcePermissionChecker实现即可,完全不需要修改PermissionManager原有代码,符合开闭原则。
方案3:查询层全局过滤(适合读多写少场景)
如果大部分操作是查询,可以用JPA/Hibernate的全局过滤器,直接在查询阶段就过滤掉不属于当前用户的资源:
- 给实体类添加过滤注解:
@Entity @Filter(name = "ownerFilter", condition = "owner_id = :currentUserId") public class Workspace { // 实体属性 }
- 在请求拦截器中获取当前登录用户ID,绑定到JPA的过滤器上下文,后续所有Workspace的查询都会自动带上
owner_id = 当前用户ID的条件,天然不会返回其他用户的资源,无需额外校验。
方案4:统一拦截器校验(适合RESTful路径规范的项目)
如果你的接口遵循层级RESTful规范,比如路径格式为/workspaces/{workspaceId}/work-items/{workItemId},可以直接在Spring MVC拦截器中统一解析路径参数,提前完成权限校验,所有同层级的接口都不需要单独加校验逻辑。
优化建议
- 校验优先用ID查询判断,不要提前查询完整实体:比如校验WorkItem权限时,直接用关联查询SQL判断
workItemId对应的Workspace是否属于当前用户,减少不必要的数据库查询 - 新增资源场景直接绑定当前用户:比如新增Workspace时直接把当前登录用户设置为owner,不需要额外校验
内容的提问来源于stack exchange,提问作者Arthur Klezovich
相关产品推荐
相关产品推荐

