随着Spring Security 4的发布及其对测试的增强支持,我想更新我当前的Spring Security oauth2资源服务器测试。
目前,我有一个帮助程序类,用于建立与测试连接到实际应用程序的OAuth2RestTemplate使用ResourceOwnerPasswordResourceDetails,以为我的测试请求有效令牌。然后,此resttemplate用于在s中发出请求。ClientId``AccessTokenUri``@WebIntegrationTest
OAuth2RestTemplate
ResourceOwnerPasswordResourceDetails
ClientId``AccessTokenUri``@WebIntegrationTest
我想利用Spring Security 4中的新测试支持,放弃对实际AuthorizationServer的依赖,并在测试中使用有效的(如果有限制的)用户凭据。
到现在为止我都在尝试使用@WithMockUser,@WithSecurityContext,SecurityMockMvcConfigurers.springSecurity()和SecurityMockMvcRequestPostProcessors.*都没能进入通过身份验证的电话MockMvc,我找不到在Spring示例项目任何此类工作的例子。
@WithMockUser
@WithSecurityContext
SecurityMockMvcConfigurers.springSecurity()
SecurityMockMvcRequestPostProcessors.*
MockMvc
有人可以帮助我使用某种模拟的凭据来测试我的oauth2资源服务器,同时仍然测试所施加的安全性限制吗?
编辑 此处提供示例代码:https : //github.com/timtebeek/resource-server- testing 对于每个测试类,我都知道为什么它不能如此工作,但是我正在寻找方法可以让我轻松测试安全设置。
我现在正在考虑在下创建一个非常宽松的OAuthServer src/test/java,这可能会有所帮助。还有其他建议吗?
src/test/java
为了有效地测试资源服务器的安全性,通过MockMvc和RestTemplate都可以帮助配置AuthorizationServerunder src/test/java:
RestTemplate
AuthorizationServer
授权服务器
@Configuration @EnableAuthorizationServer @SuppressWarnings("static-method") class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { @Bean public JwtAccessTokenConverter accessTokenConverter() throws Exception { JwtAccessTokenConverter jwt = new JwtAccessTokenConverter(); jwt.setSigningKey(SecurityConfig.key("rsa")); jwt.setVerifierKey(SecurityConfig.key("rsa.pub")); jwt.afterPropertiesSet(); return jwt; } @Autowired private AuthenticationManager authenticationManager; @Override public void configure(final AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints .authenticationManager(authenticationManager) .accessTokenConverter(accessTokenConverter()); } @Override public void configure(final ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() .withClient("myclientwith") .authorizedGrantTypes("password") .authorities("myauthorities") .resourceIds("myresource") .scopes("myscope") .and() .withClient("myclientwithout") .authorizedGrantTypes("password") .authorities("myauthorities") .resourceIds("myresource") .scopes(UUID.randomUUID().toString()); } }
集成测试 对于集成测试,您可以简单地使用内置的OAuth2测试支持规则和注释:
@RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = MyApp.class) @WebIntegrationTest(randomPort = true) @OAuth2ContextConfiguration(MyDetails.class) public class MyControllerIT implements RestTemplateHolder { @Value("http://localhost:${local.server.port}") @Getter String host; @Getter @Setter RestOperations restTemplate = new TestRestTemplate(); @Rule public OAuth2ContextSetup context = OAuth2ContextSetup.standard(this); @Test public void testHelloOAuth2WithRole() { ResponseEntity<String> entity = getRestTemplate().getForEntity(host + "/hello", String.class); assertTrue(entity.getStatusCode().is2xxSuccessful()); } } class MyDetails extends ResourceOwnerPasswordResourceDetails { public MyDetails(final Object obj) { MyControllerIT it = (MyControllerIT) obj; setAccessTokenUri(it.getHost() + "/oauth/token"); setClientId("myclientwith"); setUsername("user"); setPassword("password"); } }
MockMvc测试 也可以 进行 测试MockMvc,但需要一个小的帮助程序类来获取一个在请求上RequestPostProcessor设置Authorization: Bearer <token>标头的类:
RequestPostProcessor
Authorization: Bearer <token>
@Component public class OAuthHelper { // For use with MockMvc public RequestPostProcessor bearerToken(final String clientid) { return mockRequest -> { OAuth2AccessToken token = createAccessToken(clientid); mockRequest.addHeader("Authorization", "Bearer " + token.getValue()); return mockRequest; }; } @Autowired ClientDetailsService clientDetailsService; @Autowired AuthorizationServerTokenServices tokenservice; OAuth2AccessToken createAccessToken(final String clientId) { // Look up authorities, resourceIds and scopes based on clientId ClientDetails client = clientDetailsService.loadClientByClientId(clientId); Collection<GrantedAuthority> authorities = client.getAuthorities(); Set<String> resourceIds = client.getResourceIds(); Set<String> scopes = client.getScope(); // Default values for other parameters Map<String, String> requestParameters = Collections.emptyMap(); boolean approved = true; String redirectUrl = null; Set<String> responseTypes = Collections.emptySet(); Map<String, Serializable> extensionProperties = Collections.emptyMap(); // Create request OAuth2Request oAuth2Request = new OAuth2Request(requestParameters, clientId, authorities, approved, scopes, resourceIds, redirectUrl, responseTypes, extensionProperties); // Create OAuth2AccessToken User userPrincipal = new User("user", "", true, true, true, true, authorities); UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userPrincipal, null, authorities); OAuth2Authentication auth = new OAuth2Authentication(oAuth2Request, authenticationToken); return tokenservice.createAccessToken(auth); } }
MockMvc然后,您的测试必须RequestPostProcessor从OauthHelper类中获取一个,并在发出请求时将其传递:
OauthHelper
@RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = MyApp.class) @WebAppConfiguration public class MyControllerTest { @Autowired private WebApplicationContext webapp; private MockMvc mvc; @Before public void before() { mvc = MockMvcBuilders.webAppContextSetup(webapp) .apply(springSecurity()) .alwaysDo(print()) .build(); } @Autowired private OAuthHelper helper; @Test public void testHelloWithRole() throws Exception { RequestPostProcessor bearerToken = helper.bearerToken("myclientwith"); mvc.perform(get("/hello").with(bearerToken)).andExpect(status().isOk()); } @Test public void testHelloWithoutRole() throws Exception { RequestPostProcessor bearerToken = helper.bearerToken("myclientwithout"); mvc.perform(get("/hello").with(bearerToken)).andExpect(status().isForbidden()); } }
完整的示例项目可在GitHub上找到:https : //github.com/timtebeek/resource-server- testing
Spring Boot 1.5引入了类似的测试片@WebMvcTest。使用这些测试切片并手动加载OAuth2AutoConfiguration将为您的测试提供更少的样板,并且它们将比@SpringBootTest基于建议的解决方案运行得更快。如果还导入生产安全性配置,则可以测试配置的筛选器链是否适用于Web服务。
@WebMvcTest
OAuth2AutoConfiguration
@SpringBootTest
这是设置以及一些可能会有所帮助的其他类:
控制器:
@RestController @RequestMapping(BookingController.API_URL) public class BookingController { public static final String API_URL = "/v1/booking"; @Autowired private BookingRepository bookingRepository; @PreAuthorize("#oauth2.hasScope('myapi:write')") @PatchMapping(consumes = APPLICATION_JSON_UTF8_VALUE, produces = APPLICATION_JSON_UTF8_VALUE) public Booking patchBooking(OAuth2Authentication authentication, @RequestBody @Valid Booking booking) { String subjectId = MyOAuth2Helper.subjectId(authentication); booking.setSubjectId(subjectId); return bookingRepository.save(booking); } }
测试:
@RunWith(SpringRunner.class) @AutoConfigureJsonTesters @WebMvcTest @Import(DefaultTestConfiguration.class) public class BookingControllerTest { @Autowired private MockMvc mvc; @Autowired private JacksonTester<Booking> json; @MockBean private BookingRepository bookingRepository; @MockBean public ResourceServerTokenServices resourceServerTokenServices; @Before public void setUp() throws Exception { // Stub the remote call that loads the authentication object when(resourceServerTokenServices.loadAuthentication(anyString())).thenAnswer(invocation -> SecurityContextHolder.getContext().getAuthentication()); } @Test @WithOAuthSubject(scopes = {"myapi:read", "myapi:write"}) public void mustHaveValidBookingForPatch() throws Exception { mvc.perform(patch(API_URL) .header(AUTHORIZATION, "Bearer foo") .content(json.write(new Booking("myguid", "aes")).getJson()) .contentType(MediaType.APPLICATION_JSON_UTF8) ).andExpect(status().is2xxSuccessful()); } } DefaultTestConfiguration: @TestConfiguration @Import({MySecurityConfig.class, OAuth2AutoConfiguration.class}) public class DefaultTestConfiguration { }
MySecurityConfig(用于生产):
@Configuration @EnableOAuth2Client @EnableResourceServer @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/v1/**").authenticated(); } }
自定义注解,用于从测试中注入作用域:
@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @WithSecurityContext(factory = WithOAuthSubjectSecurityContextFactory.class) public @interface WithOAuthSubject { String[] scopes() default {"myapi:write", "myapi:read"}; String subjectId() default "a1de7cc9-1b3a-4ecd-96fa-dab6059ccf6f"; }
用于处理自定义注释的工厂类:
public class WithOAuthSubjectSecurityContextFactory implements WithSecurityContextFactory<WithOAuthSubject> { private DefaultAccessTokenConverter defaultAccessTokenConverter = new DefaultAccessTokenConverter(); @Override public SecurityContext createSecurityContext(WithOAuthSubject withOAuthSubject) { SecurityContext context = SecurityContextHolder.createEmptyContext(); // Copy of response from https://myidentityserver.com/identity/connect/accesstokenvalidation Map<String, ?> remoteToken = ImmutableMap.<String, Object>builder() .put("iss", "https://myfakeidentity.example.com/identity") .put("aud", "oauth2-resource") .put("exp", OffsetDateTime.now().plusDays(1L).toEpochSecond() + "") .put("nbf", OffsetDateTime.now().plusDays(1L).toEpochSecond() + "") .put("client_id", "my-client-id") .put("scope", Arrays.asList(withOAuthSubject.scopes())) .put("sub", withOAuthSubject.subjectId()) .put("auth_time", OffsetDateTime.now().toEpochSecond() + "") .put("idp", "idsrv") .put("amr", "password") .build(); OAuth2Authentication authentication = defaultAccessTokenConverter.extractAuthentication(remoteToken); context.setAuthentication(authentication); return context; } }
我使用来自身份服务器的响应副本来创建实际的OAuth2Authentication。您可能可以复制我的代码。如果要对身份服务器重复该过程,请在org.springframework.security.oauth2.provider.token.RemoteTokenServices#loadAuthentication或中放置一个断点org.springframework.boot.autoconfigure.security.oauth2.resource.UserInfoTokenServices#extractAuthentication,这取决于您是否配置了自定义ResourceServerTokenServices。