接口拦住了,Service 却能直接调?Spring Security 方法级授权补上这一层
给 HTTP 路由配置了访问规则,不代表同一业务方法从定时任务、消息消费者或另一个 Service 被调用时也会自动执行这条路由规则。对于“只有具备某项权限才能读取报表”这类业务约束,可以把检查放在 Spring 管理的业务方法入口。Spring Security 的方法授权文档明确区分了请求级和方法级授权,并说明后者默认不会随 Security starter 自动开启。
以下示例针对 Spring Security 7.1 参考文档。前提是项目已经接入 Spring Security,并且调用时有认证上下文;它不负责创建用户、发放权限或配置登录。示例代码依据官方文档整理,尚未在本仓库运行验证。
开启方法授权
在应用的配置类上启用方法授权:
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
@Configuration
@EnableMethodSecurity
class MethodSecurityConfig {
}然后在 Spring 管理的 Service 方法上写明所需权限:
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
@Service
class ReportService {
@PreAuthorize("hasAuthority('report:read')")
public String read(long id) {
return "report-" + id; // 演示返回值,实际项目改为数据读取逻辑
}
}这里检查的是 Authentication 中名为 report:read 的 authority。hasRole('ADMIN') 则按角色语义检查,通常对应 ROLE_ADMIN;两者不要在接口、数据库和令牌映射中混用。@EnableMethodSecurity 默认启用 @PreAuthorize 等前后置注解;旧代码中的 @EnableGlobalMethodSecurity 已被新注解取代。上述行为可在官方方法授权说明核对。
最容易漏掉的调用路径
方法授权基于 Spring AOP 代理。若同一个对象的其他方法写 this.read(id),调用不会经过代理,因而不会触发该方法上的授权拦截器。这不是配置开关问题,而是Spring AOP 自调用机制的直接结果。把需要被保护的方法放在独立的 Spring Bean 中,由其他 Bean 调用;不要把一个内部自调用当成已受保护的入口。
另一个边界是非 HTTP 任务:授权失败会抛出访问拒绝异常,但没有 HTTP 过滤器替它转换为 403 响应。消费者或定时任务要决定由谁提供认证上下文,以及如何处理拒绝结果。方法授权不会自动替代请求入口的身份认证。
发布前验证清单
在实际项目中至少测试三种情况:具备 report:read 的账号可以调用;缺少该权限的账号被拒绝;从另一个 Spring Bean 调用时也被拒绝。再检查是否有 this 自调用、private 或 final 方法,以及调用线程是否真的携带预期的认证上下文。本文示例只说明拦截点,未做真实项目的认证链与集成测试。