如何测试spring-security-oauth2资源服务器的安全性?

73
随着Spring Security 4的发布及其对测试的改进支持,我想更新我的当前Spring安全性oauth2资源服务器测试。目前,我有一个帮助类,它使用ResourceOwnerPasswordResourceDetails设置OAuth2RestTemplate,其中包含一个测试ClientId连接到实际的AccessTokenUri以请求我的测试有效令牌。然后在我的@WebIntegrationTest中使用此resttemplate来发出请求。通过利用Spring Security 4中的新测试支持,我想放弃对实际AuthorizationServer的依赖以及在我的测试中使用有效(如果有限)用户凭据的依赖。迄今为止,我所有尝试使用@WithMockUser、@WithSecurityContext、SecurityMockMvcConfigurers.springSecurity()和SecurityMockMvcRequestPostProcessors.*都未能通过MockMvc进行身份验证调用,并且我在Spring示例项目中找不到任何这样的工作示例。是否有人可以帮助我使用某种模拟凭据测试我的oauth2资源服务器,同时仍然测试所施加的安全性限制? **编辑**可在此处找到示例代码:https://github.com/timtebeek/resource-server-testing我了解每个测试类为什么不起作用,但我正在寻找可轻松测试安全配置的方法。现在我考虑在src/test/java下创建一个非常宽容的OAuthServer,这可能会有所帮助。是否有其他建议?

你能提供一个测试样例吗?你只是测试基于方法的安全性吗?你使用MockMvc吗?你正在对你的服务进行实际的REST调用吗? - Rob Winch
@RobWinch 我已经添加了使用每种方法的示例代码,并理解为什么它不起作用。我正在寻找在测试安全性方面仍然有效的方法。 - Tim
@myspri 将项目切换回Spring Platform Athens-SR4修复了集成测试;现在可以随意查看!我将深入研究为什么布鲁塞尔会破坏该项目... - Tim
@myspri 没问题;我已经成功解决了Spring Boot 1.5的问题;你添加的SecurityContextHolder行不应该是必要的;请查看更新后的GitHub存储库。 :) - Tim
WithMockUser(等等)无法正常工作的原因可能是缺少包含OAuth2令牌的授权标头=> ResourceTokenService不会被触发=> OAuth安全上下文是匿名的。我创建了工具来轻松添加这样的标头,被模拟的TokenStore拦截以获得预期的OAuth安全上下文(请参见我的答案,链接详细解决方案:stackoverflow.com/a/48540159/619830)。顺便说一句,@RobWinch,如果Spring的某个人能够审查、评论并可能评估将我的代码贡献给框架的机会... ;) - ch4mp
显示剩余5条评论
9个回答

50

为了有效地测试资源服务器安全性,无论是使用MockMvc还是RestTemplate,都需要在src/test/java下配置一个AuthorizationServer

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>头:

@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测试必须从OauthHelper类中获取一个RequestPostProcessor并在发出请求时传递它:

@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


使用TestingAuthenticationToken(github示例)和UsernamePasswordAuthenticationToken(此处发布的示例)有什么区别?看起来代码可以与Authentication接口的任何实现一起工作...我漏掉了什么? - treaz
你在提到我之前在GitHub上使用TestingAuthenticationToken吧:实际上没有必要使用它或者UsernamePasswordAuthenticationToken;这只是我在从ClientDetailsService中获取详细信息值时所做的更改的一部分。你可以继续使用以前的版本,但我从现在开始使用这个版本。 - Tim
这个程序运行良好,但是UsernamePasswordAuthenticationToken身份验证令牌中的权限应该是用户的而不是客户端的。 - alex
2
对读者也感兴趣的内容:http://engineering.pivotal.io/post/faking_oauth_sso/ - Tim
“mockMvc”解决方案在我们的应用程序中进行了一些特定的调整后,可以完美地运行。这一行.apply(springSecurity())非常重要,以便为集成测试添加正确的安全上下文。但就个人而言,oAuthHelper类并不是必需的,因为您可以在具有特定角色的模拟用户内模拟安全详细信息 :) - Jaja

31

我发现一个更简单的方法,在这里阅读的指导:http://docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#test-method-withsecuritycontext。这个解决方案是特定于使用#oauth2.hasScope测试@PreAuthorize,但我相信它也可以适用于其他情况。

