Тестирование Spring Security
Безопасность играет важную роль в программном обеспечении. В конечном итоге каждому необходимо повысить безопасность своего проекта. В этой статье мы рассмотрим, как протестировать аутентификацию и авторизацию приложений Spring Boot. Мы рассмотрим как приложения-сервлеты MVC, так и реактивные приложения WebFlux.
Spring Security хорошо интегрируется с фреймворками Spring Web MVC и Spring WebFlux. Он также имеет комплексную интеграцию с Spring MVC Test и Spring WebTestClient
.
Защитите приложение Spring MVC
Давайте начнем с простого приложения, которое управляет клиентами. Мы хотим создавать, получать и удалять клиентов.
@RestController
@RequiredArgsConstructor
public class CustomerController {
private final CustomerRepository customerRepository;
@GetMapping("/customer/{id}")
Customer getCustomer(@PathVariable Long id) {
return customerRepository.findById(id).orElseThrow();
}
@PostMapping("/customer")
@ResponseStatus(HttpStatus.CREATED)
Customer createCustomer(@RequestBody Customer customer) {
return customerRepository.save(customer);
}
@DeleteMapping("/customer/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
void deleteCustomer(@PathVariable Long id) {
customerRepository.deleteById(id);
}
}
Мы, вероятно, не хотим, чтобы неавторизованные люди создавали и удаляли клиентов. Таким образом, мы собираемся использовать простую конфигурацию безопасности для добавления аутентификации.
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfiguration {
}
Теперь, чтобы обезопасить конечные точки, мы можем использовать @PreAuthorized
аннотацию для включения безопасности метода. Мы собираемся сделать это для POST
и DELETE
операций.
@PostMapping("/customer")
@ResponseStatus(HttpStatus.CREATED)
@PreAuthorize("hasRole('ADMIN')")
Customer createCustomer(@RequestBody Customer customer) {
return customerRepository.save(customer);
}
@DeleteMapping("/customer/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
@PreAuthorize("hasRole('ADMIN')")
void deleteCustomer(@PathVariable Long id) {
customerRepository.deleteById(id);
}
С точки зрения тестирования не имеет большого значения, как была настроена конфигурация безопасности. Для простоты мы приведем пример конфигурации вкратце.
Настройте @WebMvcTest С помощью Security
Чтобы протестировать наши контроллеры изолированно, мы можем использовать тест Spring Boot Test @WebMvcTest
тестовый фрагмент.
Использование @WebMvcTest
загружает компоненты, необходимые для контроллера, но не знает, какие другие компоненты конфигурации загружать. Мы должны сообщить Spring загрузить конфигурацию безопасности с помощью @Import(SecurityConfiguration.class)
аннотации.
@WebMvcTest(CustomerController.class)
@Import(SecurityConfiguration.class)
class CustomerControllerTests {
// ...
}
Если запуск приложения запрещает доступ к конечной точке, но наши тесты не делают того же самого, вероятно, мы забыли импортировать конфигурацию безопасности.
Это может быть трудно обнаружить, если у нас нет тестов, которые проверяют код состояния «запрещено» или «неавторизовано».
Запустите тест от имени пользователя
Чтобы протестировать Spring Security, давайте начнем с конечных точек, для которых не требуются права администратора.
Чтобы запустить тест от имени пользователя, мы можем использовать @WithMockUser
аннотацию для обеспечения поддельной аутентификации пользователя.
@Test
@WithMockUser
void getCustomer() throws Exception {
when(customerRepository.findById(1L))
.thenReturn(Optional.of(new Customer(1L, "John", "Doe")));
mockMvc.perform(get("/customer/{id}", 1L))
.andExpect(status().isOk());
}
Это приведет к аутентификации пользователя, и тест пройдет успешно.
Чтобы убедиться, что наша конфигурация безопасности работает, мы также можем добавить тест для проверки того, что ответ является 401 несанкционированным, если мы не прошли аутентификацию.
Мы можем использовать @WithAnonymousUser
аннотацию, которая необязательна, но подчеркивает тот факт, что это пользователь, не прошедший проверку подлинности.
@Test
@WithAnonymousUser
void cannotGetCustomerIfNotAuthorized() throws Exception {
mockMvc.perform(get("/customer/{id}", 1L))
.andExpect(status().isUnauthorized());
}
В дополнение к проверке подлинности, мы также можем проверить, что ответ 403 Запрещен, если пользователь не авторизован для доступа к ресурсу.
@Test
@WithMockUser
void cannotCreateCustomerIfNotAnAdmin() throws Exception {
mockMvc.perform(post("/customer")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"firstName\": \"John\", \"lastName\": \"Doe\"}")
.with(csrf())
)
.andExpect(status().isForbidden());
}
Мы можем дополнительно настроить пользователя и добавить роли с помощью @WithMockUser
аннотации. Например, мы могли бы сделать пользователя администратором с помощью @WithMockUser(roles = "ADMIN")
.
Включить токен CSRF
Spring Security по умолчанию включает защиту CSRF, что означает, что нам нужно добавить действительный токен CSRF к небезопасным HTTP-методам. Эти методы включают POST
, PUT
и DELETE
, или все методы, которые не доступны только для чтения.
Мы можем предоставить токен CSRF с постпроцессором запроса.
@Test
@WithAnonymousUser
void cannotGetCustomerIfNotAuthorized() throws Exception {
mockMvc.perform(get("/customer/{id}", 1L))
.andExpect(status().isUnauthorized());
}
Можно добавить недопустимый токен CSRF с помощью csrf().useInvalidToken()
, но это бесполезно, если вы не выполняете более сложную конфигурацию.
Запуск от имени пользователя Без аннотаций
@WithMockUser
Аннотации удобны, но если нам не нравятся аннотации, мы можем использовать вместо них другие постпроцессоры запросов.
@Test
void adminCanDeleteCustomer() throws Exception {
mockMvc.perform(delete("/customer/{id}", 1L)
.with(csrf())
.with(user("admin").roles("ADMIN"))
)
.andExpect(status().isNoContent());
}
Для проверки статуса несанкционированного доступа мы можем добавить anonymous()
постпроцессор. Его использование необязательно, но это еще раз подчеркивает тот факт, что это пользователь, не прошедший проверку подлинности.
@Test
void cannotDeleteCustomerIfNotAuthorized() throws Exception {
mockMvc.perform(delete("/customer/{id}", 1L)
.with(csrf())
.with(anonymous())
)
.andExpect(status().isUnauthorized());
}
Существует множество других почтовых процессоров, которые можно найти по адресу SecurityMockMvcRequestPostProcessors
. Например, мы можем протестировать базовую аутентификацию HTTP, вход в OAuth 2.0 или аутентификацию JWT.
Настройте MockMvc в @SpringBootTest с помощью Security
Если мы хотим протестировать большую часть приложения с помощью @SpringBootTest
, мы должны настроить MockMvc
для тестов.
@SpringBootTest
class CustomerMockEnvTests {
@Autowired
private WebApplicationContext context;
private MockMvc mockMvc;
@BeforeEach
public void setup() {
mockMvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity())
.build();
}
}
Важной частью здесь является добавление MockMvcConfigurer.springSecurity()
в конфигурацию.
Мы могли бы использовать @AutoconfigureMockMvc
аннотацию здесь вместо этого, но полезно знать, как инициализировать MockMvc
вручную. Проблема с @AutoconfigureMockMvc
аннотацией заключается в том, что она может нарушить кэширование тестирования контекста приложения Spring Boot.
После MockMvc
настройки его использование ничем не отличается от тестирования с помощью @WebMvcTest
. Мы также можем использовать @WithMockUser
аннотации.
Настройте MockMvc WebTestClient в @SpringBootTest с помощью Security
Что, если мы захотим написать сквозные тесты для приложения? Поскольку тест запускает приложение в другом процессе, одним из вариантов является использование WebTestClient
для отправки запросов к приложению.
Однако есть одна загвоздка: мы не можем автоматически подключать WebTestClient
компонент непосредственно в тесте. Нам приходится настраивать клиент вручную и использовать обходной путь.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class CustomerServerEnvTests {
@Autowired
private WebApplicationContext context;
private WebTestClient webClient;
@BeforeEach
void setup() {
webClient = MockMvcWebTestClient.bindToApplicationContext(context)
.apply(springSecurity())
.defaultRequest(get("/").with(csrf()))
.configureClient()
.build();
}
@Test
void createCustomer() {
webClient.mutateWith(csrf()).post().uri("/customer")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue("{\"firstName\": \"John\", \"lastName\": \"Doe\"}")
.exchange()
.expectStatus().isCreated();
}
}
Это потому, что они изначально были разработаны WebTestClient
для тестирования приложений reactive Spring и пока не поддерживают это.
Если мы попытаемся выполнить автоматическую проводку
WebTestClient
и попытаемся вызватьmutateWith(csrf())
, тесты завершатся ошибкой с загадочным сообщением.
Cannot invoke "org.springframework.web.server.adapter.WebHttpHandlerBuilder.filters(java.util.function.Consumer)" because "httpHandlerBuilder" is null
Поскольку это сквозной тест, вместо того, чтобы имитировать аутентификацию, мы могли бы вместо этого предоставить заголовки аутентификации.
@Test
void createCustomer() {
webClient.post().uri("/customer")
.headers(http -> http.setBasicAuth("username", "password"))
.contentType(MediaType.APPLICATION_JSON)
.bodyValue("{\"firstName\": \"John\", \"lastName\": \"Doe\"}")
.exchange()
.expectStatus().isCreated();
}
Мы можем даже предоставить токен на предъявителя с тем же механизмом. Однако создание токенов является громоздким процессом, и лучше сосредоточиться на тестировании авторизации, а не на представлении токенов на предъявителя.
Защитите приложение Spring WebFlux
Давайте начнем с предыдущего примера приложения и перенесем его в реактивное приложение.
@RestController
@RequiredArgsConstructor
public class CustomerController {
private final CustomerRepository customerRepository;
@GetMapping("/customer/{id}")
Mono getCustomer(@PathVariable Long id) {
return customerRepository.findById(id);
}
@PostMapping("/customer")
@ResponseStatus(HttpStatus.CREATED)
@PreAuthorize("hasRole('ADMIN')")
Mono createCustomer(@RequestBody Customer customer) {
return customerRepository.save(customer);
}
@DeleteMapping("/customer/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
@PreAuthorize("hasRole('ADMIN')")
Mono deleteCustomer(@PathVariable Long id) {
return customerRepository.deleteById(id);
}
}
Нам потребуется настроить безопасность. Аннотации несколько отличаются от конфигурации безопасности MVC.
@Configuration
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
public class SecurityConfiguration {
}
Как только конфигурация безопасности установлена, конечные точки теперь защищены.
Настройте @WebFluxTest с помощью Security
Чтобы протестировать наши реактивные контроллеры изолированно, мы можем использовать тест Spring Boot Test @WebFluxText
тестовый фрагмент. Мы также можем использовать WebTestClient
непосредственно в этих тестах, поскольку он был разработан для работы с реактивными приложениями.
@WebFluxTest(CustomerController.class)
@Import(SecurityConfiguration.class)
class CustomerControllerTests {
@MockBean
private CustomerRepository customerRepository;
@Autowired
private WebTestClient webClient;
@Test
@WithMockUser
void getCustomer() {
when(customerRepository.findById(1L))
.thenReturn(Mono.just(new Customer(1L, "John", "Doe")));
webClient.get().uri("/customer/{id}", 1)
.exchange()
.expectStatus().isOk();
}
Для любых конечных точек, которым требуется токен CSRF, нам нужно добавить его. Мы делаем это, изменяя WebTestClient
с помощью мутаторов, поступающих от SecurityMockServerConfigurers
.
@Test
@WithMockUser(roles = "ADMIN")
void adminCanCreateCustomers() {
when(customerRepository.save(any()))
.thenReturn(Mono.just(new Customer(1L, "John", "Doe")));
webClient.mutateWith(csrf())
.post().uri("/customer")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue("{\"firstName\": \"John\", \"lastName\": \"Doe\"}")
.exchange()
.expectStatus().isCreated();
}
Опять же, если мы не хотим использовать @WithMockUser
аннотацию, мы можем дополнительно настроить клиент с помощью мутаторов, которые обеспечивают аутентификацию.
@Test
void adminCanDeleteCustomer() {
when(customerRepository.deleteById(1L)).thenReturn(Mono.empty());
webClient.mutateWith(csrf())
.mutateWith(mockUser().roles("ADMIN"))
.delete().uri("/customer/{id}", 1)
.exchange()
.expectStatus().isNoContent();
}
В дополнение к предоставлению макета пользователя мы можем использовать, например, mockJwt()
или mockOAuth2Login()
.
Настройте WebFlux WebTestClient в @SpringBootTest с помощью Security
Переходим к сквозным тестам, и@SpringBootTest
, WebTestClient
компонент недоступен по умолчанию. Клиент необходимо настроить вручную.
@SpringBootTest
public class CustomerControllerEndToEndTests {
@Autowired
private ApplicationContext context;
private WebTestClient webClient;
@BeforeEach
void setup() {
webClient = WebTestClient.bindToApplicationContext(context)
.apply(springSecurity())
.configureClient()
.build();
}
}
Мы могли бы снова сделать то же самое, добавив @AutoconfigureWebTestClient
аннотацию, но она страдает от тех же проблем с контекстным кэшированием, что упоминались ранее.
Здесь не имеет значения, запускаем ли мы тест в макетной среде или в серверной среде. WebTestClient
настраивается одинаково в обоих реактивных приложениях.
После WebTestClient
настройки его использование ничем не отличается от тестирования с помощью @WebFluxTest
. Мы можем либо использовать @WithMockUser
, либо видоизменить клиент с помощью mock security от SecurityMockServerConfigurers
.
Включить журналы отладки Для устранения неполадок
Иногда, когда наши тесты безопасности завершаются неудачей, бывает сложно выяснить, что не так. Для устранения проблем с Spring Security мы можем включить ведение журнала отладки безопасности, чтобы увидеть, что происходит.
logging:
level:
org:
springframework:
security: DEBUG
Например, если мы забудем токен CSRF, это не будет очевидно, пока мы не используем ведение журнала отладки. После включения ведения журнала безопасности отладки мы можем выяснить причину сбоя.
o.s.security.web.csrf.CsrfFilter : Invalid CSRF token found for http://localhost/customer
Краткие сведения
Spring Security хорошо интегрируется с фреймворками Spring Web MVC и Spring WebFlux. Он также имеет всестороннюю интеграцию с MockMvc
и WebTestClient
.
Мы можем подделать аутентификацию, используя аннотацию или методический подход. Также возможно предоставить разные роли для тестирования авторизации.
Охватить различные случаи несложно. Желаемый уровень проверки зависит от того, насколько сложна наша конфигурация безопасности. Поскольку это довольно важная часть приложения, полезно протестировать ее полностью.