Tim*_*Tim 48 java spring-security spring-security-oauth2
在Spring Security 4发布之后,它改进了对测试的支持,我想更新我当前的Spring security oauth2资源服务器测试.
目前,我有一个帮助器类,它OAuth2RestTemplate
使用连接到实际ResourceOwnerPasswordResourceDetails
的测试ClientId
来设置一个使用,以便AccessTokenUri
为我的测试请求一个有效的令牌.这个resttemplate然后用于在我的@WebIntegrationTest
s中发出请求.
我想通过利用Spring Security 4中的新测试支持,放弃对实际AuthorizationServer的依赖,并在我的测试中使用有效(如果有限)用户凭据.
到现在为止我都在尝试使用@WithMockUser
,@WithSecurityContext
,SecurityMockMvcConfigurers.springSecurity()
和SecurityMockMvcRequestPostProcessors.*
都没能进入通过身份验证的电话MockMvc
,我找不到在Spring示例项目的任何这样的工作的例子.
任何人都可以帮助我使用某种模拟凭证来测试我的oauth2资源服务器,同时仍然测试所施加的安全限制吗?
**编辑**示例代码可在此处获取:https://github.com/timtebeek/resource-server-testing 对于每个测试类,我理解为什么它不能正常工作,但我正在寻找方法这将允许我轻松测试安全设置.
我现在正在考虑创建一个非常宽松的OAuthServer src/test/java
,这可能会有所帮助.有没有人有任何其他建议?
Tim*_*Tim 41
要有效地测试资源服务器安全性,使用MockMvc
和RestTemplate
有助于配置AuthorizationServer
under src/test/java
:
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());
}
}
Run Code Online (Sandbox Code Playgroud)
集成测试
对于集成测试,可以简单地使用内置的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");
}
}
Run Code Online (Sandbox Code Playgroud)
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);
}
}
Run Code Online (Sandbox Code Playgroud)
MockMvc
然后,您的测试必须RequestPostProcessor
从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());
}
}
Run Code Online (Sandbox Code Playgroud)
GitHub上提供了一个完整的示例项目:https:
//github.com/timtebeek/resource-server-testing
mcl*_*sen 26
我按照我在此处阅读的说明找到了一种更简单的方法:http://docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#test-method-withsecuritycontext.此解决方案特定于测试@PreAuthorize
,#oauth2.hasScope
但我确信它也可以适用于其他情况.
我创建了一个可以应用于@Test
s 的注释:
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 "";
}
Run Code Online (Sandbox Code Playgroud)
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;
}
}
Run Code Online (Sandbox Code Playgroud)
示例测试使用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());
}
}
Run Code Online (Sandbox Code Playgroud)
这是被测控制器:
@RequestMapping(value = "/schedule", method = RequestMethod.POST)
@PreAuthorize("#oauth2.hasScope('dataLicense')")
public int schedule() {
return 0;
}
Run Code Online (Sandbox Code Playgroud)
gog*_*tad 11
春天开机1.5引入了测试片一样@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);
}
}
Run Code Online (Sandbox Code Playgroud)
测试:
@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());
}
}
Run Code Online (Sandbox Code Playgroud)
DefaultTestConfiguration:
@TestConfiguration
@Import({MySecurityConfig.class, OAuth2AutoConfiguration.class})
public class DefaultTestConfiguration {
}
Run Code Online (Sandbox Code Playgroud)
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();
}
}
Run Code Online (Sandbox Code Playgroud)
用于从测试中注入范围的自定义注释:
@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";
}
Run Code Online (Sandbox Code Playgroud)
用于处理自定义注释的工厂类:
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;
}
}
Run Code Online (Sandbox Code Playgroud)
我使用身份服务器的响应副本来创建逼真的OAuth2Authentication
.您可以只复制我的代码.如果要为身份服务器重复此过程,请在org.springframework.security.oauth2.provider.token.RemoteTokenServices#loadAuthentication
或中放置断点org.springframework.boot.autoconfigure.security.oauth2.resource.UserInfoTokenServices#extractAuthentication
,具体取决于您是否已配置自定义ResourceServerTokenServices
.
有一种替代方法,我认为它更清洁、更有意义。
该方法是自动装配令牌存储,然后添加一个测试令牌,然后其余客户端可以使用该令牌。
一个示例测试:
@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));
}
}
Run Code Online (Sandbox Code Playgroud)
我个人认为,在启用安全性的情况下对控制器进行单元测试是不合适的,因为安全性是控制器的独立层。我将创建一个集成测试,将所有层一起测试。但是,可以轻松修改上述方法以创建使用 MockMvc 的单元测试。
上面的代码受到Dave Syer 编写的Spring Security 测试的启发。
请注意,此方法适用于与授权服务器共享相同令牌存储的资源服务器。如果您的资源服务器与授权服务器不共享相同的令牌存储,我建议使用 wiremock 来模拟 http 响应。
我对此有另一种解决方案。见下文:
@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;
}
}
Run Code Online (Sandbox Code Playgroud)
希望对您有所帮助!
归档时间: |
|
查看次数: |
38107 次 |
最近记录: |