方法级安全

返回 Spring Boot 基础 | → 安全

方法级安全在 URL 级授权 之外提供更细粒度的权限控制,直接在 Service/Controller 方法上声明权限规则,通过 AOP 代理在方法调用前后执行权限检查。


启用

在配置类上添加 @EnableMethodSecurity(Spring Boot 3.x / Spring Security 6.x 推荐):

@Configuration
@EnableWebSecurity
@EnableMethodSecurity(
    prePostEnabled = true,    // 启用 @Pre/@PostAuthorize(默认 true)
    securedEnabled = true,    // 启用 @Secured
    jsr250Enabled = true      // 启用 @RolesAllowed
)
public class SecurityConfig { ... }

Spring Security 5.x 使用的是 @EnableGlobalMethodSecurity,6.x 已废弃,统一改用 @EnableMethodSecurity


核心注解

@PreAuthorize — 调用前检查(最常用)

// 角色检查
@PreAuthorize("hasRole('ADMIN')")
public void deleteUser(Long id) { ... }
 
// 多角色(任一满足)
@PreAuthorize("hasAnyRole('ADMIN', 'MANAGER')")
public List<User> listAll() { ... }
 
// 精确权限字符串
@PreAuthorize("hasAuthority('user:delete')")
public void deleteUser(Long id) { ... }
 
// 逻辑组合
@PreAuthorize("hasRole('ADMIN') or hasAuthority('order:export')")
public void exportOrders() { ... }
 
// 访问方法参数(#参数名)
@PreAuthorize("hasRole('ADMIN') or #userId == authentication.principal.id")
public UserProfile getProfile(Long userId) { ... }
 
// 访问对象字段
@PreAuthorize("#order.status == 'DRAFT' and hasRole('EDITOR')")
public void submitOrder(Order order) { ... }

@PostAuthorize — 调用后检查返回值

// returnObject 指代返回值
@PostAuthorize("returnObject.owner == authentication.name")
public Document getDocument(Long id) { ... }
 
// 用于"只有资源所有者才能看到自己的数据"场景
@PostAuthorize("returnObject.userId == authentication.principal.id")
public Order getOrder(Long orderId) { ... }

@PostAuthorize 方法已经执行并产生副作用后才做权限检查,不适合有写操作的方法。

@PreFilter / @PostFilter — 集合过滤

// 过滤入参集合(保留满足条件的元素)
@PreFilter("filterObject.owner == authentication.name")
public void batchDelete(List<Document> docs) { ... }
 
// 过滤返回集合
@PostFilter("filterObject.owner == authentication.name")
public List<Document> findAll() { ... }

集合较大时性能差,建议在查询层直接加条件过滤。

@Secured(简单角色检查)

@Secured("ROLE_ADMIN")                      // 注意需要 ROLE_ 前缀
public void adminOperation() { ... }
 
@Secured({"ROLE_ADMIN", "ROLE_MANAGER"})    // 任一满足
public void managerOperation() { ... }

@RolesAllowed(JSR-250)

@RolesAllowed("ADMIN")                      // 无需 ROLE_ 前缀
public void adminOperation() { ... }

SpEL 表达式速查

方法级安全基于 SpEL 表达式,可用内置变量:

变量类型说明
authenticationAuthentication当前认证对象
authentication.nameString当前用户名
authentication.principalObjectPrincipal(通常是 UserDetails
authentication.authoritiesCollection权限集合
principalObjectauthentication.principal 的快捷方式
#参数名对应类型方法入参(需编译保留参数名或用 -parameters
returnObject对应类型方法返回值(仅 @PostAuthorize/@PostFilter
filterObject集合元素类型当前迭代元素(仅 @PreFilter/@PostFilter
hasRole(r)boolean是否拥有角色(自动补 ROLE_ 前缀)
hasAuthority(a)boolean是否拥有精确权限字符串
isAnonymous()boolean是否匿名用户
isAuthenticated()boolean是否已登录
permitAllboolean始终 true
denyAllboolean始终 false

自定义权限评估器

复杂权限逻辑(如从数据库查询 ACL)可实现 PermissionEvaluator

// 1. 实现 PermissionEvaluator
@Component
public class DocumentPermissionEvaluator implements PermissionEvaluator {
 
    private final DocumentAclService aclService;
 
    @Override
    public boolean hasPermission(Authentication auth,
                                 Object targetDomainObject,
                                 Object permission) {
        if (targetDomainObject instanceof Document doc) {
            return aclService.hasPermission(
                auth.getName(), doc.getId(), permission.toString());
        }
        return false;
    }
 
    @Override
    public boolean hasPermission(Authentication auth,
                                 Serializable targetId,
                                 String targetType,
                                 Object permission) {
        return aclService.hasPermission(
            auth.getName(), (Long) targetId, permission.toString());
    }
}
 
// 2. 注册
@Configuration
public class MethodSecurityConfig {
 
    @Bean
    public MethodSecurityExpressionHandler expressionHandler(
            DocumentPermissionEvaluator evaluator) {
        var handler = new DefaultMethodSecurityExpressionHandler();
        handler.setPermissionEvaluator(evaluator);
        return handler;
    }
}
 
// 3. 使用 hasPermission()
@PreAuthorize("hasPermission(#docId, 'Document', 'READ')")
public Document getDocument(Long docId) { ... }
 
@PreAuthorize("hasPermission(#doc, 'WRITE')")
public void updateDocument(Document doc) { ... }

数据权限(行级过滤)

结合 AOP 实现 SaaS 租户隔离或用户数据隔离:

@Aspect
@Component
public class DataScopeAspect {
 
    @Around("@annotation(dataScope)")
    public Object filter(ProceedingJoinPoint pjp, DataScope dataScope)
            throws Throwable {
        LoginUser user = SecurityUtils.getCurrentUser();
        // 将当前用户的数据范围注入查询条件(如 MyBatis 拦截器)
        DataScopeContextHolder.set(user.getDeptIds());
        try {
            return pjp.proceed();
        } finally {
            DataScopeContextHolder.clear();
        }
    }
}

注意事项

  • 方法级安全基于 AOP 代理,同类内部调用不生效(绕过代理),需通过 ApplicationContext 获取代理对象或将方法移到另一个 Bean
  • @PreAuthorize 加在 private 方法上无效,必须是 public
  • 参数名访问(#参数名)需编译时保留参数名:javac -parameters 或 Spring Boot 默认开启
  • @Transactional 同时使用时,注意两个切面的 @Order 顺序

相关链接

  • 安全 — 全局认证与 URL 级授权配置
  • AOP — 方法级安全的底层代理机制
  • SpEL表达式 — 权限表达式语法详解
  • OAuth2与JWT — JWT 中 Claims 映射为权限