«1. Введение

В нашем руководстве по безопасности методов Spring мы увидели, как мы можем использовать аннотации @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
@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()
        );
    }
}

Наконец, у нас есть контроллер rest с двумя методами. Однако мы «забыли» защитить конечную точку /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!";
    }
}

При запуске примера мы можем войти в систему с помощью пользователя/пароля. Затем мы получаем доступ к конечной точке /hello. Мы также можем войти в систему как гость/гость. В этом случае мы не сможем получить доступ к конечной точке /hello.

Однако любой аутентифицированный пользователь может получить доступ к конечной точке /bye. В следующем разделе мы напишем тест, чтобы доказать это.

4. Тестирование решения

Используя MockMvc, мы можем настроить тест. Мы проверяем, что наш неаннотированный метод по-прежнему доступен:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = DenyApplication.class)
public class DenyOnMissingControllerIntegrationTest {
    @Rule
    public ExpectedException expectedException = ExpectedException.none();

    @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")
    // This will fail without the changes from the next section
    public void givenANormalUser_whenCallingBye_thenAccessDenied() throws Exception {
        expectedException.expectCause(isA(AccessDeniedException.class));

        mockMvc.perform(get("/bye"));
    }
}

Второй тест завершается неудачно, потому что конечная точка /bye доступна. В следующем разделе мы обновим нашу конфигурацию, чтобы запретить доступ к неаннотированным конечным точкам.

5. Решение: Запретить по умолчанию

Давайте расширим наш класс MethodSecurityConfig и настроим MethodSecurityMetadataSource:

@Configuration 
@EnableWebSecurity 
@EnableGlobalMethodSecurity(prePostEnabled = true) 
public class DenyMethodSecurityConfig extends GlobalMethodSecurityConfiguration {
    @Override
    protected MethodSecurityMetadataSource customMethodSecurityMetadataSource() {
        return new CustomPermissionAllowedMethodSecurityMetadataSource();
    }
    // setting up in memory users not repeated
    ...
}

Теперь давайте реализуем интерфейс MethodSecurityMetadataSource:

public class CustomPermissionAllowedMethodSecurityMetadataSource 
  extends AbstractFallbackMethodSecurityMetadataSource {
    @Override
    protected Collection findAttributes(Class<?> clazz) { return null; }

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

        // if the class is annotated as @Controller we should by default deny access to all methods
        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 getAllConfigAttributes() { return null; }
}

Мы добавим DENY_ALL_ATTRIBUTE в все методы классов @Controller.

Но мы не добавляем их, если найдена аннотация @PreAuthorize/@PostAuthorize. Мы делаем это, возвращая null, указывая на то, что метаданные не применяются.

Благодаря обновленному коду наша конечная точка /bye защищена, и тесты проходят успешно.

6. Заключение

В этом коротком руководстве мы показали, как защитить конечные точки, на которых отсутствуют аннотации @PreAuthorize / @PostAuthorize.

Также мы показываем, что неаннотированные методы теперь действительно защищены.

Как всегда, полный исходный код статьи доступен на GitHub.