我创建了一个可以应用于@Test的注释:

WithMockOAuth2Scope

import org.springframework.security.test.context.support.WithSecurityContext;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithMockOAuth2ScopeSecurityContextFactory.class)
public @interface WithMockOAuth2Scope {

    String scope() default "";
}

WithMockOAuth2ScopeSecurityContextFactory

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.OAuth2Request;
import org.springframework.security.test.context.support.WithSecurityContextFactory;

import java.util.HashSet;
import java.util.Set;

public class WithMockOAuth2ScopeSecurityContextFactory implements WithSecurityContextFactory<WithMockOAuth2Scope> {

    @Override
    public SecurityContext createSecurityContext(WithMockOAuth2Scope mockOAuth2Scope) {
        SecurityContext context = SecurityContextHolder.createEmptyContext();

        Set<String> scope = new HashSet<>();
        scope.add(mockOAuth2Scope.scope());

        OAuth2Request request = new OAuth2Request(null, null, null, true, scope, null, null, null, null);

        Authentication auth = new OAuth2Authentication(request, null);

        context.setAuthentication(auth);

        return context;
    }
}

使用 MockMvc 的示例测试:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class LoadScheduleControllerTest {

    private MockMvc mockMvc;

    @Autowired
    LoadScheduleController loadScheduleController;

    @Before
    public void setup() {
        mockMvc = MockMvcBuilders.standaloneSetup(loadScheduleController)
                    .build();
    }

    @Test
    @WithMockOAuth2Scope(scope = "dataLicense")
    public void testSchedule() throws Exception {
        mockMvc.perform(post("/schedule").contentType(MediaType.APPLICATION_JSON_UTF8).content(json)).andDo(print());
    }
}

这是正在测试的控制器:

@RequestMapping(value = "/schedule", method = RequestMethod.POST)
@PreAuthorize("#oauth2.hasScope('dataLicense')")
public int schedule() {
    return 0;
}

2
有趣的方法!这可能使我免于设置AuthorizationServer和获取测试令牌。但是,我在尝试将您的示例适应于在OAuth2Authentication中使用特定用户时遇到了问题。我的安全模型主要基于您是谁,而不是您的令牌范围。您有关于如何调整您的示例以支持此功能的建议吗? - Tim
1
@Tim 你应该可以将Authentication设置为任意的身份验证对象,放入安全上下文中。我认为这里的关键区别可能在于你正在尝试使用真正的OAuth2RestTemplate发送请求,而我在我的测试中是使用mockMvc来发送请求。 - mclaassen
1
谢谢!我终于能够更清晰地看待这个问题,并已相应地更新了我的示例项目:https://github.com/timtebeek/resource-server-testing/pull/1 现在两种方法都可以工作,但用途不同。对于基于用户名/范围的访问规则,我建议您使用您的方法;在我的情况下,我解码访问令牌并基于其中的属性进行多租户访问规则;那确实需要一个实际的令牌。 :) - Tim
2
如果有人对模拟令牌值感兴趣,可以设置OAuth2AuthenticationDetails中的details,并在具有属性OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE和"Bearer"以及OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE和令牌值的httpServletrequest上进行传递。之后,您可以使用((OAuth2AuthenticationDetails) SecurityContextHolder.getContext().getAuthentication().getDetails()).getTokenValue()在应用程序中访问令牌值。 - Dherik
我不得不将无状态更改为false - DEWA Kazuyuki - 出羽和之
显示剩余3条评论

14

Spring Boot 1.5 推出了像 test slices 这样的东西,比如 @WebMvcTest。使用这些测试切片并手动加载 OAuth2AutoConfiguration 可以减少您的测试样板代码,并且比基于 @SpringBootTest 的解决方案运行更快。如果您还导入了生产安全配置,您可以测试已配置的筛选链是否适用于您的 Web 服务。

以下是设置和一些其他可能有益的类:

控制器:

@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());
    }
}

默认测试配置:

@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#loadAuthenticationorg.springframework.boot.autoconfigure.security.oauth2.resource.UserInfoTokenServices#extractAuthentication中设置断点,具体取决于您是否配置了自定义的ResourceServerTokenServices


