禁止在 Spring 控制器方法中缺少 @PreAuthorize 註解時訪問 或者 缺少 @PreAuthorize 註解將禁止訪問 Spring 控制器方法

Spring Security
Remote
1
06:40 AM · Nov 30 ,2025

1. 簡介

在我們的關於 Spring method security 的教程中,我們看到了如何使用 @PreAuthorize@PostAuthorize 註解。

在本教程中,我們將看到 如何拒絕訪問缺少授權註解的方法

2. 默認安全保障

畢竟,我們畢竟是人,可能會忘記保護某個端點。不幸的是,無法輕鬆阻止對未標註端點的訪問。

幸運的是,Spring Security 默認要求所有端點進行身份驗證。但是,它不會要求特定的角色。此外,它不會在未添加安全註解時阻止訪問

3. 設置

首先,讓我們來查看一下這個示例中的應用程序。我們有一個簡單的 Spring Boot 應用程序:

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

其次,我們有一個安全配置。我們設置了兩個用户並啓用了 pre/post 註解:

@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()
        );
    }
}

最後,我們有一個帶有兩個方法的 rest controller。但是,我們“忘記”保護 /bye 端點:

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

    @GetMapping(path = "bye")
    // whoops!
    public String bye() {
        return "Bye bye world!";
    }
}

運行示例時,我們可以使用 user/password 登錄。然後,我們訪問 /hello 端點。 也可以使用 guest/guest 登錄。在這種情況下,我們無法訪問 /hello 端點。

但是,任何已認證的用户都可以訪問 /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")
    // 這將由於下個部分的變化而失敗
    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. 解決方案:默認拒絕

讓我們擴展我們的 MethodSecurityConfig 類並設置一個 MethodSecurityMetadataSource

@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;
    }
    
    // 設置內存中的用户不重複
    ....
}

現在讓我們實現 MethodSecurityMetadataSource 接口:

@Component
public class CustomPermissionAllowedMethodSecurityMetadataSource implements AuthorizationManager<MethodInvocation> {

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

        MergedAnnotations classAnnotations = MergedAnnotations.from(DenyOnMissingController.class,  MergedAnnotations.SearchStrategy.DIRECT);
        // 如果類被註釋為 @Controller,則默認拒絕對所有方法的訪問
        if (classAnnotations.get(Controller.class).isPresent()) {
            attributes.add(DENY_ALL_ATTRIBUTE);
        }

        if (annotations.get(PreAuthorize.class).isPresent() || annotations.get(PostAuthorize.class).isPresent()) {
            return null;
        }
        return new AuthorizationDecision(!Collections.disjoint(attributes, authentication.get().getAuthorities()));
    }
}

我們將向 DENY_ALL_ATTRIBUTE 添加到所有 @Controller 類的方法中。

但是,如果找到 @PreAuthorize / @PostAuthorize 註解,則我們不這樣做,而是返回 null,這表明沒有元數據適用

通過更新後的代碼,我們的 /bye 端點已受保護,測試通過。

6. 結論

在本教程中,我們演示瞭如何保護缺少 @PreAuthorize/ @PostAuthorize 註解的端點。

此外,我們還證明了非註解方法現在確實得到了保護。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.