【问题标题】:Spring Integration Tests for Resource Server (based on spring-cloud-starter-oauth2)资源服务器的 Spring 集成测试(基于 spring-cloud-starter-oauth2)
【发布时间】:2021-02-08 05:47:42
【问题描述】:

我正在使用 Spring Boot 和 Spring Cloud 作为 oAuth2 资源服务器。这是配置:

pom.xml

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-acl</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>

...
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.4.RELEASE</version>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR8</version>

资源服务器配置

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    @Value("${my-app.security.audience}")
    private String audience;

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        resources.resourceId(audience);
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
                .cors().and()
                .httpBasic().disable()
                .formLogin().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                .authorizeRequests(authorize -> authorize
                    .antMatchers("/actuator/**").permitAll() // TODO: Enable basic auth for actuator
                    .anyRequest().authenticated()
                );
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration corsConfiguration = new CorsConfiguration().applyPermitDefaultValues();
        corsConfiguration.addAllowedMethod("PATCH");
        source.registerCorsConfiguration("/**", corsConfiguration);
        return source;
    }

    @Bean
    public ResourceServerProperties resourceServerProperties() {
        return new ResourceServerProperties(null, null);
    }
}

OidcJwkTokenStoreConfig

@Configuration
public class OidcJwkTokenStoreConfig {
    private final ResourceServerProperties resource;

    public OidcJwkTokenStoreConfig(ResourceServerProperties resource) {
        this.resource = resource;
    }

    @Bean
    public TokenStore jwkTokenStore(UserDetailsService userDetailsService) {
        DefaultAccessTokenConverter tokenConverter = new DefaultAccessTokenConverter();
        tokenConverter.setUserTokenConverter(new MvcUserAuthenticationConverter(userDetailsService));
        return new JwkTokenStore(this.resource.getJwk().getKeySetUri(), tokenConverter);
    }
}

自定义用户认证转换器

public class MvcUserAuthenticationConverter implements UserAuthenticationConverter {
    private final String SUB = "sub";
    private final UserDetailsService userDetailsService;

    public MvcUserAuthenticationConverter(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    @Override
    public Map<String, ?> convertUserAuthentication(Authentication userAuthentication) {
        throw new UnsupportedOperationException();
    }

    @Override
    public Authentication extractAuthentication(Map<String, ?> map) {
        if (map.containsKey(SUB)) {
            Object principal = map.get(SUB);
            Collection<? extends GrantedAuthority> authorities = getAuthorities(map);
            if (userDetailsService != null) {
                UserDetails user = userDetailsService.loadUserByUsername((String) map.get(SUB));
                authorities = user.getAuthorities();
                principal = user;
            }
            return new UsernamePasswordAuthenticationToken(principal, "N/A", authorities);
        }
        return null;
    }

    private Collection<? extends GrantedAuthority> getAuthorities(Map<String, ?> map) {
        if (!map.containsKey(AUTHORITIES)) {
            return null;
        }
        Object authorities = map.get(AUTHORITIES);
        if (authorities instanceof String) {
            return AuthorityUtils.commaSeparatedStringToAuthorityList((String) authorities);
        }
        if (authorities instanceof Collection) {
            return AuthorityUtils.commaSeparatedStringToAuthorityList(StringUtils
                    .collectionToCommaDelimitedString((Collection<?>) authorities));
        }
        throw new IllegalArgumentException("Authorities must be either a String or a Collection");
    }
}

如何进行集成测试?

单元测试不是一个大问题。但是当涉及到集成测试时,我很挣扎。我如何模拟/跳过提供真实的不记名令牌?首选解决方案是使用 MockMvc 进行集成测试。

到目前为止,我得到了以下信息:

@SpringBootTest
@Testcontainers
@ContextConfiguration(
        initializers = ProjectResourceTest.Initializer.class,
        classes = {ProjectResourceTest.ApiTestConfiguration.class, MyApplication.class}
)
@AutoConfigureMockMvc
public class ProjectResourceTest {
    @Container
    private static final MongoDBContainer mongoDB = new MongoDBContainer("mongo:4.2.5");

    public static class Initializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {

        @Override
        public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
            TestPropertyValues values = TestPropertyValues.of(
                    "spring.data.mongodb.uri=" + mongoDB.getReplicaSetUrl()
            );
            values.applyTo(configurableApplicationContext);
        }
    }

    @TestConfiguration
    public static class ApiTestConfiguration {
        @Bean
        @Primary
        public UserDetailsService userDetailsService() {
            MvcUser defaultUser = new MvcUser("default-user", "Default User");

            return new InMemoryUserDetailsManager(singletonList(defaultUser));
        }
    }

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    MongoTemplate mongo;

    @Test
    @WithUserDetails("default-user")
    void shouldReturnAllProjects() throws Exception {
        ProjectEntity project = ProjectFaker.newProjectEntity();
        mongo.save(project);

        mockMvc.perform(get("/api/v1/projects"))
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.key", is(project.getKey())))
                .andExpect(jsonPath("$.name", is(project.getName())))
                .andExpect(jsonPath("$.createdAt", is(project.getCreatedAt())));
    }
}

但是这种方法以以下异常结束