哇,感谢您付出努力提出全新的测试方式,正如您所说,这可能更快,并且不会设置应用程序上下文中不需要的部分。非常酷! :) - Tim
我尝试了你的解决方案,但在构建测试请求时忘记添加身份验证标头,当然它没有起作用 :/。也许可以更加强调每个涉及安全的请求都需要添加此授权标头的必要性? - ch4mp

7
我有另一种解决方案。请看下面:
@RunWith(SpringRunner.class)
@SpringBootTest
@WebAppConfiguration
@ActiveProfiles("test")
public class AccountContollerTest {

    public static Logger log = LoggerFactory.getLogger(AccountContollerTest.class);

    @Autowired
    private WebApplicationContext webApplicationContext;

    private MockMvc mvc;

    @Autowired
    private FilterChainProxy springSecurityFilterChain;

    @Autowired
    private UserRepository users;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private CustomClientDetailsService clientDetialsService;

    @Before
    public void setUp() {
         mvc = MockMvcBuilders
                 .webAppContextSetup(webApplicationContext)
                 .apply(springSecurity(springSecurityFilterChain))
                 .build();

         BaseClientDetails testClient = new ClientBuilder("testclient")
                    .secret("testclientsecret")
                    .authorizedGrantTypes("password")
                    .scopes("read", "write")
                    .autoApprove(true)
                    .build();

         clientDetialsService.addClient(testClient);

         User user = createDefaultUser("testuser", passwordEncoder.encode("testpassword"), "max", "Mustermann", new Email("myemail@test.de"));

         users.deleteAll();
         users.save(user);

    }

