Как написать модульный тест для SecurityConfig для весенней безопасности

У меня есть класс для весенней безопасности, проверяющий токен от пользователя. Я получил код с веб-сайта Auth0 и изменил часть antMatcher для своей конфигурации. Вот код:

@EnableWebSecurity
public class SecurityConfig {

    @Value("${auth0.audience}")
    private String audience;

    @Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
    private String issuer;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        /*
        This is where we configure the security required for our endpoints and setup our app to serve as
        an OAuth2 Resource Server, using JWT validation.
        */
        http
            .csrf().disable()
            .authorizeRequests()
            .antMatchers(HttpMethod.GET, "/data/actuator/**").permitAll()
            .antMatchers(HttpMethod.PUT, "/data/**").hasAuthority("SCOPE_data:write")
            .anyRequest().authenticated()
            .and().cors()
            .and().oauth2ResourceServer().jwt();
        return http.build();
    }

    @Bean
    JwtDecoder jwtDecoder() {
        /*
        By default, Spring Security does not validate the "aud" claim of the token, to ensure that this token is
        indeed intended for our app. Adding our own validator is easy to do:
        */
        NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder)
                JwtDecoders.fromOidcIssuerLocation(issuer);
        OAuth2TokenValidator<Jwt> audienceValidator =
                new com.nuance.pindata.health.importer.security.AudienceValidator(audience);
        OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuer);
        OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator);
        jwtDecoder.setJwtValidator(withAudience);
        return jwtDecoder;
    }
}

Сейчас я пытаюсь написать модульный тест, но нет хорошего способа его протестировать. Я могу практически протестировать изменение метода/пути, но не так просто написать этот модульный тест, и это можно сделать с помощью интеграционных (автоматизированных) тестов.

Из Тестирование конфигурации Spring Security HttpSecurity он также предлагает не писать модульный тест для такой конфигурации безопасности. Какой здесь правильный подход? Если я должен написать модульный тест, как я могу этого добиться?

Вы хотите протестировать JwtDecoder?

lane.maxwell 03.02.2023 21:12
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
1
1
100
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Вы можете протестировать управление доступом к конечным точкам привода только в интеграционных тестах (@SpringBootTest). Для вашего собственного защищенного @Components вы можете сделать это также в юнит-тестах (много примеров в этом репо):

  • @Controller с @WebMvcTest (@WebfluxTest, если вы были в реактивном приложении)
  • простой JUnit с @ExtendWith(SpringExtension.class), @EnableMethodSecurity и @Import тестируемого компонента (@Service или @Repository с безопасностью метода, такой как @PreAuthorize выражения), чтобы получить автоматически подключенный экземпляр, оснащенный средствами безопасности

spring-security-test поставляется с некоторыми постпроцессорами запросов MockMvc (см. org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt в вашем случае), а также мутаторами WebTestClient (см. org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockJwt) для настройки аутентификации правильного типа (JwtAuthenticationToken в вашем случае) и установки ее в тестовом контексте безопасности, но это ограничено для MockMvc и WebTestClient, а также для @Controller тестов.

Пример использования в интеграционном тесте (@SpringBootTest) для запуска привода (но вы поняли идею модульных тестов):

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-test</artifactId>
    <scope>test</scope>
</dependency>
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt;

@SpringBootTest(webEnvironment = WebEnvironment.MOCK)
@AutoConfigureMockMvc
class ApplicationIntegrationTest {

    @Autowired
    MockMvc api;

    @Test
    void givenUserIsAnonymous_whenGetLiveness_thenOk() throws Exception {
        api.perform(get("/data/actuator/health/liveness"))
            .andExpect(status().isOk());
    }

    @Test
    void givenUserIsAnonymous_whenGetMachin_thenUnauthorized() throws Exception {
        api.perform(get("/data/machin"))
            .andExpect(status().isUnauthorized());
    }

    @Test
    void givenUserIsGrantedWithDataWrite_whenGetMachin_thenOk() throws Exception {
        api.perform(get("/data/machin")
                .with(jwt().jwt(jwt -> jwt.authorities(List.of(new SimpleGrantedAuthority("SCOPE_data:write"))))))
            .andExpect(status().isOk());
    }

    @Test
    void givenUserIsAuthenticatedButNotGrantedWithDataWrite_whenGetMachin_thenForbidden() throws Exception {
        api.perform(get("/data/machin")
                .with(jwt().jwt(jwt -> jwt.authorities(List.of(new SimpleGrantedAuthority("SCOPE_openid"))))))
            .andExpect(status().isForbidden());
    }
}

Вы также можете использовать @WithMockJwtAuth из этих библиотек, которые я поддерживаю. Этот репозиторий содержит довольно много примеров для модульного и интеграционного тестирования любого вида @Component (@Controllers, конечно, но также @Services или @Repositories, украшенных безопасностью методов).