java.lang.IllegalStateException: Unable to create SecurityContext using @org.springframework.security.test.context.support.WithUserDetails(setupBefore=TEST_METHOD, userDetailsServiceBeanName=, value=default-user)

    at org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener.createTestSecurityContext(WithSecurityContextTestExecutionListener.java:126)
    at org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener.createTestSecurityContext(WithSecurityContextTestExecutionListener.java:96)
    at org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener.beforeTestMethod(WithSecurityContextTestExecutionListener.java:62)
    at org.springframework.test.context.TestContextManager.beforeTestMethod(TestContextManager.java:289)
    at org.springframework.test.context.junit.jupiter.SpringExtension.beforeEach(SpringExtension.java:108)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeBeforeEachCallbacks$1(TestMethodTestDescriptor.java:161)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeBeforeMethodsOrCallbacksUntilExceptionOccurs$5(TestMethodTestDescriptor.java:197)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeBeforeMethodsOrCallbacksUntilExceptionOccurs(TestMethodTestDescriptor.java:197)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeBeforeEachCallbacks(TestMethodTestDescriptor.java:160)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:131)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:71)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:135)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:125)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:135)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:123)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:122)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:80)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1540)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:139)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:125)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:135)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:123)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:122)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:80)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1540)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:139)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:125)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:135)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:123)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:122)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:80)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1540)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:139)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:125)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:135)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:123)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:122)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:80)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:32)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:51)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:248)
    at org.junit.platform.launcher.core.DefaultLauncher.lambda$execute$5(DefaultLauncher.java:211)
    at org.junit.platform.launcher.core.DefaultLauncher.withInterceptedStreams(DefaultLauncher.java:226)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:199)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:132)
    at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:71)
    at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
    at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:220)
    at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:53)
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'org.springframework.security.core.userdetails.UserDetailsService' available
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:351)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:342)
    at org.springframework.security.test.context.support.WithUserDetailsSecurityContextFactory.findUserDetailsService(WithUserDetailsSecurityContextFactory.java:78)
    at org.springframework.security.test.context.support.WithUserDetailsSecurityContextFactory.createSecurityContext(WithUserDetailsSecurityContextFactory.java:58)
    at org.springframework.security.test.context.support.WithUserDetailsSecurityContextFactory.createSecurityContext(WithUserDetailsSecurityContextFactory.java:44)
    at org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener.createTestSecurityContext(WithSecurityContextTestExecutionListener.java:123)
    ... 61 more

我的自定义 UserDetailsS​​ervice

@Service
public class MvcUserDetailsService implements UserDetailsService {
    private final UserRepository userRepository;

    @Autowired
    public MvcUserDetailsService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Optional<UserEntity> user = userRepository.findByUserId(username);

        if (user.isPresent()) {
            return new MvcUser(user.get());
        } else {
            return createNewUser();
        }
    }
    ...
}

我在这里错过了什么?

【问题讨论】:

    标签: spring-boot spring-security oauth-2.0 spring-oauth2 spring-test-mvc


    【解决方案1】:

    可以执行以下操作:

    1. 为您的测试覆盖您的资源服务器配置,并确保在您的类路径中放置一个有效的 RSA 公钥(例如/src/test/resources):

    application.yml 内部 /src/test/resources 的示例:

    spring:
      security:
        oauth2:
          resourceserver:
            jwt:
              public-key-location: classpath:id_rsa.pub
    

    这应该满足您的应用程序启动并且不需要与授权服务器进行任何 HTTP 通信。

    1. 接下来,结合使用@SpringBootTest@AutoConfigureMockMvc 来针对模拟的Servlet 环境进行测试。确保您有可用的 spring-security-test 依赖项。

    2. 在您的测试中摆脱您自定义的UserDetailsServicebean,而是依赖正常的自动配置。

    3. 现在您可以使用例如@WithMockUser 在测试时在 Spring SecurityContext 中提供一个模拟用户。

    要在集成测试期间提供真实的不记名令牌,您可以执行以下操作:

    1. 覆盖您的配置以指向假授权服务器(例如,使用 WireMock 启动本地 HTTP 服务器)
    2. 模拟应用程序(充当资源服务器)的初始 HTTP 通信与应用程序请求 JWKS 的授权服务。在那里,您可以创建一个内存中的 RSA 密钥对并让模拟身份提供者返回公钥
    3. 创建一个有效的 JWT 并使用私钥对其进行签名
    4. 使用例如访问您启动的应用程序WebTestClientTestRestTemplate 并将令牌添加为请求标头的一部分(为此您需要 @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)

    【讨论】:

    • mmh 在我的情况下不起作用。我正在提供JwkTokenStore 的自定义实例(见上文),以便在我的资源服务器中使用自定义UserDetailsService。您提供的配置用于 spring-boot-security。在我的情况下,我使用 spring-cloud-starter-oauth2 (因此使用了另一个配置)。好像我搞砸了什么……?
    • 您可以添加您的自定义UserDetailsService 吗? Spring OAuth2 仍然使用 Spring Security 的大量基础设施,因此您也可以使用例如@WithMockUser 在安全上下文中硬编码经过身份验证的用户。
    • 添加我的自定义 UserDetailsS​​ervice。服务实现总是返回一个用户(来自数据库)。如果用户不存在,将创建一个新用户。
    • @WithMockUser 帮助我对经过身份验证的用户进行硬编码。这种方法帮助我进行测试。谢谢! :)
    猜你喜欢
    • 2019-11-27
    • 2017-08-28
    • 2019-04-29
    • 2015-06-24
    • 2015-06-13
    • 2013-12-21
    • 2018-08-17
    • 2018-04-14
    • 2020-08-14
    相关资源
    最近更新 更多