Spring 资源服务器集成测试(基于 spring-cloud-starter-oauth2)
Spring Integration Tests for Resource Server (based on spring-cloud-starter-oauth2)
我正在使用 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>
ResourceServerConfig
@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");
}
}
如何进行集成测试?
单元测试不是大问题。但是当涉及到集成测试时,我很挣扎。我怎样才能 mock/skip 提供真正的不记名令牌?首选解决方案是使用 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(TestMethodTestDescriptor.java:161)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeBeforeMethodsOrCallbacksUntilExceptionOccurs(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(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(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(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(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(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(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(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(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(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(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(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(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(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
我的自定义 UserDetailsService
@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();
}
}
...
}
我在这里错过了什么?
您可以执行以下操作:
- 为您的测试覆盖您的资源服务器配置,并确保您在类路径中放置了有效的 RSA public 密钥(例如
/src/test/resources
):
示例 application.yml
inside /src/test/resources
:
spring:
security:
oauth2:
resourceserver:
jwt:
public-key-location: classpath:id_rsa.pub
这应该可以满足您的应用程序启动,并且不需要与授权服务器进行任何 HTTP 通信。
接下来,使用 @SpringBootTest
和 @AutoConfigureMockMvc
的组合针对模拟的 Servlet 环境进行测试。确保您有可用的 spring-security-test
依赖项。
摆脱测试中的自定义 UserDetailsService
bean,而是依赖正常的 auto-configuration.
现在您可以使用例如@WithMockUser
在测试时在 Spring SecurityContext 中提供模拟用户。
要在集成测试期间提供真实的不记名令牌,您可以执行以下操作:
- 重写您的配置以指向伪造的授权服务器(例如,使用 WireMock 启动本地 HTTP 服务器)
- 在您的应用程序请求 JWKS 的地方模拟您的应用程序(充当资源服务器)与授权服务的初始 HTTP 通信。在那里你可以创建一个 in-memory RSA 密钥对并让模拟身份提供者 return public 密钥
- 创建有效的 JWT 并使用私钥对其签名
- 使用例如访问您启动的应用程序
WebTestClient
或 TestRestTemplate
并将令牌添加为请求的一部分 header(为此你需要 @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
)
我正在使用 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>
ResourceServerConfig
@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");
}
}
如何进行集成测试?
单元测试不是大问题。但是当涉及到集成测试时,我很挣扎。我怎样才能 mock/skip 提供真正的不记名令牌?首选解决方案是使用 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(TestMethodTestDescriptor.java:161)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeBeforeMethodsOrCallbacksUntilExceptionOccurs(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(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(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(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(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(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(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(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(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(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(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(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(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(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
我的自定义 UserDetailsService
@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();
}
}
...
}
我在这里错过了什么?
您可以执行以下操作:
- 为您的测试覆盖您的资源服务器配置,并确保您在类路径中放置了有效的 RSA public 密钥(例如
/src/test/resources
):
示例 application.yml
inside /src/test/resources
:
spring:
security:
oauth2:
resourceserver:
jwt:
public-key-location: classpath:id_rsa.pub
这应该可以满足您的应用程序启动,并且不需要与授权服务器进行任何 HTTP 通信。
接下来,使用
@SpringBootTest
和@AutoConfigureMockMvc
的组合针对模拟的 Servlet 环境进行测试。确保您有可用的spring-security-test
依赖项。摆脱测试中的自定义
UserDetailsService
bean,而是依赖正常的 auto-configuration.现在您可以使用例如
@WithMockUser
在测试时在 Spring SecurityContext 中提供模拟用户。
要在集成测试期间提供真实的不记名令牌,您可以执行以下操作:
- 重写您的配置以指向伪造的授权服务器(例如,使用 WireMock 启动本地 HTTP 服务器)
- 在您的应用程序请求 JWKS 的地方模拟您的应用程序(充当资源服务器)与授权服务的初始 HTTP 通信。在那里你可以创建一个 in-memory RSA 密钥对并让模拟身份提供者 return public 密钥
- 创建有效的 JWT 并使用私钥对其签名
- 使用例如访问您启动的应用程序
WebTestClient
或TestRestTemplate
并将令牌添加为请求的一部分 header(为此你需要@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
)