web-dev-qa-db-ja.com

セキュリティOAuth2を使用したSpring Boot-Webログインフォームでリソースサーバーを使用する方法

私はSpring Boot(1.2.1.RELEASE)アプリケーションを提供していますOAuth2(2.0.6.RELEASE)1つのアプリケーションインスタンス内の承認とリソースサーバー。 MongoDBでユーザーを検索するためにUserDetailsServiceを利用するカスタムMongoTemplate実装を使用します。 _grant_type=password_に対する_/oauth/token_を使用した認証はチャームのように機能し、特定のリソースを呼び出すときに_Authorization: Bearer {token}_ヘッダーを使用した認証も機能します。

簡単なOAuth確認ダイアログをサーバーに追加したいので、保護されたリソースのapi-docsでSwagger UI呼び出しなどを認証および承認できます。これまでに行ったことは次のとおりです。

_@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)
        }
    }
}
_

主な問題は、両方(ヘッダーのWebログインフォームとOAuth2認証トークン)を実行できないことです。 ResourceServerの方が優先度が高い場合、OAuth2トークン認証は機能しますが、Webフォームを使用してログインできません。一方、LoginConfigクラスに高い優先度を設定すると、OAuth2トークン認証が機能しなくなります。

ケーススタディ:ログインフォームは機能するが、OAuth2トークン認証が機能しない

その場合、問題は未登録の_OAuth2AuthenticationProcessingFilter_が原因であることがわかりました。 ResourceServer.configure(HttpSecurity http)メソッドで手動で登録しようとしましたが、機能しませんでした。FilterChainリストにフィルターが表示されましたが、トリガーされませんでした。 ResourceServerの初期化中には他にも多くの魔法が実行されたため、2番目のケースに移動しました。

ケーススタディ:ログインフォームが機能せず、OAuth2トークン認証が機能する

その場合の主な問題は、デフォルトでUsernamePasswordAuthenticationFilterが適切に構成されたAuthenticationProviderインスタンス(ProviderManager内)を見つけられないことです。私が手動で追加しようとしたとき:

_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つのアプリケーションにリソースサーバーが統合された承認サーバーを設定するにはどうすればよいですか?多分それは別のはるかに簡単な方法で行うことができます。私はどんな助けにも感謝します。

13
Szymon Stepniak

これが問題の解決策です。この例示的な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認証をWebフォーム認証と並行して実行するには、

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

構成クラスに。以前の構成ではこの重要な部分を見逃していたため、OAuth2.0のみが認証プロセスに参加しました。

11
Szymon Stepniak

ResourceServerConfigurerAdapterにフォームログインまたはhttp basicを設定する必要があるとは思いません。他のWebSecurityConfigurerAdapterにすでに設定している場合は、そうではありません(これらはオンになっているため、そうします)デフォルトで)。動作する可能性がありますが、OAuth2で保護されたリソースとUIでは認証とアクセスの決定が非常に異なるため、(githubのすべてのサンプルにあるように)別々にしておくことをお勧めします。推奨事項に沿って、定義済みのコンポーネントを続行する場合、これを正しく行うための鍵は、フィルターチェーンが順番に試行され、最初に一致したものが成功するため、そのうちの1つだけが動作することを知ることです。与えられた要求。リクエストマッチャーを両方のチェーン(または少なくとも最低の順序のチェーン)に配置し、それらが重複しないようにする必要があります。

6
Dave Syer

異なるセキュリティで構成された異なるエンドポイントを使用するとどうなりますか?

上記の例では、/ uaa/**を含むすべてのものはWebSecurityConfigurerAdapterで保護されており、/ api-docs/**はResourceServerConfigurerAdapterで保護されています。

その場合、フィルターチェーンはまだ競合しますか?

2
turgos