Simeon Leyzerzon
Simeon Leyzerzon

Reputation: 19074

Spring Boot app requires a bean annotated with @Primary to start

I'm seeing the following message on a Spring Boot app startup:

> *************************** APPLICATION FAILED TO START
> ***************************
> 
> Description:
> 
> Field oauthProps in com.example.authservice.AuthorizationServerConfig
> required a single bean, but 2 were found:
>   - OAuthProperties: defined in file [/Users/simeonleyzerzon/abc/spring-security/spring-security-5-oauth-client/auth-service/target/classes/com/example/authservice/config/OAuthProperties.class]
>   - kai-com.example.authservice.config.OAuthProperties: defined in null
> 
> 
> Action:
> 
> Consider marking one of the beans as @Primary, updating the consumer
> to accept multiple beans, or using @Qualifier to identify the bean
> that should be consumed

I'm wondering what's causing the duplication of that bean and how one can remove it without the necessity of using the @Primary annotation? Not sure where the kai-com package(?) from the above is coming from.

Here's the bean in question:

package com.example.authservice.config;

    //@Primary
    @Component
    @ConfigurationProperties(prefix="kai")
    @Setter @Getter
    public class OAuthProperties {


        private String[] redirectUris;


        private String clientId;


        private String clientSecret;

        private final Token token = new Token();


        @Setter @Getter
        public static class Token{

            private String value;

            private String type="";

        }

    }

and the app/config, etc.:

package com.example.authservice;

import ...
@SpringBootApplication
public class AuthServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(AuthServiceApplication.class, args);
    }
}

@Controller
class MainController {

    @GetMapping("/")
    String index() {
        return "index";
    }
}

@RestController
class ProfileRestController {

    @GetMapping("/resources/userinfo")
    Map<String, String> profile(Principal principal) {
        return Collections.singletonMap("name", principal.getName());
    }
}

@Configuration
@EnableResourceServer
class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
                .antMatcher("/resources/**")
                .authorizeRequests()
                .mvcMatchers("/resources/userinfo").access("#oauth2.hasScope('profile')");
    }
}

@Configuration
@EnableAuthorizationServer
@EnableConfigurationProperties(OAuthProperties.class)
class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {



    @Autowired private OAuthProperties oauthProps;

    private final AuthenticationManager authenticationManager;

    AuthorizationServerConfig(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {

        clients
            .inMemory()

                .withClient(oauthProps.getClientId())
                .secret(oauthProps.getClientSecret())
                .authorizedGrantTypes("authorization_code")
                .scopes("profile")
                .redirectUris(oauthProps.getRedirectUris());


    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {

        endpoints.authenticationManager(this.authenticationManager);

        if (oauthProps.getToken().getType().equals("jwt")) {
            endpoints.tokenStore(this.tokenStore()).accessTokenConverter(jwtAccessTokenConverter());
        }else {
            endpoints.tokenEnhancer(eapiTokenEnhancer());
        }
    }

    TokenEnhancer eapiTokenEnhancer() {

        return new TokenEnhancer() {

            @Override
            public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {

                DefaultOAuth2AccessToken result = new DefaultOAuth2AccessToken(accessToken);
                result.setValue(oauthProps.getToken().getValue());
                return result;
            }
        };

    }

    @Bean
    JwtAccessTokenConverter jwtAccessTokenConverter() {
        KeyStoreKeyFactory factory = new KeyStoreKeyFactory(new ClassPathResource(".keystore-oauth2-demo"), //keystore
                "admin1234".toCharArray());                                                                 //storepass
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        jwtAccessTokenConverter.setKeyPair(factory.getKeyPair("oauth2-demo-key"));                          //alias
        return jwtAccessTokenConverter;
    }

    @Bean
    TokenStore tokenStore() {
        return new JwtTokenStore(this.jwtAccessTokenConverter());
    }
}

@Service
class SimpleUserDetailsService implements UserDetailsService {

    private final Map<String, UserDetails> users = new ConcurrentHashMap<>();

    SimpleUserDetailsService() {
        Arrays.asList("josh", "rob", "joe")
                .forEach(username -> this.users.putIfAbsent(
                        username, new User(username, "pw", true, true, true, true, AuthorityUtils.createAuthorityList("USER","ACTUATOR"))));
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return this.users.get(username);
    }
}

@Configuration
@EnableWebSecurity
class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http
                .authorizeRequests()
                    .anyRequest().authenticated()
                    .and()
                .formLogin();

    }
}

Eclipse too seems to be only aware of a single instance of the bean:

enter image description here

Upvotes: 1

Views: 3349

Answers (2)

M. Deinum
M. Deinum

Reputation: 124581

When using @EnableConfigurationProperties with @ConfigurationProperties you will get a bean named <prefix>-<fqn>, the kai-com.example.authservice.config.OAuthProperties. (See also the reference guide).

When the @ConfigurationProperties bean is registered that way, the bean has a conventional name: <prefix>-<fqn>, where <prefix> is the environment key prefix specified in the @ConfigurationProperties annotation and <fqn> is the fully qualified name of the bean. If the annotation does not provide any prefix, only the fully qualified name of the bean is used. The bean name in the example above is acme-com.example.AcmeProperties. (From the Reference Guide).

The @Component will lead to another registration of the bean with the regular name of the classname with a lowercase character. The other instance of your properties.

the @EnableConfigurationProperties annotation is also automatically applied to your project so that any existing bean annotated with @ConfigurationProperties is configured from the Environment. You could shortcut MyConfiguration by making sure AcmeProperties is already a bean, as shown in the following example: (From the Reference Guide).

The key here is that @EnableConfigurationProperties is already globally applied and processes any bean annotated with @ConfigurationProperties.

So basically you where mixing the 2 ways of using @ConfigurationProperties and Spring Boot 2 now prevents that misuse. This way you write better code (and reduce the memory footprint and performance slightly).

So either remove the @Component or remove the @EnableConfigurationProperties, either way will work.

Upvotes: 5

Simeon Leyzerzon
Simeon Leyzerzon

Reputation: 19074

The following change (removing of @EnableConfigurationProperties) seems to help relieving the need for the @Primary annotation:

@Configuration
@EnableAuthorizationServer
//@EnableConfigurationProperties(OAuthProperties.class)
class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {


    @Autowired private OAuthProperties oauthProps;

Perhaps someone can describe the internal Spring mechanics of secondary bean creation (and its namespace/package assignment) by that annotation which seemingly causes the collision with the @Autowired one, or point me to the appropriate documentation of this behavior.

Upvotes: 0

Related Questions