«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.