Add autoconfiguration for JWKTokenStore
If `jwk.key-set-uri` is present. Closes gh-4437
This commit is contained in:
parent
01729cc1d2
commit
b4134e239e
|
@ -35,6 +35,7 @@ import org.springframework.validation.Validator;
|
||||||
* Configuration properties for OAuth2 Resources.
|
* Configuration properties for OAuth2 Resources.
|
||||||
*
|
*
|
||||||
* @author Dave Syer
|
* @author Dave Syer
|
||||||
|
* @author Madhura Bhave
|
||||||
* @since 1.3.0
|
* @since 1.3.0
|
||||||
*/
|
*/
|
||||||
@ConfigurationProperties(prefix = "security.oauth2.resource")
|
@ConfigurationProperties(prefix = "security.oauth2.resource")
|
||||||
|
@ -78,6 +79,8 @@ public class ResourceServerProperties implements Validator, BeanFactoryAware {
|
||||||
|
|
||||||
private Jwt jwt = new Jwt();
|
private Jwt jwt = new Jwt();
|
||||||
|
|
||||||
|
private Jwk jwk = new Jwk();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The order of the filter chain used to authenticate tokens. Default puts it after
|
* The order of the filter chain used to authenticate tokens. Default puts it after
|
||||||
* the actuator endpoints and before the default HTTP basic filter chain (catchall).
|
* the actuator endpoints and before the default HTTP basic filter chain (catchall).
|
||||||
|
@ -158,6 +161,14 @@ public class ResourceServerProperties implements Validator, BeanFactoryAware {
|
||||||
this.jwt = jwt;
|
this.jwt = jwt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Jwk getJwk() {
|
||||||
|
return this.jwk;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setJwk(Jwk jwk) {
|
||||||
|
this.jwk = jwk;
|
||||||
|
}
|
||||||
|
|
||||||
public String getClientId() {
|
public String getClientId() {
|
||||||
return this.clientId;
|
return this.clientId;
|
||||||
}
|
}
|
||||||
|
@ -192,29 +203,40 @@ public class ResourceServerProperties implements Validator, BeanFactoryAware {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ResourceServerProperties resource = (ResourceServerProperties) target;
|
ResourceServerProperties resource = (ResourceServerProperties) target;
|
||||||
if (StringUtils.hasText(this.clientId)) {
|
|
||||||
if (!StringUtils.hasText(this.clientSecret)) {
|
if ((StringUtils.hasText(this.jwt.getKeyUri())
|
||||||
if (!StringUtils.hasText(resource.getUserInfoUri())) {
|
|| StringUtils.hasText(this.jwt.getKeyValue()))
|
||||||
errors.rejectValue("userInfoUri", "missing.userInfoUri",
|
&& StringUtils.hasText(this.jwk.getKeySetUri())) {
|
||||||
"Missing userInfoUri (no client secret available)");
|
errors.reject("ambiguous.keyUri", "Only one of jwt.keyUri (or jwt.keyValue) and jwk.keySetUri should be configured.");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
else {
|
else {
|
||||||
if (isPreferTokenInfo()
|
if (StringUtils.hasText(this.clientId)) {
|
||||||
&& !StringUtils.hasText(resource.getTokenInfoUri())) {
|
if (!StringUtils.hasText(this.clientSecret)) {
|
||||||
if (StringUtils.hasText(getJwt().getKeyUri())
|
|
||||||
|| StringUtils.hasText(getJwt().getKeyValue())) {
|
|
||||||
// It's a JWT decoder
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!StringUtils.hasText(resource.getUserInfoUri())) {
|
if (!StringUtils.hasText(resource.getUserInfoUri())) {
|
||||||
errors.rejectValue("tokenInfoUri", "missing.tokenInfoUri",
|
errors.rejectValue("userInfoUri", "missing.userInfoUri",
|
||||||
"Missing tokenInfoUri and userInfoUri and there is no "
|
"Missing userInfoUri (no client secret available)");
|
||||||
+ "JWT verifier key");
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (isPreferTokenInfo()
|
||||||
|
&& !StringUtils.hasText(resource.getTokenInfoUri())) {
|
||||||
|
if (StringUtils.hasText(getJwt().getKeyUri())
|
||||||
|
|| StringUtils.hasText(getJwt().getKeyValue())
|
||||||
|
|| StringUtils.hasText(getJwk().getKeySetUri())) {
|
||||||
|
// It's a JWT decoder
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!StringUtils.hasText(resource.getUserInfoUri())) {
|
||||||
|
errors.rejectValue("tokenInfoUri", "missing.tokenInfoUri",
|
||||||
|
"Missing tokenInfoUri and userInfoUri and there is no "
|
||||||
|
+ "JWT verifier key");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private int countBeans(Class<?> type) {
|
private int countBeans(Class<?> type) {
|
||||||
|
@ -269,4 +291,22 @@ public class ResourceServerProperties implements Validator, BeanFactoryAware {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class Jwk {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The URI to get verification keys to verify the JWT token.
|
||||||
|
* This can be set when the authorization server returns a
|
||||||
|
* set of verification keys.
|
||||||
|
*/
|
||||||
|
private String keySetUri;
|
||||||
|
|
||||||
|
public String getKeySetUri() {
|
||||||
|
return this.keySetUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setKeySetUri(String keySetUri) {
|
||||||
|
this.keySetUri = keySetUri;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.NoneNestedConditions;
|
||||||
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
|
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
|
||||||
import org.springframework.boot.bind.RelaxedPropertyResolver;
|
import org.springframework.boot.bind.RelaxedPropertyResolver;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
|
@ -61,6 +62,7 @@ import org.springframework.security.oauth2.provider.token.ResourceServerTokenSer
|
||||||
import org.springframework.security.oauth2.provider.token.TokenStore;
|
import org.springframework.security.oauth2.provider.token.TokenStore;
|
||||||
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
|
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
|
||||||
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
|
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
|
||||||
|
import org.springframework.security.oauth2.provider.token.store.jwk.JwkTokenStore;
|
||||||
import org.springframework.social.connect.ConnectionFactoryLocator;
|
import org.springframework.social.connect.ConnectionFactoryLocator;
|
||||||
import org.springframework.social.connect.support.OAuth2ConnectionFactory;
|
import org.springframework.social.connect.support.OAuth2ConnectionFactory;
|
||||||
import org.springframework.util.CollectionUtils;
|
import org.springframework.util.CollectionUtils;
|
||||||
|
@ -73,6 +75,7 @@ import org.springframework.web.client.RestTemplate;
|
||||||
* Configuration for an OAuth2 resource server.
|
* Configuration for an OAuth2 resource server.
|
||||||
*
|
*
|
||||||
* @author Dave Syer
|
* @author Dave Syer
|
||||||
|
* @author Madhura Bhave
|
||||||
* @since 1.3.0
|
* @since 1.3.0
|
||||||
*/
|
*/
|
||||||
@Configuration
|
@Configuration
|
||||||
|
@ -93,7 +96,7 @@ public class ResourceServerTokenServicesConfiguration {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@Conditional(NotJwtTokenCondition.class)
|
@Conditional(RemoteTokenCondition.class)
|
||||||
protected static class RemoteTokenServicesConfiguration {
|
protected static class RemoteTokenServicesConfiguration {
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
|
@ -214,6 +217,30 @@ public class ResourceServerTokenServicesConfiguration {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@Conditional(JwkCondition.class)
|
||||||
|
protected static class JwkTokenStoreConfiguration {
|
||||||
|
|
||||||
|
private final ResourceServerProperties resource;
|
||||||
|
|
||||||
|
public JwkTokenStoreConfiguration(ResourceServerProperties resource) {
|
||||||
|
this.resource = resource;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnMissingBean(ResourceServerTokenServices.class)
|
||||||
|
public DefaultTokenServices jwkTokenServices() {
|
||||||
|
DefaultTokenServices services = new DefaultTokenServices();
|
||||||
|
services.setTokenStore(jwkTokenStore());
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public TokenStore jwkTokenStore() {
|
||||||
|
return new JwkTokenStore(this.resource.getJwk().getKeySetUri());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@Conditional(JwtTokenCondition.class)
|
@Conditional(JwtTokenCondition.class)
|
||||||
protected static class JwtTokenServicesConfiguration {
|
protected static class JwtTokenServicesConfiguration {
|
||||||
|
@ -341,6 +368,26 @@ public class ResourceServerTokenServicesConfiguration {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static class JwkCondition extends SpringBootCondition {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ConditionOutcome getMatchOutcome(ConditionContext context,
|
||||||
|
AnnotatedTypeMetadata metadata) {
|
||||||
|
ConditionMessage.Builder message = ConditionMessage
|
||||||
|
.forCondition("OAuth JWK Condition");
|
||||||
|
RelaxedPropertyResolver resolver = new RelaxedPropertyResolver(
|
||||||
|
context.getEnvironment(), "security.oauth2.resource.jwk.");
|
||||||
|
String keyUri = resolver.getProperty("key-set-uri");
|
||||||
|
if (StringUtils.hasText(keyUri)) {
|
||||||
|
return ConditionOutcome
|
||||||
|
.match(message.foundExactly("provided jwk key set URI"));
|
||||||
|
}
|
||||||
|
return ConditionOutcome
|
||||||
|
.noMatch(message.didNotFind("key jwk set URI not provided").atAll());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
private static class NotTokenInfoCondition extends SpringBootCondition {
|
private static class NotTokenInfoCondition extends SpringBootCondition {
|
||||||
|
|
||||||
private TokenInfoCondition tokenInfoCondition = new TokenInfoCondition();
|
private TokenInfoCondition tokenInfoCondition = new TokenInfoCondition();
|
||||||
|
@ -354,17 +401,21 @@ public class ResourceServerTokenServicesConfiguration {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class NotJwtTokenCondition extends SpringBootCondition {
|
private static class RemoteTokenCondition extends NoneNestedConditions {
|
||||||
|
|
||||||
private JwtTokenCondition jwtTokenCondition = new JwtTokenCondition();
|
RemoteTokenCondition() {
|
||||||
|
super(ConfigurationPhase.PARSE_CONFIGURATION);
|
||||||
@Override
|
|
||||||
public ConditionOutcome getMatchOutcome(ConditionContext context,
|
|
||||||
AnnotatedTypeMetadata metadata) {
|
|
||||||
return ConditionOutcome
|
|
||||||
.inverse(this.jwtTokenCondition.getMatchOutcome(context, metadata));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Conditional(JwtTokenCondition.class)
|
||||||
|
static class HasJwtConfiguration {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Conditional(JwkCondition.class)
|
||||||
|
static class HasJwkConfiguration {
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static class AcceptJsonRequestInterceptor implements ClientHttpRequestInterceptor {
|
static class AcceptJsonRequestInterceptor implements ClientHttpRequestInterceptor {
|
||||||
|
|
|
@ -21,13 +21,21 @@ import java.util.Map;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.ListableBeanFactory;
|
||||||
|
import org.springframework.validation.Errors;
|
||||||
|
import org.springframework.web.context.support.StaticWebApplicationContext;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.verifyZeroInteractions;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for {@link ResourceServerProperties}.
|
* Tests for {@link ResourceServerProperties}.
|
||||||
*
|
*
|
||||||
* @author Dave Syer
|
* @author Dave Syer
|
||||||
* @author Vedran Pavic
|
* @author Vedran Pavic
|
||||||
|
* @author Madhura Bhave
|
||||||
*/
|
*/
|
||||||
public class ResourceServerPropertiesTests {
|
public class ResourceServerPropertiesTests {
|
||||||
|
|
||||||
|
@ -59,4 +67,36 @@ public class ResourceServerPropertiesTests {
|
||||||
.isEqualTo("http://example.com/token_key");
|
.isEqualTo("http://example.com/token_key");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void validateWhenBothJwtAndJwtKeyConfigurationPresentShouldFail() throws Exception {
|
||||||
|
this.properties.getJwk().setKeySetUri("http://my-auth-server/token_keys");
|
||||||
|
this.properties.getJwt().setKeyUri("http://my-auth-server/token_key");
|
||||||
|
setListableBeanFactory();
|
||||||
|
Errors errors = mock(Errors.class);
|
||||||
|
this.properties.validate(this.properties, errors);
|
||||||
|
verify(errors).reject("ambiguous.keyUri", "Only one of jwt.keyUri (or jwt.keyValue) and jwk.keySetUri should be configured.");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void validateWhenKeySetUriProvidedShouldSucceed() throws Exception {
|
||||||
|
this.properties.getJwk().setKeySetUri("http://my-auth-server/token_keys");
|
||||||
|
setListableBeanFactory();
|
||||||
|
Errors errors = mock(Errors.class);
|
||||||
|
this.properties.validate(this.properties, errors);
|
||||||
|
verifyZeroInteractions(errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setListableBeanFactory() {
|
||||||
|
ListableBeanFactory beanFactory = new StaticWebApplicationContext() {
|
||||||
|
@Override
|
||||||
|
public String[] getBeanNamesForType(Class<?> type, boolean includeNonSingletons, boolean allowEagerInit) {
|
||||||
|
if (type.isAssignableFrom(ResourceServerTokenServicesConfiguration.class)) {
|
||||||
|
return new String[]{"ResourceServerTokenServicesConfiguration"};
|
||||||
|
}
|
||||||
|
return new String[0];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.properties.setBeanFactory(beanFactory);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,8 +21,11 @@ import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import org.junit.After;
|
import org.junit.After;
|
||||||
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.ExpectedException;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||||
import org.springframework.beans.factory.config.BeanDefinition;
|
import org.springframework.beans.factory.config.BeanDefinition;
|
||||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||||
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
|
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
|
||||||
|
@ -61,6 +64,7 @@ import static org.mockito.Mockito.mock;
|
||||||
* Tests for {@link ResourceServerTokenServicesConfiguration}.
|
* Tests for {@link ResourceServerTokenServicesConfiguration}.
|
||||||
*
|
*
|
||||||
* @author Dave Syer
|
* @author Dave Syer
|
||||||
|
* @author Madhura Bhave
|
||||||
*/
|
*/
|
||||||
public class ResourceServerTokenServicesConfigurationTests {
|
public class ResourceServerTokenServicesConfigurationTests {
|
||||||
|
|
||||||
|
@ -76,6 +80,9 @@ public class ResourceServerTokenServicesConfigurationTests {
|
||||||
|
|
||||||
private ConfigurableEnvironment environment = new StandardEnvironment();
|
private ConfigurableEnvironment environment = new StandardEnvironment();
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public ExpectedException thrown = ExpectedException.none();
|
||||||
|
|
||||||
@After
|
@After
|
||||||
public void close() {
|
public void close() {
|
||||||
if (this.context != null) {
|
if (this.context != null) {
|
||||||
|
@ -186,6 +193,8 @@ public class ResourceServerTokenServicesConfigurationTests {
|
||||||
.environment(this.environment).web(false).run();
|
.environment(this.environment).web(false).run();
|
||||||
DefaultTokenServices services = this.context.getBean(DefaultTokenServices.class);
|
DefaultTokenServices services = this.context.getBean(DefaultTokenServices.class);
|
||||||
assertThat(services).isNotNull();
|
assertThat(services).isNotNull();
|
||||||
|
this.thrown.expect(NoSuchBeanDefinitionException.class);
|
||||||
|
this.context.getBean(RemoteTokenServices.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -198,6 +207,18 @@ public class ResourceServerTokenServicesConfigurationTests {
|
||||||
assertThat(services).isNotNull();
|
assertThat(services).isNotNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void jwkConfiguration() throws Exception {
|
||||||
|
EnvironmentTestUtils.addEnvironment(this.environment,
|
||||||
|
"security.oauth2.resource.jwk.key-set-uri=http://my-auth-server/token_keys");
|
||||||
|
this.context = new SpringApplicationBuilder(ResourceConfiguration.class)
|
||||||
|
.environment(this.environment).web(false).run();
|
||||||
|
DefaultTokenServices services = this.context.getBean(DefaultTokenServices.class);
|
||||||
|
assertThat(services).isNotNull();
|
||||||
|
this.thrown.expect(NoSuchBeanDefinitionException.class);
|
||||||
|
this.context.getBean(RemoteTokenServices.class);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void springSocialUserInfo() {
|
public void springSocialUserInfo() {
|
||||||
EnvironmentTestUtils.addEnvironment(this.environment,
|
EnvironmentTestUtils.addEnvironment(this.environment,
|
||||||
|
|
|
@ -168,7 +168,7 @@
|
||||||
<spring-retry.version>1.2.0.RELEASE</spring-retry.version>
|
<spring-retry.version>1.2.0.RELEASE</spring-retry.version>
|
||||||
<spring-security.version>4.2.1.RELEASE</spring-security.version>
|
<spring-security.version>4.2.1.RELEASE</spring-security.version>
|
||||||
<spring-security-jwt.version>1.0.7.RELEASE</spring-security-jwt.version>
|
<spring-security-jwt.version>1.0.7.RELEASE</spring-security-jwt.version>
|
||||||
<spring-security-oauth.version>2.0.12.RELEASE</spring-security-oauth.version>
|
<spring-security-oauth.version>2.0.13.BUILD-SNAPSHOT</spring-security-oauth.version>
|
||||||
<spring-session.version>1.3.0.RELEASE</spring-session.version>
|
<spring-session.version>1.3.0.RELEASE</spring-session.version>
|
||||||
<spring-social.version>1.1.4.RELEASE</spring-social.version>
|
<spring-social.version>1.1.4.RELEASE</spring-social.version>
|
||||||
<spring-social-facebook.version>2.0.3.RELEASE</spring-social-facebook.version>
|
<spring-social-facebook.version>2.0.3.RELEASE</spring-social-facebook.version>
|
||||||
|
|
|
@ -21,29 +21,34 @@ import org.springframework.boot.diagnostics.FailureAnalysis;
|
||||||
import org.springframework.util.CollectionUtils;
|
import org.springframework.util.CollectionUtils;
|
||||||
import org.springframework.validation.BindException;
|
import org.springframework.validation.BindException;
|
||||||
import org.springframework.validation.FieldError;
|
import org.springframework.validation.FieldError;
|
||||||
|
import org.springframework.validation.ObjectError;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An {@link AbstractFailureAnalyzer} that performs analysis of failures caused by a
|
* An {@link AbstractFailureAnalyzer} that performs analysis of failures caused by a
|
||||||
* {@link BindException}.
|
* {@link BindException}.
|
||||||
*
|
*
|
||||||
* @author Andy Wilkinson
|
* @author Andy Wilkinson
|
||||||
|
* @author Madhura Bhave
|
||||||
*/
|
*/
|
||||||
class BindFailureAnalyzer extends AbstractFailureAnalyzer<BindException> {
|
class BindFailureAnalyzer extends AbstractFailureAnalyzer<BindException> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected FailureAnalysis analyze(Throwable rootFailure, BindException cause) {
|
protected FailureAnalysis analyze(Throwable rootFailure, BindException cause) {
|
||||||
if (CollectionUtils.isEmpty(cause.getFieldErrors())) {
|
if (CollectionUtils.isEmpty(cause.getAllErrors())) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
StringBuilder description = new StringBuilder(
|
StringBuilder description = new StringBuilder(
|
||||||
String.format("Binding to target %s failed:%n", cause.getTarget()));
|
String.format("Binding to target %s failed:%n", cause.getTarget()));
|
||||||
for (FieldError fieldError : cause.getFieldErrors()) {
|
for (ObjectError error : cause.getAllErrors()) {
|
||||||
description.append(String.format("%n Property: %s",
|
if (error instanceof FieldError) {
|
||||||
cause.getObjectName() + "." + fieldError.getField()));
|
FieldError fieldError = (FieldError) error;
|
||||||
|
description.append(String.format("%n Property: %s",
|
||||||
|
cause.getObjectName() + "." + fieldError.getField()));
|
||||||
|
description.append(
|
||||||
|
String.format("%n Value: %s", fieldError.getRejectedValue()));
|
||||||
|
}
|
||||||
description.append(
|
description.append(
|
||||||
String.format("%n Value: %s", fieldError.getRejectedValue()));
|
String.format("%n Reason: %s%n", error.getDefaultMessage()));
|
||||||
description.append(
|
|
||||||
String.format("%n Reason: %s%n", fieldError.getDefaultMessage()));
|
|
||||||
}
|
}
|
||||||
return new FailureAnalysis(description.toString(),
|
return new FailureAnalysis(description.toString(),
|
||||||
"Update your application's configuration", cause);
|
"Update your application's configuration", cause);
|
||||||
|
|
|
@ -32,6 +32,8 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
|
||||||
import org.springframework.boot.diagnostics.FailureAnalysis;
|
import org.springframework.boot.diagnostics.FailureAnalysis;
|
||||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||||
import org.springframework.context.i18n.LocaleContextHolder;
|
import org.springframework.context.i18n.LocaleContextHolder;
|
||||||
|
import org.springframework.validation.Errors;
|
||||||
|
import org.springframework.validation.Validator;
|
||||||
import org.springframework.validation.annotation.Validated;
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
@ -40,6 +42,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||||
* Tests for {@link BindFailureAnalyzer}.
|
* Tests for {@link BindFailureAnalyzer}.
|
||||||
*
|
*
|
||||||
* @author Andy Wilkinson
|
* @author Andy Wilkinson
|
||||||
|
* @author Madhura Bhave
|
||||||
*/
|
*/
|
||||||
public class BindFailureAnalyzerTests {
|
public class BindFailureAnalyzerTests {
|
||||||
|
|
||||||
|
@ -54,8 +57,8 @@ public class BindFailureAnalyzerTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void bindExceptionDueToValidationFailure() {
|
public void bindExceptionWithFieldErrorsDueToValidationFailure() {
|
||||||
FailureAnalysis analysis = performAnalysis(ValidationFailureConfiguration.class);
|
FailureAnalysis analysis = performAnalysis(FieldValidationFailureConfiguration.class);
|
||||||
assertThat(analysis.getDescription())
|
assertThat(analysis.getDescription())
|
||||||
.contains(failure("test.foo.foo", "null", "may not be null"));
|
.contains(failure("test.foo.foo", "null", "may not be null"));
|
||||||
assertThat(analysis.getDescription())
|
assertThat(analysis.getDescription())
|
||||||
|
@ -64,6 +67,13 @@ public class BindFailureAnalyzerTests {
|
||||||
.contains(failure("test.foo.nested.bar", "null", "may not be null"));
|
.contains(failure("test.foo.nested.bar", "null", "may not be null"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void bindExceptionWithObjectErrorsDueToValidationFailure() throws Exception {
|
||||||
|
FailureAnalysis analysis = performAnalysis(ObjectValidationFailureConfiguration.class);
|
||||||
|
assertThat(analysis.getDescription())
|
||||||
|
.contains("Reason: This object could not be bound.");
|
||||||
|
}
|
||||||
|
|
||||||
private static String failure(String property, String value, String reason) {
|
private static String failure(String property, String value, String reason) {
|
||||||
return String.format("Property: %s%n Value: %s%n Reason: %s", property,
|
return String.format("Property: %s%n Value: %s%n Reason: %s", property,
|
||||||
value, reason);
|
value, reason);
|
||||||
|
@ -85,14 +95,19 @@ public class BindFailureAnalyzerTests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@EnableConfigurationProperties(ValidationFailureProperties.class)
|
@EnableConfigurationProperties(FieldValidationFailureProperties.class)
|
||||||
static class ValidationFailureConfiguration {
|
static class FieldValidationFailureConfiguration {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@EnableConfigurationProperties(ObjectErrorFailureProperties.class)
|
||||||
|
static class ObjectValidationFailureConfiguration {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ConfigurationProperties("test.foo")
|
@ConfigurationProperties("test.foo")
|
||||||
@Validated
|
@Validated
|
||||||
static class ValidationFailureProperties {
|
static class FieldValidationFailureProperties {
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
private String foo;
|
private String foo;
|
||||||
|
@ -144,4 +159,18 @@ public class BindFailureAnalyzerTests {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ConfigurationProperties("foo.bar")
|
||||||
|
static class ObjectErrorFailureProperties implements Validator {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void validate(Object target, Errors errors) {
|
||||||
|
errors.reject("my.objectError", "This object could not be bound.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supports(Class<?> clazz) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue