1. 引言

在之前关于 Spring 方法级安全控制 的教程中,我们已经了解了如何使用 @PreAuthorize@PostAuthorize 注解来实现方法级别的权限校验。

但今天我们要解决一个更实际的问题:如何确保那些遗漏了权限注解的 Controller 方法,默认情况下被拒绝访问。这在团队协作或大型项目中非常关键——谁还没“手滑”过呢?✅

2. Spring Security 的默认行为

先说结论:Spring Security 并不会自动拒绝未加注解的方法访问

虽然 Spring Security 默认会对所有接口要求认证(authenticated),但它不会强制要求特定角色,更不会因为你没写 @PreAuthorize 就直接拦掉请求。这意味着:

  • 有认证 → 可访问
  • 无注解 → 不代表没权限

这就埋下了安全隐患。比如你写了个新接口,忘了加权限控制,结果所有人都能调——典型的“踩坑”现场。

3. 示例项目结构

我们来看一个简单的 Spring Boot 应用,包含基本的配置和一个存在漏洞的 Controller。

启动类

@SpringBootApplication
public class DenyApplication {
    public static void main(String[] args) {
        SpringApplication.run(DenyApplication.class, args);
    }
}

安全配置

启用方法级安全,并创建两个用户:user(拥有 USER 角色)和 guest(无角色)

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class DenyMethodSecurityConfig {
    @Bean
    public UserDetailsService userDetailsService() {
        return new InMemoryUserDetailsManager(
            User.withUsername("user").password("{noop}password").roles("USER").build(),
            User.withUsername("guest").password("{noop}password").roles().build()
        );
    }
}

存在风险的 Controller

注意 /bye 接口没有加任何权限注解:

@RestController
public class DenyOnMissingController {
    @GetMapping(path = "hello")
    @PreAuthorize("hasRole('USER')")
    public String hello() {
        return "Hello world!";
    }

    @GetMapping(path = "bye")
    // 警告:这里忘了加 @PreAuthorize!
    public String bye() {
        return "Bye bye world!";
    }
}

测试结果:

  • user 用户可以访问 /hello
  • guest 用户无法访问 /hello
  • guest 用户可以访问 /bye ❌ 安全缺口!

4. 编写测试验证问题

我们用 MockMvc 写个集成测试,验证未注解接口是否真的能被访问。

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = DenyApplication.class)
public class DenyOnMissingControllerIntegrationTest {
   
    @Autowired
    private WebApplicationContext context;
    private MockMvc mockMvc;

    @Before
    public void setUp() {
        mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
    }

    @Test
    @WithMockUser(username = "user")
    public void givenANormalUser_whenCallingHello_thenAccessDenied() throws Exception {
        mockMvc.perform(get("/hello"))
          .andExpect(status().isOk())
          .andExpect(content().string("Hello world!"));
    }

    @Test
    @WithMockUser(username = "user")
    // 当前会失败:/bye 仍可访问
    public void givenANormalUser_whenCallingBye_thenAccessDenied() {
        ServletException exception = Assertions.assertThrows(ServletException.class, () -> mockMvc.perform(get("/bye")));

        Assertions.assertNotNull(exception);
        Assertions.assertEquals(exception.getCause().getClass(), AccessDeniedException.class);
    }
}

⚠️ 第二个测试会失败,因为 /bye 确实可以被访问。接下来我们修复这个问题。

5. 解决方案:默认拒绝策略

目标很明确:所有 Controller 中的方法,除非显式标注 @PreAuthorize@PostAuthorize,否则一律拒绝访问

核心思路

通过自定义 AuthorizationManager<MethodInvocation>,拦截所有 Controller 方法调用,动态判断是否应拒绝访问。

配置增强

DenyMethodSecurityConfig 中添加一个优先级更高的拦截器:

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class DenyMethodSecurityConfig {

    @Bean
    public Advisor preAuthorize(CustomPermissionAllowedMethodSecurityMetadataSource manager) {
        JdkRegexpMethodPointcut pattern = new JdkRegexpMethodPointcut();
        pattern.setPattern("com.baeldung.denyonmissing.*");
        AuthorizationManagerBeforeMethodInterceptor interceptor = new AuthorizationManagerBeforeMethodInterceptor(pattern, manager);
        interceptor.setOrder(AuthorizationInterceptorsOrder.PRE_AUTHORIZE.getOrder() - 1);
        return interceptor;
    }
    
    // userDetailsService 省略...
}

自定义权限管理器

关键实现:检查方法是否带有安全注解,若无且类是 @Controller,则默认拒绝。

@Component
public class CustomPermissionAllowedMethodSecurityMetadataSource implements AuthorizationManager<MethodInvocation> {

    private static final GrantedAuthority DENY_ALL_ATTRIBUTE = () -> "DENY_ALL";

    @Override
    public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocation mi) {
        MergedAnnotations annotations = MergedAnnotations.from(mi.getMethod(), MergedAnnotations.SearchStrategy.DIRECT);
        List<ConfigAttribute> attributes = new ArrayList<>();

        // 判断类是否是 Controller
        MergedAnnotations classAnnotations = MergedAnnotations.from(mi.getThis().getClass(), MergedAnnotations.SearchStrategy.DIRECT);
        if (classAnnotations.get(Controller.class).isPresent()) {
            attributes.add(DENY_ALL_ATTRIBUTE);
        }

        // 如果方法上有 @PreAuthorize 或 @PostAuthorize,交由标准流程处理
        if (annotations.get(PreAuthorize.class).isPresent() || annotations.get(PostAuthorize.class).isPresent()) {
            return null; // 返回 null 表示不应用此规则
        }

        // 检查是否有匹配的权限(这里其实是没有的,因为 DENY_ALL 不会被用户拥有)
        boolean granted = !Collections.disjoint(attributes, authentication.get().getAuthorities());
        return new AuthorizationDecision(granted);
    }
}

📌 重点说明

  • return null 是关键,表示当前决策器不介入,交还给 Spring Security 默认逻辑处理有注解的方法。
  • 我们构造了一个特殊的 DENY_ALL 权限,但没有任何用户拥有它,因此最终决策为拒绝。

效果验证

修改完成后,重新运行测试:

  • /hello:正常访问(有 @PreAuthorize
  • /bye:抛出 AccessDeniedException

完美解决“忘记加注解”的安全隐患。

6. 总结

本文提供了一种简单粗暴但极其有效的方案,用于防止因遗漏 @PreAuthorize 导致的安全漏洞:

✅ 所有 Controller 方法默认拒绝访问
✅ 显式标注权限注解的方法正常放行
✅ 不影响已有安全逻辑,兼容性好

这种“默认拒绝”模式非常适合对安全性要求较高的系统,建议在金融、后台管理类项目中强制启用。

示例代码已上传至 GitHub:https://github.com/baeldung/tutorials/tree/master/spring-security-modules/spring-security-core


原始标题:Deny Access on Missing @PreAuthorize to Spring Controller Methods | Baeldung