    @Test
    public void shouldRetriveAccountDetailsWithValidAccessToken() throws Exception {
        mvc.perform(get("/api/me")
                .header("Authorization", "Bearer " + validAccessToken())
                .accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andDo(print())
                .andExpect(jsonPath("$.userAuthentication.name").value("testuser"))
                .andExpect(jsonPath("$.authorities[0].authority").value("ROLE_USER"));
    }

    @Test
    public void shouldReciveHTTPStatusUnauthenticatedWithoutAuthorizationHeader() throws Exception{
        mvc.perform(get("/api/me")
                .accept(MediaType.APPLICATION_JSON))
                .andDo(print())
                .andExpect(status().isUnauthorized());
    }

    private String validAccessToken() throws Exception {  
        String username = "testuser";
        String password = "testpassword";

        MockHttpServletResponse response = mvc
            .perform(post("/oauth/token")
                    .header("Authorization", "Basic "
                           + new String(Base64Utils.encode(("testclient:testclientsecret")
                            .getBytes())))
                    .param("username", username)
                    .param("password", password)
                    .param("grant_type", "password"))
            .andDo(print())
            .andReturn().getResponse();

    return new ObjectMapper()
            .readValue(response.getContentAsByteArray(), OAuthToken.class)
            .accessToken;
    }

    @JsonIgnoreProperties(ignoreUnknown = true)
    private static class OAuthToken {
        @JsonProperty("access_token")
        public String accessToken;
    }
}

希望这能有所帮助!


6

我认为有一种更加简洁和有意义的替代方法。

这种方法是自动装配令牌存储,然后添加一个测试令牌,可以供其余客户端使用。

一个示例测试:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class UserControllerIT {

    @Autowired
    private TestRestTemplate testRestTemplate;

    @Autowired
    private TokenStore tokenStore;

    @Before
    public void setUp() {

        final OAuth2AccessToken token = new DefaultOAuth2AccessToken("FOO");
        final ClientDetails client = new BaseClientDetails("client", null, "read", "client_credentials", "ROLE_CLIENT");
        final OAuth2Authentication authentication = new OAuth2Authentication(
                new TokenRequest(null, "client", null, "client_credentials").createOAuth2Request(client), null);

        tokenStore.storeAccessToken(token, authentication);

    }

    @Test
    public void testGivenPathUsersWhenGettingForEntityThenStatusCodeIsOk() {

        final HttpHeaders headers = new HttpHeaders();
        headers.add(HttpHeaders.AUTHORIZATION, "Bearer FOO");
        headers.setContentType(MediaType.APPLICATION_JSON);

        // Given Path Users
        final UriComponentsBuilder uri = UriComponentsBuilder.fromPath("/api/users");

        // When Getting For Entity
        final ResponseEntity<String> response = testRestTemplate.exchange(uri.build().toUri(), HttpMethod.GET,
                new HttpEntity<>(headers), String.class);

        // Then Status Code Is Ok
        assertThat(response.getStatusCode(), is(HttpStatus.OK));
    }

}

个人认为,启用安全性后,对控制器进行单元测试是不恰当的,因为安全性是控制器的一个独立层。我会创建一个集成测试,测试所有层一起工作。但是,上述方法可以轻松修改为使用MockMvc的单元测试。
上述代码受Dave Syer编写的Spring Security测试的启发。
请注意,此方法适用于与授权服务器共享相同令牌存储的资源服务器。如果您的资源服务器与授权服务器不共享相同的令牌存储,则建议使用WireMock模拟HTTP响应。

在下面的头部区域中,可以传递什么替代“Bearer Foo”? headers.add(HttpHeaders.AUTHORIZATION, "Bearer FOO"); - user178822
@user178822 在设置中与 DefaultOAuth2AccessToken 值匹配的任何内容。在此示例中,令牌为 FOO。 - Thomas Turrell-Croft

4

我找到了一种简单快捷的方法,可以使用任何令牌存储来测试Spring Security资源服务器。在我的示例中,@EnabledResourceServer 使用 JWT 令牌存储。

这里的魔法是在集成测试中用 InMemoryTokenStore 替换了 JwtTokenStore

@RunWith (SpringRunner.class)
@SpringBootTest (classes = {Application.class}, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles ("test")
@TestPropertySource (locations = "classpath:application.yml")
@Transactional
public class ResourceServerIntegrationTest {

@Autowired
private TokenStore tokenStore;

@Autowired
private ObjectMapper jacksonObjectMapper;

@LocalServerPort
int port;

@Configuration
protected static class PrepareTokenStore {

    @Bean
    @Primary
    public TokenStore tokenStore() {
        return new InMemoryTokenStore();
    }

}

private OAuth2AccessToken token;
private OAuth2Authentication authentication;

@Before
public void init() {

    RestAssured.port = port;

    token = new DefaultOAuth2AccessToken("FOO");
    ClientDetails client = new BaseClientDetails("client", null, "read", "client_credentials", "ROLE_READER,ROLE_CLIENT");

    // Authorities
    List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
    authorities.add(new SimpleGrantedAuthority("ROLE_READER"));
    UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken("writer", "writer", authorities);

    authentication = new OAuth2Authentication(new TokenRequest(null, "client", null, "client_credentials").createOAuth2Request(client), authenticationToken);
    tokenStore.storeAccessToken(token, authentication);

}

@Test
public void gbsUserController_findById() throws Exception {

    RestAssured.given().log().all().when().headers("Authorization", "Bearer FOO").get("/gbsusers/{id}", 2L).then().log().all().statusCode(HttpStatus.OK.value());

}

4

好的,我还没有能够使用新的@WithMockUser或相关注释来测试我的独立oauth2 JWT令牌保护的资源服务器。

作为解决方法,我已经能够通过设置src/test/java下的宽松的AuthorizationServer,并在其中定义两个客户端,我通过helper类引用这些客户端来集成测试我的资源服务器安全性。这让我有了一定的进展,但是我仍然不易于测试各种用户、角色、范围等内容。

我猜从现在开始,实现自己的WithSecurityContextFactory应该更容易了,它创建一个OAuth2Authentication而不是通常的UsernamePasswordAuthentication。然而,我仍然无法确定如何轻松地设置这个问题的细节。欢迎您提出任何评论或建议,以帮助我解决这个问题。


1

我尝试详细说明的另一个解决方案 :-D

它基于设置授权标头,类似于上面的一些方法,但我想要:

  • 不创建实际有效的JWT令牌并使用所有JWT身份验证堆栈(单元测试...)
  • 测试认证包含测试用例定义的范围和权限

所以我:

  • 创建了自定义注释来设置每个测试的OAuth2Authentication: @WithMockOAuth2Client(直接客户端连接)和@WithMockOAuth2User(代表最终用户操作的客户端 => 包括我的自定义@WithMockOAuth2Client和Spring @WithMockUser)
  • @MockBean TokenStore返回配置了上述自定义注释的OAuth2Authentication
  • 提供了MockHttpServletRequestBuilder工厂,设置特定的Authorization标头,被TokenStore mock拦截以注入预期的认证。

结果让您进行测试:

@WebMvcTest(MyController.class) // Controller to unit-test
@Import(WebSecurityConfig.class) // your class extending WebSecurityConfigurerAdapter
public class MyControllerTest extends OAuth2ControllerTest {

    @Test
    public void testWithUnauthenticatedClient() throws Exception {
        api.post(payload, "/endpoint")
                .andExpect(...);
    }

    @Test
    @WithMockOAuth2Client
    public void testWithDefaultClient() throws Exception {
        api.get("/endpoint")
                .andExpect(...);
    }

    @Test
    @WithMockOAuth2User
    public void testWithDefaultClientOnBehalfDefaultUser() throws Exception {
            MockHttpServletRequestBuilder req = api.postRequestBuilder(null, "/uaa/refresh")
                .header("refresh_token", JWT_REFRESH_TOKEN);

        api.perform(req)
                .andExpect(status().isOk())
                .andExpect(...)
    }

    @Test
    @WithMockOAuth2User(
        client = @WithMockOAuth2Client(
                clientId = "custom-client",
                scope = {"custom-scope", "other-scope"},
                authorities = {"custom-authority", "ROLE_CUSTOM_CLIENT"}),
        user = @WithMockUser(
                username = "custom-username",
                authorities = {"custom-user-authority"}))
    public void testWithCustomClientOnBehalfCustomUser() throws Exception {
        api.get(MediaType.APPLICATION_ATOM_XML, "/endpoint")
                .andExpect(status().isOk())
                .andExpect(xpath(...));
    }
}

1
重新阅读整个堆栈,我才意识到这个解决方案离我的解决方案有多近。我曾经尝试过,错过了设置标题的那一行,并从头开始构建了自己的解决方案。最终,我只是在OAuth2Authentication配置选项上再推进一步,并添加了包装器,以便永远不会忘记这个可恶的标题。 - ch4mp

0
我尝试了很多方法,但我的解决方案比其他人更简单。我在spring boot应用程序中使用OAuth2 JWT身份验证。我的目标是进行契约测试。我正在使用groovy编写脚本,并且合同插件会为我生成测试代码。因此,我不能干扰这些代码。我有一个简单的BaseTest类。我需要在这个类中进行所有必要的配置。这个解决方案对我很有效。
导入的依赖项:
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-test</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-contract-verifier</artifactId>
    <version>2.1.1.RELEASE</version>
    <scope>test</scope>
</dependency>

已导入的插件:

    <plugin>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-contract-maven-plugin</artifactId>
        <version>2.1.1.RELEASE</version>
        <extensions>true</extensions>
        <configuration>
            <baseClassForTests>com.test.services.BaseTestClass
            </baseClassForTests>
        </configuration>
    </plugin>

BaseTestClass.java

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@DirtiesContext
@AutoConfigureMessageVerifier
@ContextConfiguration
@WithMockUser(username = "admin", roles = {"USER", "ADMIN"})
public class BaseTestClass {

    @Autowired
    private MyController myController;

    @Autowired
    private WebApplicationContext webApplicationContext;

    @Before
    public void setup() {
        StandaloneMockMvcBuilder standaloneMockMvcBuilder = MockMvcBuilders.standaloneSetup(myController);
        RestAssuredMockMvc.standaloneSetup(standaloneMockMvcBuilder);
        RestAssuredMockMvc.webAppContextSetup(webApplicationContext);
    }

}

myFirstScenario.groovy (包:"/test/resources/contracts"):

import org.springframework.cloud.contract.spec.Contract

Contract.make {
    description "should return ok"
    request {
        method GET()
        url("/api/contract/test") {
            headers {
                header("Authorization","Bearer FOO")
            }
        }
    }
    response {
        status 200
    }
}

MyController.java:

@RestController
@RequestMapping(value = "/api/contract")
@PreAuthorize("hasRole('ROLE_ADMIN')")
public class MyController {
...
}

如果你想测试非管理员用户,可以使用以下代码:

@WithMockUser(username = "admin", roles = {"USER"})

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接