Spring Загрузка с безопасностью OAuth2 - как использовать сервер ресурсов с формой входа в систему?

У меня есть приложение Spring Boot (1.2.1.RELEASE), которое обслуживает авторизацию и сервер ресурсов OAuth2 (2.0.6.RELEASE) в одном экземпляре приложения. Он использует пользовательскую реализацию UserDetailsService, которая использует MongoTemplate для поиска пользователей в MongoDB. Аутентификация с помощью grant_type=password на /oauth/token работает как шарм, а также авторизация с заголовком Authorization: Bearer {token} при вызове определенных ресурсов.

Теперь я хочу добавить на сервер простое диалоговое окно подтверждения OAuth, чтобы я мог аутентифицироваться и авторизироваться, например. Swagger UI вызывает api-docs для защищенных ресурсов. Вот что я сделал до сих пор:

@Configuration
@SessionAttributes("authorizationRequest")
class OAuth2ServerConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/login").setViewName("login");
        registry.addViewController("/oauth/confirm_access").setViewName("authorize");
    }

    @Configuration
    @Order(2)
    protected static class LoginConfig extends WebSecurityConfigurerAdapter implements ApplicationEventPublisherAware {

        @Autowired
        UserDetailsService userDetailsService

        @Autowired
        PasswordEncoder passwordEncoder

        ApplicationEventPublisher applicationEventPublisher


        @Bean
        DaoAuthenticationProvider daoAuthenticationProvider() {
            DaoAuthenticationProvider provider = new DaoAuthenticationProvider()
            provider.passwordEncoder = passwordEncoder
            provider.userDetailsService = userDetailsService
            return provider
        }

        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.parentAuthenticationManager(authenticationManagerBean())
                    .userDetailsService(userDetailsService)
                    .passwordEncoder(passwordEncoder())
        }

        @Bean
        @Override
        public AuthenticationManager authenticationManagerBean() throws Exception {
            //return super.authenticationManagerBean()
            ProviderManager providerManager = new ProviderManager([daoAuthenticationProvider()], super.authenticationManagerBean())
            providerManager.setAuthenticationEventPublisher(new DefaultAuthenticationEventPublisher(applicationEventPublisher))
            return providerManager
        }

        @Bean
        public PasswordEncoder passwordEncoder() {
            new BCryptPasswordEncoder(5)
        }
    }


    @Configuration
    @EnableResourceServer
    protected static class ResourceServer extends ResourceServerConfigurerAdapter {

        @Value('${oauth.resourceId}')
        private String resourceId

        @Autowired
        @Qualifier('authenticationManagerBean')
        private AuthenticationManager authenticationManager

        @Override
        public void configure(HttpSecurity http) throws Exception {
            http.setSharedObject(AuthenticationManager.class, authenticationManager)

            http.csrf().disable()
            http.httpBasic().disable()

            http.formLogin().loginPage("/login").permitAll()

            //http.authenticationProvider(daoAuthenticationProvider())

            http.anonymous().and()
                    .authorizeRequests()
                    .antMatchers('/login/**').permitAll()
                    .antMatchers('/uaa/register/**').permitAll()
                    .antMatchers('/uaa/activate/**').permitAll()
                    .antMatchers('/uaa/password/**').permitAll()
                    .antMatchers('/uaa/account/**').hasAuthority('ADMIN')
                    .antMatchers('/api-docs/**').permitAll()
                    .antMatchers('/admin/**').hasAuthority('SUPERADMIN')
                    .anyRequest().authenticated()

            //http.sessionManagement().sessionCreationPolicy(STATELESS)
        }

        @Override
        public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
            resources.resourceId(resourceId)
            resources.authenticationManager(authenticationManager)
        }
    }

    @Configuration
    @EnableAuthorizationServer
    protected static class OAuth2Config extends AuthorizationServerConfigurerAdapter {

        @Value('${oauth.clientId}')
        private String clientId

        @Value('${oauth.secret:}')
        private String secret

        @Value('${oauth.resourceId}')
        private String resourceId

        @Autowired
        @Qualifier('authenticationManagerBean')
        private AuthenticationManager authenticationManager

        @Bean
        public JwtAccessTokenConverter accessTokenConverter() {
            return new JwtAccessTokenConverter();
        }

        @Override
        public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
            oauthServer.checkTokenAccess("permitAll()")
            oauthServer.allowFormAuthenticationForClients()
        }

        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
            endpoints.authenticationManager(authenticationManager)
                    .accessTokenConverter(accessTokenConverter())
        }

        @Override
        public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
            clients.inMemory()
                    .withClient(clientId)
                    .secret(secret)
                    .authorizedGrantTypes("password", "authorization_code", "refresh_token", "implicit")
                    .authorities("USER", "ADMIN")
                    .scopes("read", "write", "trust")
                    .resourceIds(resourceId)
        }
    }
}

Основная проблема заключается в том, что я не могу запустить оба (форма входа в систему и токен авторизации OAuth2 в заголовке). Если ResourceServer получает более высокий приоритет, то авторизация авторизации OAuth2 работает, но я не могу войти в систему, используя веб-форму. С другой стороны, если я задаю более высокий приоритет классу LoginConfig, то авторизация токена OAuth2 перестает работать.