Образец выше становится:

<dependency>
    <groupId>com.c4-soft.springaddons</groupId>
    <artifactId>spring-addons-oauth2-test</artifactId>
    <version>6.0.12</version>
    <scope>test</scope>
</dependency>
@SpringBootTest(webEnvironment = WebEnvironment.MOCK)
@AutoConfigureMockMvc
class ApplicationIntegrationTest {

    @Autowired
    MockMvc api;

    @Test
    void givenUserIsAnonymous_whenGetLiveness_thenOk() throws Exception {
        api.perform(get("/data/actuator/health/liveness"))
            .andExpect(status().isOk());
    }

    @Test
    void givenUserIsAnonymous_whenGetMachin_thenUnauthorized() throws Exception {
        api.perform(get("/data/machin"))
            .andExpect(status().isUnauthorized());
    }

    @Test
    @WithMockJwtAuth("SCOPE_data:write")
    void givenUserIsGrantedWithDataWrite_whenGetMachin_thenOk() throws Exception {
        api.perform(get("/data/machin"))
            .andExpect(status().isOk());
    }

    @Test
    @WithMockJwtAuth("SCOPE_openid")
    void givenUserIsAuthenticatedButNotGrantedWithDataWrite_whenGetMachin_thenForbidden() throws Exception {
        api.perform(get("/data/machin"))
            .andExpect(status().isForbidden());
    }
}

Spring-аддоны стартер

В том же репозитории, что и тестовые аннотации, вы найдете стартеры для упрощения конфигурации безопасности вашего сервера ресурсов (а также для улучшения конфигурации CORS и синхронизации сеансов и отключения защиты CSRF, поскольку второй не следует отключать при активных сеансах...).

Использование очень простое, и все, что вам нужно изменить, чтобы переключиться на другой сервер авторизации OIDC, это свойства. Это может произойти, например, из-за того, что вас вынуждает занятость (если они решат, что Auth0 слишком дорог или ему больше нельзя доверять) или, может быть, потому, что вы считаете более удобным использовать автономный Keycloak на вашей машине разработки (это доступны в автономном режиме, что я часто).

Вместо того, чтобы напрямую импортировать spring-boot-starter-oauth2-resource-server, импортируйте вокруг него тонкую оболочку (, состоящую только из 3 файлов ):

<dependency>
    <groupId>com.c4-soft.springaddons</groupId>
    <artifactId>spring-addons-webmvc-jwt-resource-server</artifactId>
    <version>6.0.12</version>
</dependency>

По умолчанию пользователи должны пройти аутентификацию для доступа к любому маршруту, кроме тех, которые перечислены в свойстве com.c4-soft.springaddons.security.permit-all (см. ниже). Замените всю вашу конфигурацию Java на:

@Configuration
@EnableMethodSecurity
public class SecurityConfig {
    // If not using method-security or to configure actuator RBAC
    // You might define a bean of type ExpressionInterceptUrlRegistryPostProcessor
    // and Fine tune AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry
}

Вы можете удалить все свойства spring.security.oauth2.resourceserver, они игнорируются (за исключением spring.security.oauth2.resourceserver.jwt.audiences, если вы проверяете аудиторию). Вместо этого можно использовать следующие свойства:

# Define this instead of auth0.audience
spring.security.oauth2.resourceserver.jwt.audiences=http://localhost:8080,https://localhost:8080

# Single OIDC JWT issuer but you can add as many as you like
com.c4-soft.springaddons.security.issuers[0].location=https://dev-ch4mpy.eu.auth0.com/

# Mimic spring-security default converter: map authorities with "SCOPE_" prefix
# Difference with your current conf is authorities source is not only "scope" claim but also "roles" and "permissions" ones 
# I would consider map authorities without "SCOPE_" prefix (the default behaviour of my starters) and update access control expressions accordingly
com.c4-soft.springaddons.security.issuers[0].authorities.claims=scope,roles,permissions
com.c4-soft.springaddons.security.issuers[0].authorities.prefix=SCOPE_

# Fine-grained CORS configuration can be set per path as follow:
com.c4-soft.springaddons.security.cors[0].path=/data/api/**
com.c4-soft.springaddons.security.cors[0].allowed-origins=https://localhost,https://localhost:8100,https://localhost:4200
com.c4-soft.springaddons.security.cors[0].allowedOrigins=*
com.c4-soft.springaddons.security.cors[0].allowedMethods=*
com.c4-soft.springaddons.security.cors[0].allowedHeaders=*
com.c4-soft.springaddons.security.cors[0].exposedHeaders=*

# Comma separated list of ant path matchers for resources accessible to anonymous
com.c4-soft.springaddons.security.permit-all=/data/actuator/**

Пипец, не так ли?

Другие вопросы по теме