拒绝访问缺少@PreAuthorize的Spring控制器方法

2023/05/17

1. 概述

在我们的Spring方法安全教程中,我们介绍了如何使用@PreAuthorize和@PostAuthorize注解。

在本教程中,我们将了解如何拒绝对缺少授权注解的方法的访问

2. 默认安全

不管怎么样,我们可能会忘记保护我们的某个端点。不幸的是,没有简单的方法可以拒绝对非注解端点的访问。

幸运的是,默认情况下,Spring Security需要对所有端点进行身份验证。但是,它不需要特定的角色。此外,当我们没有添加安全注解时,它不会拒绝访问

3. 项目构建

首先,让我们看一下该示例的应用程序。我们使用一个简单的Spring Boot应用程序:

@SpringBootApplication
public class DenyAccessApplication {

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

其次,我们有一个安全配置。我们设置两个用户并启用pre/post注解:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class DenyMethodSecurityConfig extends GlobalMethodSecurityConfiguration {

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

最后,我们有一个带有两个方法的RestController。但是,我们假装忘记了保护/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/password登录。在这种情况下,我们无法访问/hello端点,因为guest不具有”USER”角色。

但是,任何经过身份验证的用户都可以访问/bye。在下一节中,我们将编写一个测试来证明这一点。

4. 测试解决方案

使用MockMvc我们可以编写一个测试检查不带安全注解的方法是否仍然可以访问:

@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = DenyApplication.class)
class DenyOnMissingControllerIntegrationTest {
    @Autowired
    private WebApplicationContext context;
    private MockMvc mockMvc;

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

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

    @Test
    @WithMockUser(username = "user")
    void givenANormalUser_whenCallingBye_thenAccessDenied() throws Exception {
        Assertions.assertThatThrownBy(() -> mockMvc.perform(get("/bye")))
              .hasCauseExactlyInstanceOf(AccessDeniedException.class);
    }
}

第二个测试失败,因为/bye端点是可访问的。在下一节中,我们将更新配置以拒绝访问未带注解的方法API

5. 解决方案:默认拒绝

让我们扩展MethodSecurityConfig类并设置MethodSecurityMetadataSource:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class DenyMethodSecurityConfig extends GlobalMethodSecurityConfiguration {

    @Override
    protected MethodSecurityMetadataSource customMethodSecurityMetadataSource() {
        return new CustomPermissionAllowedMethodSecurityMetadataSource();
    }
}

现在让我们实现MethodSecurityMetadataSource接口:

public class CustomPermissionAllowedMethodSecurityMetadataSource extends AbstractFallbackMethodSecurityMetadataSource {

    @Override
    protected Collection<ConfigAttribute> findAttributes(Class<?> clazz) {
        return null;
    }

    @Override
    protected Collection<ConfigAttribute> findAttributes(Method method, Class<?> targetClass) {
        Annotation[] annotations = AnnotationUtils.getAnnotations(method);
        List<ConfigAttribute> attributes = new ArrayList<>();

        // if the class is annotated as @Controller we should by default deny access to every method
        if (AnnotationUtils.findAnnotation(targetClass, Controller.class) != null) {
            attributes.add(DENY_ALL_ATTRIBUTE);
        }

        if (annotations != null) {
            for (Annotation a : annotations) {
                // but not if the method has at least a PreAuthorize or PostAuthorize annotation
                if (a instanceof PreAuthorize || a instanceof PostAuthorize) {
                    return null;
                }
            }
        }
        return attributes;
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }
}

我们将DENY_ALL_ATTRIBUTE添加到@Controller类的所有方法中

但是,如果方法上存在@PreAuthorize/@PostAuthorize注解,我们不会添加它们。我们通过返回null,这表示不应用元数据

使用更新的代码,我们的/bye端点受到保护并且测试应该成功。

6. 总结

在这个简短的教程中,我们展示了如何保护缺少@PreAuthorize/@PostAuthorize注解的端点

与往常一样,本教程的完整源代码可在GitHub上获得。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章