Пример из практики: форма входа в систему, авторизация авторизации OAuth2 не

Я выяснил, что в этом случае проблема вызвана незарегистрированным OAuth2AuthenticationProcessingFilter. Я попытался зарегистрировать его вручную в методе ResourceServer.configure(HttpSecurity http), но он не сработал - я мог видеть фильтр в списке FilterChain, но он не запускался. Это не был хороший способ исправить это, потому что во время инициализации ResourceServer появилось много другой магии, поэтому я перешел во второй случай.

Тематическое исследование: форма входа в систему не работает, авторизация авторизации OAuth2

В этом случае основная проблема заключается в том, что по умолчанию UsernamePasswordAuthenticationFilter не может найти правильно сконфигурированный экземпляр AuthenticationProviderProviderManager). Когда я попытался добавить его вручную, выполните следующие действия:

http.authenticationProvide(daoAuthenticationProvider())

он получает один, но в этом случае нет AuthenticationEventPublisher. Определенная и успешная аутентификация не может быть опубликована другим компонентам. А на следующей итерации она заменяется на AnonymousAuthenticationToken. Поэтому я попытался определить вручную AuthenticationManager экземпляр с DaoAuthenticationProvider внутри:

@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
    //return super.authenticationManagerBean()
    ProviderManager providerManager = new ProviderManager([daoAuthenticationProvider()], super.authenticationManagerBean())
    providerManager.setAuthenticationEventPublisher(new DefaultAuthenticationEventPublisher(applicationEventPublisher))
    return providerManager
}

Я думал, что это сработает, но есть другая проблема с предоставлением экземпляра AuthenticationManager для зарегистрированных фильтров. Оказывается, каждый фильтр имеет AuthenticationManager, введенный вручную, используя компонент sharedObjects:

authFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));

Проблема заключается в том, что у вас нет гарантированного правильного набора экземпляров, потому что есть простой HashMap (проверить его на GitHub), используемый для хранения определенных общих объект, и он может быть изменен в любое время. Я попытался установить его в:

http.setSharedObject(AuthenticationManager.class, authenticationManager)

но прежде чем я доберусь до места, где он читается, он уже заменен по умолчанию. Я проверил его с помощью отладчика, и похоже, что для каждого нового фильтра есть новый экземпляр диспетчера проверки подлинности.

Мой вопрос: Я делаю это правильно? Как настроить сервер авторизации с сервером ресурсов, встроенным в одно приложение, с формой входа (диалог OAuth2)? Может быть, это можно сделать другим и намного проще. Я был бы благодарен за любую помощь.

Ответ 1

Вот решение проблемы. Взгляните на этот примерный класс Groovy:

@Configuration
@EnableResourceServer
class ResourceServer extends ResourceServerConfigurerAdapter {

    @Value('${oauth.resourceId}')
    private String resourceId

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
        http.httpBasic().disable()

        http.requestMatchers().antMatchers('/admin/**', '/uaa/**')
                .and().authorizeRequests()
                    .antMatchers('/uaa/authenticated/**').authenticated()
                    .antMatchers('/uaa/register/**').permitAll()
                    .antMatchers('/uaa/activate/**').permitAll()
                    .antMatchers('/uaa/password/**').permitAll()
                    .antMatchers('/uaa/auth/**').permitAll()
                    .antMatchers('/uaa/account/**').hasAuthority('ADMIN')
                    .antMatchers('/admin/**').hasAuthority('ADMIN')
                    .anyRequest().authenticated()

        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
    }

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.resourceId(resourceId);
    }
}

В принципе, для проверки подлинности OAuth2.0 параллельно с аутентификацией веб-формы вам нужно поставить

http.requestMatchers().antMatchers('/path/1/**', '/path/2/**')

в класс конфигурации. Моя предыдущая конфигурация пропустила эту важную часть, поэтому в процессе аутентификации участвовал только OAuth2.0.

Ответ 2

Я не думаю, что вы должны пытаться настроить регистрационную форму или http basic в своем ResourceServerConfigurerAdapter, и, конечно, нет, если у вас уже есть их в другой WebSecurityConfigurerAdapter (вы делаете, потому что они по умолчанию включены), Это может сработать, но решения для аутентификации и доступа настолько различны для защищенного ресурса OAuth2 и пользовательского интерфейса, который я рекомендую вам держать отдельно (как и во всех образцах в github). Если вы пойдете с рекомендацией и продолжите работу с компонентами, которые вы уже определили, ключом к правильному праву является знать, что цепочки фильтров последовательно используются, а первый соответствует победам, поэтому только один из них будет действовать на любом данный запрос. Вы должны поместить запросы в обе цепи (или, по крайней мере, одну с наименьшим порядком), и убедитесь, что они не перекрываются.

Ответ 3

что, если вы используете разные конечные точки, настроенные с другой безопасностью?

В приведенном выше примере все с /uaa/ ** защищено с помощью WebSecurityConfigurerAdapter и /api -docs/** с ResourceServerConfigurerAdapter.

В этом случае будут ли конфликты фильтровать цепи?