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