SecurityMockServerConfigurers.java
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.test.web.reactive.server;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.jspecify.annotations.NullUnmarked;
import reactor.core.publisher.Mono;
import org.springframework.context.ApplicationContext;
import org.springframework.core.convert.converter.Converter;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.lang.Nullable;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextImpl;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.web.DefaultReactiveOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.reactive.result.method.annotation.OAuth2AuthorizedClientArgumentResolver;
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.server.WebSessionServerOAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimNames;
import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimNames;
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthentication;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionAuthenticatedPrincipal;
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors;
import org.springframework.security.web.server.csrf.CsrfWebFilter;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.test.web.reactive.server.MockServerConfigurer;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.test.web.reactive.server.WebTestClientConfigurer;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver;
import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer;
import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerAdapter;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
/**
* Test utilities for working with Spring Security and
* {@link org.springframework.test.web.reactive.server.WebTestClient.Builder#apply(WebTestClientConfigurer)}.
*
* @author Rob Winch
* @since 5.0
*/
public final class SecurityMockServerConfigurers {
private SecurityMockServerConfigurers() {
}
/**
* Sets up Spring Security's {@link WebTestClient} test support
* @return the MockServerConfigurer to use
*/
public static MockServerConfigurer springSecurity() {
return new MockServerConfigurer() {
@Override
public void beforeServerCreated(WebHttpHandlerBuilder builder) {
builder.filters((filters) -> filters.add(0, new MutatorFilter()));
}
};
}
/**
* Updates the ServerWebExchange to use the provided Authentication as the Principal
* @param authentication the Authentication to use.
* @return the configurer to use
*/
public static <T extends WebTestClientConfigurer & MockServerConfigurer> T mockAuthentication(
Authentication authentication) {
return (T) new MutatorWebTestClientConfigurer(() -> Mono.just(authentication).map(SecurityContextImpl::new));
}
/**
* Updates the ServerWebExchange to use the provided UserDetails to create a
* UsernamePasswordAuthenticationToken as the Principal
* @param userDetails the UserDetails to use.
* @return the configurer to use
*/
public static <T extends WebTestClientConfigurer & MockServerConfigurer> T mockUser(UserDetails userDetails) {
return mockAuthentication(UsernamePasswordAuthenticationToken.authenticated(userDetails,
userDetails.getPassword(), userDetails.getAuthorities()));
}
/**
* Updates the ServerWebExchange to use a UserDetails to create a
* UsernamePasswordAuthenticationToken as the Principal. This uses a default username
* of "user", password of "password", and granted authorities of "ROLE_USER".
* @return the {@link UserExchangeMutator} to use
*/
public static UserExchangeMutator mockUser() {
return mockUser("user");
}
/**
* Updates the ServerWebExchange to use a UserDetails to create a
* UsernamePasswordAuthenticationToken as the Principal. This uses a default password
* of "password" and granted authorities of "ROLE_USER".
* @return the {@link WebTestClientConfigurer} to use
*/
public static UserExchangeMutator mockUser(String username) {
return new UserExchangeMutator(username);
}
/**
* Updates the ServerWebExchange to establish a {@link SecurityContext} that has a
* {@link JwtAuthenticationToken} for the {@link Authentication} and a {@link Jwt} for
* the {@link Authentication#getPrincipal()}. All details are declarative and do not
* require the JWT to be valid.
* @return the {@link JwtMutator} to further configure or use
* @since 5.2
*/
public static JwtMutator mockJwt() {
return new JwtMutator();
}
/**
* Updates the ServerWebExchange to establish a {@link SecurityContext} that has a
* {@link BearerTokenAuthentication} for the {@link Authentication} and an
* {@link OAuth2AuthenticatedPrincipal} for the {@link Authentication#getPrincipal()}.
* All details are declarative and do not require the token to be valid.
* @return the {@link OpaqueTokenMutator} to further configure or use
* @since 5.3
*/
public static OpaqueTokenMutator mockOpaqueToken() {
return new OpaqueTokenMutator();
}
/**
* Updates the ServerWebExchange to establish a {@link SecurityContext} that has a
* {@link OAuth2AuthenticationToken} for the {@link Authentication}. All details are
* declarative and do not require the corresponding OAuth 2.0 tokens to be valid.
* @return the {@link OAuth2LoginMutator} to further configure or use
* @since 5.3
*/
@NullUnmarked
public static OAuth2LoginMutator mockOAuth2Login() {
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "access-token", null,
null, Collections.singleton("read"));
return new OAuth2LoginMutator(accessToken);
}
/**
* Updates the ServerWebExchange to establish a {@link SecurityContext} that has a
* {@link OAuth2AuthenticationToken} for the {@link Authentication}. All details are
* declarative and do not require the corresponding OAuth 2.0 tokens to be valid.
* @return the {@link OidcLoginMutator} to further configure or use
* @since 5.3
*/
@NullUnmarked
public static OidcLoginMutator mockOidcLogin() {
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "access-token", null,
null, Collections.singleton("read"));
return new OidcLoginMutator(accessToken);
}
/**
* Updates the ServerWebExchange to establish a {@link OAuth2AuthorizedClient} in the
* session. All details are declarative and do not require the corresponding OAuth 2.0
* tokens to be valid.
*
* <p>
* The support works by associating the authorized client to the ServerWebExchange
* using a {@link ServerOAuth2AuthorizedClientRepository}
* </p>
* @return the {@link OAuth2ClientMutator} to further configure or use
* @since 5.3
*/
public static OAuth2ClientMutator mockOAuth2Client() {
return new OAuth2ClientMutator();
}
/**
* Updates the ServerWebExchange to establish a {@link OAuth2AuthorizedClient} in the
* session. All details are declarative and do not require the corresponding OAuth 2.0
* tokens to be valid.
*
* <p>
* The support works by associating the authorized client to the ServerWebExchange
* using a {@link ServerOAuth2AuthorizedClientRepository}
* </p>
* @param registrationId The registration id associated with the
* {@link OAuth2AuthorizedClient}
* @return the {@link OAuth2ClientMutator} to further configure or use
* @since 5.3
*/
public static OAuth2ClientMutator mockOAuth2Client(String registrationId) {
return new OAuth2ClientMutator(registrationId);
}
public static CsrfMutator csrf() {
return new CsrfMutator();
}
public static final class CsrfMutator implements WebTestClientConfigurer, MockServerConfigurer {
private CsrfMutator() {
}
@NullUnmarked
@Override
public void afterConfigurerAdded(WebTestClient.Builder builder,
@Nullable WebHttpHandlerBuilder httpHandlerBuilder, @Nullable ClientHttpConnector connector) {
CsrfWebFilter filter = new CsrfWebFilter();
filter.setRequireCsrfProtectionMatcher((e) -> ServerWebExchangeMatcher.MatchResult.notMatch());
httpHandlerBuilder.filters((filters) -> filters.add(0, filter));
}
@Override
public void afterConfigureAdded(WebTestClient.MockServerSpec<?> serverSpec) {
}
@Override
public void beforeServerCreated(WebHttpHandlerBuilder builder) {
}
}
/**
* Updates the WebServerExchange using {@code {@link
* SecurityMockServerConfigurers#mockUser(UserDetails)}}. Defaults to use a password
* of "password" and granted authorities of "ROLE_USER".
*/
public static final class UserExchangeMutator implements WebTestClientConfigurer, MockServerConfigurer {
private final User.UserBuilder userBuilder;
private UserExchangeMutator(String username) {
this.userBuilder = User.withUsername(username);
password("password");
roles("USER");
}
/**
* Specifies the password to use. Default is "password".
* @param password the password to use
* @return the UserExchangeMutator
*/
public UserExchangeMutator password(String password) {
this.userBuilder.password(password);
return this;
}
/**
* Specifies the roles to use. Default is "USER". This is similar to authorities
* except each role is automatically prefixed with "ROLE_USER".
* @param roles the roles to use.
* @return the UserExchangeMutator
*/
public UserExchangeMutator roles(String... roles) {
this.userBuilder.roles(roles);
return this;
}
/**
* Specifies the {@code GrantedAuthority}s to use. Default is "ROLE_USER".
* @param authorities the authorities to use.
* @return the UserExchangeMutator
*/
public UserExchangeMutator authorities(GrantedAuthority... authorities) {
this.userBuilder.authorities(authorities);
return this;
}
/**
* Specifies the {@code GrantedAuthority}s to use. Default is "ROLE_USER".
* @param authorities the authorities to use.
* @return the UserExchangeMutator
*/
public UserExchangeMutator authorities(Collection<? extends GrantedAuthority> authorities) {
this.userBuilder.authorities(authorities);
return this;
}
/**
* Specifies the {@code GrantedAuthority}s to use. Default is "ROLE_USER".
* @param authorities the authorities to use.
* @return the UserExchangeMutator
*/
public UserExchangeMutator authorities(String... authorities) {
this.userBuilder.authorities(authorities);
return this;
}
public UserExchangeMutator accountExpired(boolean accountExpired) {
this.userBuilder.accountExpired(accountExpired);
return this;
}
public UserExchangeMutator accountLocked(boolean accountLocked) {
this.userBuilder.accountLocked(accountLocked);
return this;
}
public UserExchangeMutator credentialsExpired(boolean credentialsExpired) {
this.userBuilder.credentialsExpired(credentialsExpired);
return this;
}
public UserExchangeMutator disabled(boolean disabled) {
this.userBuilder.disabled(disabled);
return this;
}
@Override
public void beforeServerCreated(WebHttpHandlerBuilder builder) {
configurer().beforeServerCreated(builder);
}
@Override
public void afterConfigureAdded(WebTestClient.MockServerSpec<?> serverSpec) {
configurer().afterConfigureAdded(serverSpec);
}
@Override
public void afterConfigurerAdded(WebTestClient.Builder builder,
@Nullable WebHttpHandlerBuilder webHttpHandlerBuilder,
@Nullable ClientHttpConnector clientHttpConnector) {
configurer().afterConfigurerAdded(builder, webHttpHandlerBuilder, clientHttpConnector);
}
private <T extends WebTestClientConfigurer & MockServerConfigurer> T configurer() {
return mockUser(this.userBuilder.build());
}
}
private static final class MutatorWebTestClientConfigurer implements WebTestClientConfigurer, MockServerConfigurer {
private final Supplier<Mono<SecurityContext>> context;
private MutatorWebTestClientConfigurer(Supplier<Mono<SecurityContext>> context) {
this.context = context;
}
@Override
public void beforeServerCreated(WebHttpHandlerBuilder builder) {
builder.filters(addSetupMutatorFilter());
}
@NullUnmarked
@Override
public void afterConfigurerAdded(WebTestClient.Builder builder,
@Nullable WebHttpHandlerBuilder webHttpHandlerBuilder,
@Nullable ClientHttpConnector clientHttpConnector) {
webHttpHandlerBuilder.filters(addSetupMutatorFilter());
}
private Consumer<List<WebFilter>> addSetupMutatorFilter() {
return (filters) -> filters.add(0, new SetupMutatorFilter(this.context));
}
}
private static final class SetupMutatorFilter implements WebFilter {
private final Supplier<Mono<SecurityContext>> context;
private SetupMutatorFilter(Supplier<Mono<SecurityContext>> context) {
this.context = context;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain webFilterChain) {
exchange.getAttributes().computeIfAbsent(MutatorFilter.ATTRIBUTE_NAME, (key) -> this.context);
return webFilterChain.filter(exchange);
}
}
private static class MutatorFilter implements WebFilter {
public static final String ATTRIBUTE_NAME = "context";
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain webFilterChain) {
Supplier<Mono<SecurityContext>> context = exchange.getAttribute(ATTRIBUTE_NAME);
if (context != null) {
exchange.getAttributes().remove(ATTRIBUTE_NAME);
return webFilterChain.filter(exchange)
.contextWrite(ReactiveSecurityContextHolder.withSecurityContext(context.get()));
}
return webFilterChain.filter(exchange);
}
}
/**
* Updates the WebServerExchange using {@code {@link
* SecurityMockServerConfigurers#mockAuthentication(Authentication)}}.
*
* @author J��r��me Wacongne <ch4mp@c4-soft.com>
* @author Josh Cummings
* @since 5.2
*/
public static final class JwtMutator implements WebTestClientConfigurer, MockServerConfigurer {
private Jwt jwt;
private Converter<Jwt, Collection<GrantedAuthority>> authoritiesConverter = new JwtGrantedAuthoritiesConverter();
private JwtMutator() {
jwt((jwt) -> {
});
}
/**
* Use the given {@link Jwt.Builder} {@link Consumer} to configure the underlying
* {@link Jwt}
*
* This method first creates a default {@link Jwt.Builder} instance with default
* values for the {@code alg}, {@code sub}, and {@code scope} claims. The
* {@link Consumer} can then modify these or provide additional configuration.
*
* Calling {@link SecurityMockServerConfigurers#mockJwt()} is the equivalent of
* calling {@code SecurityMockMvcRequestPostProcessors.mockJwt().jwt(() -> {})}.
* @param jwtBuilderConsumer For configuring the underlying {@link Jwt}
* @return the {@link JwtMutator} for further configuration
*/
public JwtMutator jwt(Consumer<Jwt.Builder> jwtBuilderConsumer) {
Jwt.Builder jwtBuilder = Jwt.withTokenValue("token")
.header("alg", "none")
.claim(JwtClaimNames.SUB, "user")
.claim("scope", "read");
jwtBuilderConsumer.accept(jwtBuilder);
this.jwt = jwtBuilder.build();
return this;
}
/**
* Use the given {@link Jwt}
* @param jwt The {@link Jwt} to use
* @return the {@link JwtMutator} for further configuration
*/
public JwtMutator jwt(Jwt jwt) {
this.jwt = jwt;
return this;
}
/**
* Use the provided authorities in the token
* @param authorities the authorities to use
* @return the {@link JwtMutator} for further configuration
*/
public JwtMutator authorities(Collection<GrantedAuthority> authorities) {
Assert.notNull(authorities, "authorities cannot be null");
this.authoritiesConverter = (jwt) -> authorities;
return this;
}
/**
* Use the provided authorities in the token
* @param authorities the authorities to use
* @return the {@link JwtMutator} for further configuration
*/
public JwtMutator authorities(GrantedAuthority... authorities) {
Assert.notNull(authorities, "authorities cannot be null");
this.authoritiesConverter = (jwt) -> Arrays.asList(authorities);
return this;
}
/**
* Provides the configured {@link Jwt} so that custom authorities can be derived
* from it
* @param authoritiesConverter the conversion strategy from {@link Jwt} to a
* {@link Collection} of {@link GrantedAuthority}s
* @return the {@link JwtMutator} for further configuration
*/
public JwtMutator authorities(Converter<Jwt, Collection<GrantedAuthority>> authoritiesConverter) {
Assert.notNull(authoritiesConverter, "authoritiesConverter cannot be null");
this.authoritiesConverter = authoritiesConverter;
return this;
}
@Override
public void beforeServerCreated(WebHttpHandlerBuilder builder) {
configurer().beforeServerCreated(builder);
}
@Override
public void afterConfigureAdded(WebTestClient.MockServerSpec<?> serverSpec) {
configurer().afterConfigureAdded(serverSpec);
}
@NullUnmarked
@Override
public void afterConfigurerAdded(WebTestClient.Builder builder,
@Nullable WebHttpHandlerBuilder httpHandlerBuilder, @Nullable ClientHttpConnector connector) {
httpHandlerBuilder.filter((exchange, chain) -> {
CsrfWebFilter.skipExchange(exchange);
return chain.filter(exchange);
});
configurer().afterConfigurerAdded(builder, httpHandlerBuilder, connector);
}
@NullUnmarked
private <T extends WebTestClientConfigurer & MockServerConfigurer> T configurer() {
return mockAuthentication(
new JwtAuthenticationToken(this.jwt, this.authoritiesConverter.convert(this.jwt)));
}
}
/**
* @author Josh Cummings
* @since 5.3
*/
public static final class OpaqueTokenMutator implements WebTestClientConfigurer, MockServerConfigurer {
private Supplier<Map<String, Object>> attributes = this::defaultAttributes;
private Supplier<Collection<GrantedAuthority>> authorities = this::defaultAuthorities;
private Supplier<OAuth2AuthenticatedPrincipal> principal = this::defaultPrincipal;
private OpaqueTokenMutator() {
}
/**
* Mutate the attributes using the given {@link Consumer}
* @param attributesConsumer The {@link Consumer} for mutating the {@code Map} of
* attributes
* @return the {@link OpaqueTokenMutator} for further configuration
*/
public OpaqueTokenMutator attributes(Consumer<Map<String, Object>> attributesConsumer) {
Assert.notNull(attributesConsumer, "attributesConsumer cannot be null");
this.attributes = () -> {
Map<String, Object> attributes = defaultAttributes();
attributesConsumer.accept(attributes);
return attributes;
};
this.principal = this::defaultPrincipal;
return this;
}
/**
* Use the provided authorities in the resulting principal
* @param authorities the authorities to use
* @return the {@link OpaqueTokenMutator} for further configuration
*/
public OpaqueTokenMutator authorities(Collection<GrantedAuthority> authorities) {
Assert.notNull(authorities, "authorities cannot be null");
this.authorities = () -> authorities;
this.principal = this::defaultPrincipal;
return this;
}
/**
* Use the provided authorities in the resulting principal
* @param authorities the authorities to use
* @return the {@link OpaqueTokenMutator} for further configuration
*/
public OpaqueTokenMutator authorities(GrantedAuthority... authorities) {
Assert.notNull(authorities, "authorities cannot be null");
this.authorities = () -> Arrays.asList(authorities);
this.principal = this::defaultPrincipal;
return this;
}
/**
* Use the provided principal
* @param principal the principal to use
* @return the {@link OpaqueTokenMutator} for further configuration
*/
public OpaqueTokenMutator principal(OAuth2AuthenticatedPrincipal principal) {
Assert.notNull(principal, "principal cannot be null");
this.principal = () -> principal;
return this;
}
@Override
public void beforeServerCreated(WebHttpHandlerBuilder builder) {
configurer().beforeServerCreated(builder);
}
@Override
public void afterConfigureAdded(WebTestClient.MockServerSpec<?> serverSpec) {
configurer().afterConfigureAdded(serverSpec);
}
@NullUnmarked
@Override
public void afterConfigurerAdded(WebTestClient.Builder builder,
@Nullable WebHttpHandlerBuilder httpHandlerBuilder, @Nullable ClientHttpConnector connector) {
httpHandlerBuilder.filter((exchange, chain) -> {
CsrfWebFilter.skipExchange(exchange);
return chain.filter(exchange);
});
configurer().afterConfigurerAdded(builder, httpHandlerBuilder, connector);
}
private <T extends WebTestClientConfigurer & MockServerConfigurer> T configurer() {
OAuth2AuthenticatedPrincipal principal = this.principal.get();
OAuth2AccessToken accessToken = getOAuth2AccessToken(principal);
BearerTokenAuthentication token = new BearerTokenAuthentication(principal, accessToken,
principal.getAuthorities());
return mockAuthentication(token);
}
private Map<String, Object> defaultAttributes() {
Map<String, Object> attributes = new HashMap<>();
attributes.put(OAuth2TokenIntrospectionClaimNames.SUB, "user");
attributes.put(OAuth2TokenIntrospectionClaimNames.SCOPE, "read");
return attributes;
}
private Collection<GrantedAuthority> defaultAuthorities() {
Map<String, Object> attributes = this.attributes.get();
Object scope = attributes.get(OAuth2TokenIntrospectionClaimNames.SCOPE);
if (scope == null) {
return Collections.emptyList();
}
if (scope instanceof Collection) {
return getAuthorities((Collection) scope);
}
String scopes = scope.toString();
if (!StringUtils.hasText(scopes)) {
return Collections.emptyList();
}
return getAuthorities(Arrays.asList(scopes.split(" ")));
}
private OAuth2AuthenticatedPrincipal defaultPrincipal() {
return new OAuth2IntrospectionAuthenticatedPrincipal(this.attributes.get(), this.authorities.get());
}
private Collection<GrantedAuthority> getAuthorities(Collection<?> scopes) {
return scopes.stream()
.map((scope) -> new SimpleGrantedAuthority("SCOPE_" + scope))
.collect(Collectors.toList());
}
private OAuth2AccessToken getOAuth2AccessToken(OAuth2AuthenticatedPrincipal principal) {
Instant expiresAt = getInstant(principal.getAttributes(), "exp");
Instant issuedAt = getInstant(principal.getAttributes(), "iat");
return new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "token", issuedAt, expiresAt);
}
@NullUnmarked
private Instant getInstant(Map<String, Object> attributes, String name) {
Object value = attributes.get(name);
if (value == null) {
return null;
}
if (value instanceof Instant) {
return (Instant) value;
}
throw new IllegalArgumentException(name + " attribute must be of type Instant");
}
}
/**
* @author Josh Cummings
* @since 5.3
*/
public static final class OAuth2LoginMutator implements WebTestClientConfigurer, MockServerConfigurer {
private final String nameAttributeKey = "sub";
private ClientRegistration clientRegistration;
private OAuth2AccessToken accessToken;
private Supplier<Collection<GrantedAuthority>> authorities = this::defaultAuthorities;
private Supplier<Map<String, Object>> attributes = this::defaultAttributes;
private Supplier<OAuth2User> oauth2User = this::defaultPrincipal;
private OAuth2LoginMutator(OAuth2AccessToken accessToken) {
this.accessToken = accessToken;
this.clientRegistration = clientRegistrationBuilder().build();
}
/**
* Use the provided authorities in the {@link Authentication}
* @param authorities the authorities to use
* @return the {@link OAuth2LoginMutator} for further configuration
*/
public OAuth2LoginMutator authorities(Collection<GrantedAuthority> authorities) {
Assert.notNull(authorities, "authorities cannot be null");
this.authorities = () -> authorities;
this.oauth2User = this::defaultPrincipal;
return this;
}
/**
* Use the provided authorities in the {@link Authentication}
* @param authorities the authorities to use
* @return the {@link OAuth2LoginMutator} for further configuration
*/
public OAuth2LoginMutator authorities(GrantedAuthority... authorities) {
Assert.notNull(authorities, "authorities cannot be null");
this.authorities = () -> Arrays.asList(authorities);
this.oauth2User = this::defaultPrincipal;
return this;
}
/**
* Mutate the attributes using the given {@link Consumer}
* @param attributesConsumer The {@link Consumer} for mutating the {@code Map} of
* attributes
* @return the {@link OAuth2LoginMutator} for further configuration
*/
public OAuth2LoginMutator attributes(Consumer<Map<String, Object>> attributesConsumer) {
Assert.notNull(attributesConsumer, "attributesConsumer cannot be null");
this.attributes = () -> {
Map<String, Object> attributes = defaultAttributes();
attributesConsumer.accept(attributes);
return attributes;
};
this.oauth2User = this::defaultPrincipal;
return this;
}
/**
* Use the provided {@link OAuth2User} as the authenticated user.
* @param oauth2User the {@link OAuth2User} to use
* @return the {@link OAuth2LoginMutator} for further configuration
*/
public OAuth2LoginMutator oauth2User(OAuth2User oauth2User) {
this.oauth2User = () -> oauth2User;
return this;
}
/**
* Use the provided {@link ClientRegistration} as the client to authorize.
* <p>
* The supplied {@link ClientRegistration} will be registered into a
* {@link ServerOAuth2AuthorizedClientRepository}.
* @param clientRegistration the {@link ClientRegistration} to use
* @return the {@link OAuth2LoginMutator} for further configuration
*/
public OAuth2LoginMutator clientRegistration(ClientRegistration clientRegistration) {
this.clientRegistration = clientRegistration;
return this;
}
@Override
public void beforeServerCreated(WebHttpHandlerBuilder builder) {
OAuth2AuthenticationToken token = getToken();
mockOAuth2Client().accessToken(this.accessToken)
.clientRegistration(this.clientRegistration)
.principalName(token.getPrincipal().getName())
.beforeServerCreated(builder);
mockAuthentication(token).beforeServerCreated(builder);
}
@Override
public void afterConfigureAdded(WebTestClient.MockServerSpec<?> serverSpec) {
OAuth2AuthenticationToken token = getToken();
mockOAuth2Client().accessToken(this.accessToken)
.clientRegistration(this.clientRegistration)
.principalName(token.getPrincipal().getName())
.afterConfigureAdded(serverSpec);
mockAuthentication(token).afterConfigureAdded(serverSpec);
}
@Override
public void afterConfigurerAdded(WebTestClient.Builder builder,
@Nullable WebHttpHandlerBuilder httpHandlerBuilder, @Nullable ClientHttpConnector connector) {
OAuth2AuthenticationToken token = getToken();
mockOAuth2Client().accessToken(this.accessToken)
.clientRegistration(this.clientRegistration)
.principalName(token.getPrincipal().getName())
.afterConfigurerAdded(builder, httpHandlerBuilder, connector);
mockAuthentication(token).afterConfigurerAdded(builder, httpHandlerBuilder, connector);
}
private OAuth2AuthenticationToken getToken() {
OAuth2User oauth2User = this.oauth2User.get();
return new OAuth2AuthenticationToken(oauth2User, oauth2User.getAuthorities(),
this.clientRegistration.getRegistrationId());
}
private ClientRegistration.Builder clientRegistrationBuilder() {
return ClientRegistration.withRegistrationId("test")
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.redirectUri("https://client.example.com")
.clientId("test-client")
.authorizationUri("https://authorize-uri.example.org")
.tokenUri("https://token-uri.example.org");
}
private Collection<GrantedAuthority> defaultAuthorities() {
Set<GrantedAuthority> authorities = new LinkedHashSet<>();
authorities.add(new OAuth2UserAuthority(this.attributes.get(), this.nameAttributeKey));
for (String authority : this.accessToken.getScopes()) {
authorities.add(new SimpleGrantedAuthority("SCOPE_" + authority));
}
return authorities;
}
private Map<String, Object> defaultAttributes() {
Map<String, Object> attributes = new HashMap<>();
attributes.put(this.nameAttributeKey, "user");
return attributes;
}
private OAuth2User defaultPrincipal() {
return new DefaultOAuth2User(this.authorities.get(), this.attributes.get(), this.nameAttributeKey);
}
}
/**
* @author Josh Cummings
* @since 5.3
*/
public static final class OidcLoginMutator implements WebTestClientConfigurer, MockServerConfigurer {
private ClientRegistration clientRegistration;
private OAuth2AccessToken accessToken;
@Nullable
private OidcIdToken idToken;
@SuppressWarnings("NullAway.Init")
private OidcUserInfo userInfo;
private Supplier<OidcUser> oidcUser = this::defaultPrincipal;
@Nullable
private Collection<GrantedAuthority> authorities;
private OidcLoginMutator(OAuth2AccessToken accessToken) {
this.accessToken = accessToken;
this.clientRegistration = clientRegistrationBuilder().build();
}
/**
* Use the provided authorities in the {@link Authentication}
* @param authorities the authorities to use
* @return the {@link OidcLoginMutator} for further configuration
*/
public OidcLoginMutator authorities(Collection<GrantedAuthority> authorities) {
Assert.notNull(authorities, "authorities cannot be null");
this.authorities = authorities;
this.oidcUser = this::defaultPrincipal;
return this;
}
/**
* Use the provided authorities in the {@link Authentication}
* @param authorities the authorities to use
* @return the {@link OidcLoginMutator} for further configuration
*/
public OidcLoginMutator authorities(GrantedAuthority... authorities) {
Assert.notNull(authorities, "authorities cannot be null");
this.authorities = Arrays.asList(authorities);
this.oidcUser = this::defaultPrincipal;
return this;
}
/**
* Use the provided {@link OidcIdToken} when constructing the authenticated user
* @param idTokenBuilderConsumer a {@link Consumer} of a
* {@link OidcIdToken.Builder}
* @return the {@link OidcLoginMutator} for further configuration
*/
public OidcLoginMutator idToken(Consumer<OidcIdToken.Builder> idTokenBuilderConsumer) {
OidcIdToken.Builder builder = OidcIdToken.withTokenValue("id-token");
builder.subject("user");
idTokenBuilderConsumer.accept(builder);
this.idToken = builder.build();
this.oidcUser = this::defaultPrincipal;
return this;
}
/**
* Use the provided {@link OidcUserInfo} when constructing the authenticated user
* @param userInfoBuilderConsumer a {@link Consumer} of a
* {@link OidcUserInfo.Builder}
* @return the {@link OidcLoginMutator} for further configuration
*/
public OidcLoginMutator userInfoToken(Consumer<OidcUserInfo.Builder> userInfoBuilderConsumer) {
OidcUserInfo.Builder builder = OidcUserInfo.builder();
userInfoBuilderConsumer.accept(builder);
this.userInfo = builder.build();
this.oidcUser = this::defaultPrincipal;
return this;
}
/**
* Use the provided {@link OidcUser} as the authenticated user.
* <p>
* Supplying an {@link OidcUser} will take precedence over {@link #idToken},
* {@link #userInfo}, and list of {@link GrantedAuthority}s to use.
* @param oidcUser the {@link OidcUser} to use
* @return the {@link OidcLoginMutator} for further configuration
*/
public OidcLoginMutator oidcUser(OidcUser oidcUser) {
this.oidcUser = () -> oidcUser;
return this;
}
/**
* Use the provided {@link ClientRegistration} as the client to authorize.
* <p>
* The supplied {@link ClientRegistration} will be registered into a
* {@link ServerOAuth2AuthorizedClientRepository}.
* @param clientRegistration the {@link ClientRegistration} to use
* @return the {@link OidcLoginMutator} for further configuration
*/
public OidcLoginMutator clientRegistration(ClientRegistration clientRegistration) {
this.clientRegistration = clientRegistration;
return this;
}
@Override
public void beforeServerCreated(WebHttpHandlerBuilder builder) {
OAuth2AuthenticationToken token = getToken();
mockOAuth2Client().accessToken(this.accessToken)
.principalName(token.getPrincipal().getName())
.clientRegistration(this.clientRegistration)
.beforeServerCreated(builder);
mockAuthentication(token).beforeServerCreated(builder);
}
@Override
public void afterConfigureAdded(WebTestClient.MockServerSpec<?> serverSpec) {
OAuth2AuthenticationToken token = getToken();
mockOAuth2Client().accessToken(this.accessToken)
.principalName(token.getPrincipal().getName())
.clientRegistration(this.clientRegistration)
.afterConfigureAdded(serverSpec);
mockAuthentication(token).afterConfigureAdded(serverSpec);
}
@Override
public void afterConfigurerAdded(WebTestClient.Builder builder,
@Nullable WebHttpHandlerBuilder httpHandlerBuilder, @Nullable ClientHttpConnector connector) {
OAuth2AuthenticationToken token = getToken();
mockOAuth2Client().accessToken(this.accessToken)
.principalName(token.getPrincipal().getName())
.clientRegistration(this.clientRegistration)
.afterConfigurerAdded(builder, httpHandlerBuilder, connector);
mockAuthentication(token).afterConfigurerAdded(builder, httpHandlerBuilder, connector);
}
private ClientRegistration.Builder clientRegistrationBuilder() {
return ClientRegistration.withRegistrationId("test")
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.redirectUri("https://client.example.com")
.clientId("test-client")
.authorizationUri("https://authorize-uri.example.org")
.tokenUri("https://token-uri.example.org");
}
private OAuth2AuthenticationToken getToken() {
OidcUser oidcUser = this.oidcUser.get();
return new OAuth2AuthenticationToken(oidcUser, oidcUser.getAuthorities(),
this.clientRegistration.getRegistrationId());
}
private Collection<GrantedAuthority> getAuthorities() {
if (this.authorities != null) {
return this.authorities;
}
Set<GrantedAuthority> authorities = new LinkedHashSet<>();
authorities.add(new OidcUserAuthority(getOidcIdToken(), getOidcUserInfo()));
for (String authority : this.accessToken.getScopes()) {
authorities.add(new SimpleGrantedAuthority("SCOPE_" + authority));
}
return authorities;
}
@NullUnmarked
private OidcIdToken getOidcIdToken() {
if (this.idToken != null) {
return this.idToken;
}
return new OidcIdToken("id-token", null, null, Collections.singletonMap(IdTokenClaimNames.SUB, "user"));
}
private OidcUserInfo getOidcUserInfo() {
return this.userInfo;
}
private OidcUser defaultPrincipal() {
return new DefaultOidcUser(getAuthorities(), getOidcIdToken(), this.userInfo);
}
}
/**
* @author Josh Cummings
* @since 5.3
*/
@NullUnmarked
public static final class OAuth2ClientMutator implements WebTestClientConfigurer, MockServerConfigurer {
private String registrationId = "test";
@Nullable
private ClientRegistration clientRegistration;
private String principalName = "user";
private OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
"access-token", null, null, Collections.singleton("read"));
private OAuth2ClientMutator() {
}
private OAuth2ClientMutator(String registrationId) {
this.registrationId = registrationId;
clientRegistration((c) -> {
});
}
/**
* Use this {@link ClientRegistration}
* @param clientRegistration
* @return the
* {@link SecurityMockMvcRequestPostProcessors.OAuth2ClientRequestPostProcessor}
* for further configuration
*/
public OAuth2ClientMutator clientRegistration(ClientRegistration clientRegistration) {
this.clientRegistration = clientRegistration;
return this;
}
/**
* Use this {@link Consumer} to configure a {@link ClientRegistration}
* @param clientRegistrationConfigurer the {@link ClientRegistration} configurer
* @return the
* {@link SecurityMockMvcRequestPostProcessors.OAuth2ClientRequestPostProcessor}
* for further configuration
*/
public OAuth2ClientMutator clientRegistration(
Consumer<ClientRegistration.Builder> clientRegistrationConfigurer) {
ClientRegistration.Builder builder = clientRegistrationBuilder();
clientRegistrationConfigurer.accept(builder);
this.clientRegistration = builder.build();
return this;
}
/**
* Use this as the resource owner's principal name
* @param principalName the resource owner's principal name
* @return the {@link OAuth2ClientMutator} for further configuration
*/
public OAuth2ClientMutator principalName(String principalName) {
Assert.notNull(principalName, "principalName cannot be null");
this.principalName = principalName;
return this;
}
/**
* Use this {@link OAuth2AccessToken}
* @param accessToken the {@link OAuth2AccessToken} to use
* @return the
* {@link SecurityMockMvcRequestPostProcessors.OAuth2ClientRequestPostProcessor}
* for further configuration
*/
public OAuth2ClientMutator accessToken(OAuth2AccessToken accessToken) {
this.accessToken = accessToken;
return this;
}
@Override
public void beforeServerCreated(WebHttpHandlerBuilder builder) {
builder.filters(addAuthorizedClientFilter());
}
@Override
public void afterConfigureAdded(WebTestClient.MockServerSpec<?> serverSpec) {
}
@NullUnmarked
@Override
public void afterConfigurerAdded(WebTestClient.Builder builder,
@Nullable WebHttpHandlerBuilder httpHandlerBuilder, @Nullable ClientHttpConnector connector) {
httpHandlerBuilder.filters(addAuthorizedClientFilter());
}
@NullUnmarked
private Consumer<List<WebFilter>> addAuthorizedClientFilter() {
OAuth2AuthorizedClient client = getClient();
return (filters) -> filters.add(0, (exchange, chain) -> {
ServerOAuth2AuthorizedClientRepository authorizedClientRepository = OAuth2ClientServerTestUtils
.getAuthorizedClientRepository(exchange);
if (!(authorizedClientRepository instanceof TestOAuth2AuthorizedClientRepository)) {
authorizedClientRepository = new TestOAuth2AuthorizedClientRepository(authorizedClientRepository);
OAuth2ClientServerTestUtils.setAuthorizedClientRepository(exchange, authorizedClientRepository);
}
TestOAuth2AuthorizedClientRepository.enable(exchange);
return authorizedClientRepository.saveAuthorizedClient(client, null, exchange)
.then(chain.filter(exchange));
});
}
@NullUnmarked
private OAuth2AuthorizedClient getClient() {
Assert.notNull(this.clientRegistration,
"Please specify a ClientRegistration via one of the clientRegistration methods");
return new OAuth2AuthorizedClient(this.clientRegistration, this.principalName, this.accessToken);
}
private ClientRegistration.Builder clientRegistrationBuilder() {
return ClientRegistration.withRegistrationId(this.registrationId)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.redirectUri("https://client.example.com")
.clientId("test-client")
.clientSecret("test-secret")
.authorizationUri("https://idp.example.org/oauth/authorize")
.tokenUri("https://idp.example.org/oauth/token");
}
/**
* Used to wrap the {@link OAuth2AuthorizedClientRepository} to provide support
* for testing when the request is wrapped
*/
private static final class TestOAuth2AuthorizedClientManager implements ReactiveOAuth2AuthorizedClientManager {
static final String ENABLED_ATTR_NAME = TestOAuth2AuthorizedClientManager.class.getName()
.concat(".ENABLED");
private final ReactiveOAuth2AuthorizedClientManager delegate;
@Nullable
private ServerOAuth2AuthorizedClientRepository authorizedClientRepository;
TestOAuth2AuthorizedClientManager(ReactiveOAuth2AuthorizedClientManager delegate) {
this.delegate = delegate;
}
@NullUnmarked
@Override
public Mono<OAuth2AuthorizedClient> authorize(OAuth2AuthorizeRequest authorizeRequest) {
ServerWebExchange exchange = authorizeRequest.getAttribute(ServerWebExchange.class.getName());
if (isEnabled(exchange)) {
return this.authorizedClientRepository.loadAuthorizedClient(
authorizeRequest.getClientRegistrationId(), authorizeRequest.getPrincipal(), exchange);
}
return this.delegate.authorize(authorizeRequest);
}
static void enable(ServerWebExchange exchange) {
exchange.getAttributes().put(ENABLED_ATTR_NAME, Boolean.TRUE);
}
@NullUnmarked
boolean isEnabled(@Nullable ServerWebExchange exchange) {
return Boolean.TRUE.equals(exchange.getAttribute(ENABLED_ATTR_NAME));
}
}
/**
* Used to wrap the {@link OAuth2AuthorizedClientRepository} to provide support
* for testing when the request is wrapped
*/
static final class TestOAuth2AuthorizedClientRepository implements ServerOAuth2AuthorizedClientRepository {
static final String TOKEN_ATTR_NAME = TestOAuth2AuthorizedClientRepository.class.getName().concat(".TOKEN");
static final String ENABLED_ATTR_NAME = TestOAuth2AuthorizedClientRepository.class.getName()
.concat(".ENABLED");
private final ServerOAuth2AuthorizedClientRepository delegate;
@NullUnmarked
TestOAuth2AuthorizedClientRepository(@Nullable ServerOAuth2AuthorizedClientRepository delegate) {
this.delegate = delegate;
}
@Override
public <T extends OAuth2AuthorizedClient> Mono<T> loadAuthorizedClient(String clientRegistrationId,
Authentication principal, ServerWebExchange exchange) {
if (isEnabled(exchange)) {
return Mono.just(exchange.getAttribute(TOKEN_ATTR_NAME));
}
return this.delegate.loadAuthorizedClient(clientRegistrationId, principal, exchange);
}
@Override
public Mono<Void> saveAuthorizedClient(OAuth2AuthorizedClient authorizedClient, Authentication principal,
ServerWebExchange exchange) {
if (isEnabled(exchange)) {
exchange.getAttributes().put(TOKEN_ATTR_NAME, authorizedClient);
return Mono.empty();
}
return this.delegate.saveAuthorizedClient(authorizedClient, principal, exchange);
}
@Override
public Mono<Void> removeAuthorizedClient(String clientRegistrationId, Authentication principal,
ServerWebExchange exchange) {
if (isEnabled(exchange)) {
exchange.getAttributes().remove(TOKEN_ATTR_NAME);
return Mono.empty();
}
return this.delegate.removeAuthorizedClient(clientRegistrationId, principal, exchange);
}
static void enable(ServerWebExchange exchange) {
exchange.getAttributes().put(ENABLED_ATTR_NAME, Boolean.TRUE);
}
boolean isEnabled(ServerWebExchange exchange) {
return Boolean.TRUE.equals(exchange.getAttribute(ENABLED_ATTR_NAME));
}
}
private static final class OAuth2ClientServerTestUtils {
private static final ServerOAuth2AuthorizedClientRepository DEFAULT_CLIENT_REPO = new WebSessionServerOAuth2AuthorizedClientRepository();
private OAuth2ClientServerTestUtils() {
}
/**
* Gets the {@link ServerOAuth2AuthorizedClientRepository} for the specified
* {@link ServerWebExchange}. If one is not found, one based off of
* {@link WebSessionServerOAuth2AuthorizedClientRepository} is used.
* @param exchange the {@link ServerWebExchange} to obtain the
* {@link ReactiveOAuth2AuthorizedClientManager}
* @return the {@link ReactiveOAuth2AuthorizedClientManager} for the specified
* {@link ServerWebExchange}
*/
static @Nullable ServerOAuth2AuthorizedClientRepository getAuthorizedClientRepository(
ServerWebExchange exchange) {
ReactiveOAuth2AuthorizedClientManager manager = getOAuth2AuthorizedClientManager(exchange);
if (manager == null) {
return DEFAULT_CLIENT_REPO;
}
if (manager instanceof DefaultReactiveOAuth2AuthorizedClientManager) {
return (ServerOAuth2AuthorizedClientRepository) ReflectionTestUtils.getField(manager,
"authorizedClientRepository");
}
if (manager instanceof TestOAuth2AuthorizedClientManager) {
return ((TestOAuth2AuthorizedClientManager) manager).authorizedClientRepository;
}
return DEFAULT_CLIENT_REPO;
}
static void setAuthorizedClientRepository(ServerWebExchange exchange,
ServerOAuth2AuthorizedClientRepository repository) {
ReactiveOAuth2AuthorizedClientManager manager = getOAuth2AuthorizedClientManager(exchange);
if (manager == null) {
return;
}
if (manager instanceof DefaultReactiveOAuth2AuthorizedClientManager) {
ReflectionTestUtils.setField(manager, "authorizedClientRepository", repository);
return;
}
if (!(manager instanceof TestOAuth2AuthorizedClientManager)) {
manager = new TestOAuth2AuthorizedClientManager(manager);
setOAuth2AuthorizedClientManager(exchange, manager);
}
TestOAuth2AuthorizedClientManager.enable(exchange);
((TestOAuth2AuthorizedClientManager) manager).authorizedClientRepository = repository;
}
static @Nullable ReactiveOAuth2AuthorizedClientManager getOAuth2AuthorizedClientManager(
ServerWebExchange exchange) {
OAuth2AuthorizedClientArgumentResolver resolver = findResolver(exchange,
OAuth2AuthorizedClientArgumentResolver.class);
if (resolver == null) {
return (authorizeRequest) -> DEFAULT_CLIENT_REPO.loadAuthorizedClient(
authorizeRequest.getClientRegistrationId(), authorizeRequest.getPrincipal(), exchange);
}
return (ReactiveOAuth2AuthorizedClientManager) ReflectionTestUtils.getField(resolver,
"authorizedClientManager");
}
/**
* Sets the {@link ReactiveOAuth2AuthorizedClientManager} for the specified
* {@link ServerWebExchange}.
* @param exchange the {@link ServerWebExchange} to obtain the
* {@link ReactiveOAuth2AuthorizedClientManager}
* @param manager the {@link ReactiveOAuth2AuthorizedClientManager} to set
*/
static void setOAuth2AuthorizedClientManager(ServerWebExchange exchange,
ReactiveOAuth2AuthorizedClientManager manager) {
OAuth2AuthorizedClientArgumentResolver resolver = findResolver(exchange,
OAuth2AuthorizedClientArgumentResolver.class);
if (resolver == null) {
return;
}
ReflectionTestUtils.setField(resolver, "authorizedClientManager", manager);
}
@SuppressWarnings("unchecked")
static <T extends HandlerMethodArgumentResolver> @Nullable T findResolver(ServerWebExchange exchange,
Class<T> resolverClass) {
if (!ClassUtils.isPresent(
"org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerAdapter",
null)) {
return null;
}
return WebFluxClasspathGuard.findResolver(exchange, resolverClass);
}
private static class WebFluxClasspathGuard {
static <T extends HandlerMethodArgumentResolver> @Nullable T findResolver(ServerWebExchange exchange,
Class<T> resolverClass) {
RequestMappingHandlerAdapter handlerAdapter = getRequestMappingHandlerAdapter(exchange);
if (handlerAdapter == null) {
return null;
}
ArgumentResolverConfigurer configurer = handlerAdapter.getArgumentResolverConfigurer();
if (configurer == null) {
return null;
}
List<HandlerMethodArgumentResolver> resolvers = (List<HandlerMethodArgumentResolver>) ReflectionTestUtils
.invokeGetterMethod(configurer, "customResolvers");
if (resolvers == null) {
return null;
}
for (HandlerMethodArgumentResolver resolver : resolvers) {
if (resolverClass.isAssignableFrom(resolver.getClass())) {
return (T) resolver;
}
}
return null;
}
private static @Nullable RequestMappingHandlerAdapter getRequestMappingHandlerAdapter(
ServerWebExchange exchange) {
ApplicationContext context = exchange.getApplicationContext();
if (context != null) {
String[] names = context.getBeanNamesForType(RequestMappingHandlerAdapter.class);
if (names.length > 0) {
return (RequestMappingHandlerAdapter) context.getBean(names[0]);
}
}
return null;
}
}
}
}
}