Java 構成

Java 構成の一般的なサポートが Spring 3.1 の Spring Framework に追加されました。Spring Security 3.2 は、ユーザーが XML を使用せずに Spring Security を構成できるようにする Java 構成を導入しました。

セキュリティ名前空間の構成に精通している場合は、セキュリティ名前空間の構成と Spring Security Java 構成の間にかなりの類似点があるはずです。

Spring Security は、Spring Security Java 構成の使用法を示すための多くのサンプルアプリケーションを提供 [GitHub] (英語) します。

Hello Web セキュリティ Java 構成

最初のステップは、Spring Security Java 構成を作成することです。この構成により、springSecurityFilterChain と呼ばれるサーブレットフィルターが作成されます。これは、アプリケーション内のすべてのセキュリティ(アプリケーションの URL の保護、送信されたユーザー名とパスワードの検証、ログインフォームへのリダイレクトなど)を担当します。次の例は、Spring Security Java 構成の最も基本的な例を示しています。

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.context.annotation.*;
import org.springframework.security.config.annotation.authentication.builders.*;
import org.springframework.security.config.annotation.web.configuration.*;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig {

	@Bean
	public UserDetailsService userDetailsService() {
		InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
		manager.createUser(User.withDefaultPasswordEncoder().username("user").password("password").roles("USER").build());
		return manager;
	}
}

この構成は複雑でも大規模でもありませんが、多くのことを行います。

AbstractSecurityWebApplicationInitializer

次のステップは、springSecurityFilterChain を WAR ファイルに登録することです。これは、Servlet 3.0+ 環境の Spring の WebApplicationInitializer サポートを使用した Java 構成で行うことができます。当然のことながら、Spring Security は、springSecurityFilterChain が確実に登録されるようにするための基本クラス(AbstractSecurityWebApplicationInitializer)を提供します。AbstractSecurityWebApplicationInitializer の使用方法は、すでに Spring を使用しているかどうか、Spring Security がアプリケーションで唯一の Spring コンポーネントであるかどうかによって異なります。

Spring が存在しない AbstractSecurityWebApplicationInitializer

Spring または Spring MVC を使用していない場合は、WebSecurityConfig をスーパークラスに渡して、構成が確実に取得されるようにする必要があります。

import org.springframework.security.web.context.*;

public class SecurityWebApplicationInitializer
	extends AbstractSecurityWebApplicationInitializer {

	public SecurityWebApplicationInitializer() {
		super(WebSecurityConfig.class);
	}
}

SecurityWebApplicationInitializer:

  • アプリケーション内のすべての URL に対して springSecurityFilterChain フィルターを自動的に登録します。

  • WebSecurityConfig をロードする ContextLoaderListener を追加します。

Spring MVC を使用した AbstractSecurityWebApplicationInitializer

アプリケーションの他の場所で Spring を使用している場合は、Spring 構成をロードしている WebApplicationInitializer がすでに存在している可能性があります。以前の構成を使用すると、エラーが発生します。代わりに、Spring Security を既存の ApplicationContext に登録する必要があります。例: Spring MVC を使用する場合、SecurityWebApplicationInitializer は次のようになります。

import org.springframework.security.web.context.*;

public class SecurityWebApplicationInitializer
	extends AbstractSecurityWebApplicationInitializer {

}

これにより、アプリケーション内のすべての URL に対して springSecurityFilterChain が登録されるだけです。その後、WebSecurityConfig が既存の ApplicationInitializer にロードされていることを確認する必要があります。例: Spring MVC を使用する場合、getServletConfigClasses() に追加されます。

public class MvcWebApplicationInitializer extends
		AbstractAnnotationConfigDispatcherServletInitializer {

	@Override
	protected Class<?>[] getServletConfigClasses() {
		return new Class[] { WebSecurityConfig.class, WebMvcConfig.class };
	}

	// ... other overrides ...
}

その理由は、基盤となるリクエストマッチャーを適切に構成するために、Spring Security が一部の Spring MVC 構成をインスペクションできる必要があるため、それらは同じアプリケーションコンテキスト内にある必要があるためです。Spring Security を getRootConfigClasses に配置すると、Spring MVC の HandlerMappingIntrospector を見つけられない可能性がある親アプリケーションコンテキストに配置されます。

複数の Spring MVC ディスパッチャーの構成

必要に応じて、Spring MVC に関係のない Spring Security 構成を次のように別の構成クラスに配置できます。

public class MvcWebApplicationInitializer extends
		AbstractAnnotationConfigDispatcherServletInitializer {

	@Override
    protected Class<?>[] getRootConfigClasses() {
		return new Class[] { NonWebSecurityConfig.class };
    }

	@Override
	protected Class<?>[] getServletConfigClasses() {
		return new Class[] { WebSecurityConfig.class, WebMvcConfig.class };
	}

	// ... other overrides ...
}

これは、AbstractAnnotationConfigDispatcherServletInitializer の複数のインスタンスがあり、それらの両方で一般的なセキュリティ構成を複製したくない場合に役立ちます。

HttpSecurity

これまでのところ、WebSecurityConfig には、ユーザーを認証する方法に関する情報のみが含まれています。Spring Security は、すべてのユーザーに認証を要求することをどのように認識しますか? Spring Security は、フォームベース認証をサポートすることをどのように認識していますか? 実際には、バックグラウンドで呼び出されている構成クラス(SecurityFilterChain と呼ばれる)があります。これは、次のデフォルトの実装で構成されています。

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
	http
		.authorizeHttpRequests(authorize -> authorize
			.anyRequest().authenticated()
		)
		.formLogin(Customizer.withDefaults())
		.httpBasic(Customizer.withDefaults());
	return http.build();
}

デフォルトの構成(前の例に示されています):

  • アプリケーションへのリクエストでは、ユーザーの認証が必要であることを保証します

  • ユーザーがフォームベースのログインで認証できるようにします

  • ユーザーが HTTP 基本認証で認証できるようにします

この構成は XML 名前空間の構成と類似していることに注意してください。

<http>
	<intercept-url pattern="/**" access="authenticated"/>
	<form-login />
	<http-basic />
</http>

複数の HttpSecurity インスタンス

特定の領域に異なる保護が必要なアプリケーションのセキュリティを効果的に管理するには、securityMatcher DSL メソッドと並行して複数のフィルターチェーンを使用します。このアプローチにより、アプリケーションの特定の部分に合わせて個別のセキュリティ構成を定義し、アプリケーション全体のセキュリティと制御を強化できます。

XML で複数の <http> ブロックを持つことができるのと同じように、複数の HttpSecurity インスタンスを構成できます。重要なのは、複数の SecurityFilterChain または @Bean を登録することです。次の例では、/api/ で始まる URL に対して異なる構成が使用されています。

@Configuration
@EnableWebSecurity
public class MultiHttpSecurityConfig {
	@Bean                                                             (1)
	public UserDetailsService userDetailsService() throws Exception {
		UserBuilder users = User.withDefaultPasswordEncoder();
		InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
		manager.createUser(users.username("user").password("password").roles("USER").build());
		manager.createUser(users.username("admin").password("password").roles("USER","ADMIN").build());
		return manager;
	}

	@Bean
	@Order(1)                                                        (2)
	public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
		http
			.securityMatcher("/api/**")                              (3)
			.authorizeHttpRequests(authorize -> authorize
				.anyRequest().hasRole("ADMIN")
			)
			.httpBasic(Customizer.withDefaults());
		return http.build();
	}

	@Bean                                                            (4)
	public SecurityFilterChain formLoginFilterChain(HttpSecurity http) throws Exception {
		http
			.authorizeHttpRequests(authorize -> authorize
				.anyRequest().authenticated()
			)
			.formLogin(Customizer.withDefaults());
		return http.build();
	}
}
1 通常どおり認証を構成します。
2@Order を含む SecurityFilterChain のインスタンスを作成して、どの SecurityFilterChain を最初に考慮するかを指定します。
3http.securityMatcher() では、この HttpSecurity は /api/ で始まる URL にのみ適用されることが規定されています。
4SecurityFilterChain の別のインスタンスを作成します。URL が /api/ で始まらない場合は、この構成が使用されます。この構成は、1 の後に @Order 値があるため、apiFilterChain の後に考慮されます (@Order はデフォルトで最後にはなりません)。

securityMatcher または requestMatchers の選択

よくある質問は次のとおりです。

リクエストの認可に使用される http.securityMatcher() メソッドと requestMatchers() (つまり、http.authorizeHttpRequests() 内部) の違いは何ですか ?

この質問に答えるには、SecurityFilterChain の構築に使用される各 HttpSecurity インスタンスに、受信リクエストに一致する RequestMatcher が含まれていることを理解すると役立ちます。リクエストが優先度の高い SecurityFilterChain (例: @Order(1)) と一致しない場合、リクエストは優先度の低いフィルターチェーン (例: @Order なし) に対して試行されます。

複数のフィルターチェーンのマッチングロジックは、FilterChainProxy によって実行されます。

デフォルトの RequestMatcher はすべてのリクエストに一致し、Spring Security がデフォルトですべてのリクエストを保護するようにします。

securityMatcher を指定すると、このデフォルトが上書きされます。

特定のリクエストに一致するフィルターチェーンがない場合、そのリクエストは Spring Security によって保護されません

次の例は、/secured/ で始まるリクエストのみを保護する単一のフィルターチェーンを示しています。

@Configuration
@EnableWebSecurity
public class PartialSecurityConfig {

	@Bean
	public UserDetailsService userDetailsService() throws Exception {
		// ...
	}

	@Bean
	public SecurityFilterChain securedFilterChain(HttpSecurity http) throws Exception {
		http
			.securityMatcher("/secured/**")                            (1)
			.authorizeHttpRequests(authorize -> authorize
				.requestMatchers("/secured/user").hasRole("USER")      (2)
				.requestMatchers("/secured/admin").hasRole("ADMIN")    (3)
				.anyRequest().authenticated()                          (4)
			)
			.httpBasic(Customizer.withDefaults())
			.formLogin(Customizer.withDefaults());
		return http.build();
	}
}
1/secured/ で始まるリクエストは保護されますが、その他のリクエストは保護されません。
2/secured/user へのリクエストには ROLE_USER 権限が必要です。
3/secured/admin へのリクエストには ROLE_ADMIN 権限が必要です。
4 その他のリクエスト (/secured/other など) では、認証されたユーザーのみが必要です。

前の例で示したように、アプリケーション全体が保護されるように、securityMatcher を指定しない SecurityFilterChain を提供することをお勧めします

requestMatchers メソッドは個々の認可ルールにのみ適用されることに注意してください。そこにリストされている各リクエストは、SecurityFilterChain の作成に使用されたこの特定の HttpSecurity インスタンスの全体的な securityMatcher とも一致する必要があります。この例で anyRequest() を使用すると、この特定の SecurityFilterChain ( /secured/ で始まる必要があります) 内の他のすべてのリクエストと一致します。

requestMatchers の詳細については、HttpServletRequests を認証するを参照してください。

SecurityFilterChain エンドポイント

SecurityFilterChain のいくつかのフィルターは、http.formLogin() によってセットアップされ、POST /login エンドポイントを提供する UsernamePasswordAuthenticationFilter など、エンドポイントを直接提供します。上記の例では、/login エンドポイントは http.securityMatcher("/secured/**") と一致しないため、そのアプリケーションには GET /login または POST /login エンドポイントがありません。このようなリクエストは 404 Not Found を返します。これはユーザーにとってしばしば驚きです。

http.securityMatcher() を指定すると、その SecurityFilterChain に一致するリクエストに影響します。ただし、フィルターチェーンによって提供されるエンドポイントには自動的に影響しません。このような場合は、フィルターチェーンが提供するエンドポイントの URL をカスタマイズする必要がある場合があります。

次の例は、/secured/ で始まるリクエストを保護し、他のすべてのリクエストを拒否するとともに、SecurityFilterChain によって提供されるエンドポイントをカスタマイズする構成を示しています。

@Configuration
@EnableWebSecurity
public class SecuredSecurityConfig {

	@Bean
	public UserDetailsService userDetailsService() throws Exception {
		// ...
	}

	@Bean
	@Order(1)
	public SecurityFilterChain securedFilterChain(HttpSecurity http) throws Exception {
		http
			.securityMatcher("/secured/**")                            (1)
			.authorizeHttpRequests(authorize -> authorize
				.anyRequest().authenticated()                          (2)
			)
			.formLogin(formLogin -> formLogin                          (3)
				.loginPage("/secured/login")
				.loginProcessingUrl("/secured/login")
				.permitAll()
			)
			.logout(logout -> logout                                   (4)
				.logoutUrl("/secured/logout")
				.logoutSuccessUrl("/secured/login?logout")
				.permitAll()
			)
			.formLogin(Customizer.withDefaults());
		return http.build();
	}

	@Bean
	public SecurityFilterChain defaultFilterChain(HttpSecurity http) throws Exception {
		http
			.authorizeHttpRequests(authorize -> authorize
				.anyRequest().denyAll()                                (5)
			);
		return http.build();
	}
}
1/secured/ で始まるリクエストは、このフィルターチェーンによって保護されます。
2/secured/ で始まるリクエストには認証されたユーザーが必要です。
3 フォームログインをカスタマイズして、URL の先頭に /secured/ を付けます。
4 ログアウトをカスタマイズして、URL のプレフィックスに /secured/ を付けます。
5 その他のリクエストはすべて拒否されます。

この例では、ログインページとログアウトページをカスタマイズし、Spring Security の生成されたページを無効にします。GET /secured/login と GET /secured/logout には独自のカスタムエンドポイントを提供する必要があります。Spring Security は引き続き POST /secured/login と POST /secured/logout エンドポイントを提供することに注意してください。

実世界の例

次の例は、これらすべての要素を組み合わせた、もう少し現実的な構成を示しています。

@Configuration
@EnableWebSecurity
public class BankingSecurityConfig {

    @Bean                                                              (1)
    public UserDetailsService userDetailsService() {
		UserBuilder users = User.withDefaultPasswordEncoder();
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(users.username("user1").password("password").roles("USER", "VIEW_BALANCE").build());
        manager.createUser(users.username("user2").password("password").roles("USER").build());
        manager.createUser(users.username("admin").password("password").roles("ADMIN").build());
        return manager;
    }

    @Bean
    @Order(1)                                                          (2)
    public SecurityFilterChain approvalsSecurityFilterChain(HttpSecurity http) throws Exception {
        String[] approvalsPaths = { "/accounts/approvals/**", "/loans/approvals/**", "/credit-cards/approvals/**" };
        http
            .securityMatcher(approvalsPaths)
            .authorizeHttpRequests(authorize -> authorize
				.anyRequest().hasRole("ADMIN")
            )
            .httpBasic(Customizer.withDefaults());
        return http.build();
    }

    @Bean
    @Order(2)                                                          (3)
    public SecurityFilterChain bankingSecurityFilterChain(HttpSecurity http) throws Exception {
        String[] bankingPaths = { "/accounts/**", "/loans/**", "/credit-cards/**", "/balances/**" };
		String[] viewBalancePaths = { "/balances/**" };
        http
			.securityMatcher(bankingPaths)
			.authorizeHttpRequests(authorize -> authorize
				.requestMatchers(viewBalancePaths).hasRole("VIEW_BALANCE")
				.anyRequest().hasRole("USER")
            );
        return http.build();
    }

    @Bean                                                              (4)
    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
		String[] allowedPaths = { "/", "/user-login", "/user-logout", "/notices", "/contact", "/register" };
        http
            .authorizeHttpRequests(authorize -> authorize
				.requestMatchers(allowedPaths).permitAll()
				.anyRequest().authenticated()
            )
			.formLogin(formLogin -> formLogin
				.loginPage("/user-login")
				.loginProcessingUrl("/user-login")
			)
			.logout(logout -> logout
				.logoutUrl("/user-logout")
				.logoutSuccessUrl("/?logout")
			);
        return http.build();
    }
}
1 まず認証設定を構成します。
2@Order(1) を使用して SecurityFilterChain インスタンスを定義します。つまり、このフィルターチェーンの優先度が最も高くなります。このフィルターチェーンは、/accounts/approvals//loans/approvals/、または /credit-cards/approvals/ で始まるリクエストにのみ適用されます。このフィルターチェーンへのリクエストには ROLE_ADMIN 権限が必要であり、HTTP 基本認証が許可されます。
3 次に、2 番目とみなされる @Order(2) を使用して、別の SecurityFilterChain インスタンスを作成します。このフィルターチェーンは、/accounts//loans//credit-cards/ または /balances/ で始まるリクエストにのみ適用されます。このフィルターチェーンは 2 番目であるため、/approvals/ を含むすべてのリクエストは前のフィルターチェーンと一致し、このフィルターチェーンとは一致しないことに注意してください。このフィルターチェーンへのリクエストには、ROLE_USER 権限が必要です。このフィルターチェーンは、次の (デフォルトの) フィルターチェーンにその構成が含まれているため、認証を定義しません。
4 最後に、@Order アノテーションなしで追加の SecurityFilterChain インスタンスを作成します。この構成は、他のフィルターチェーンでカバーされていないリクエストを処理し、最後に処理されます (@Order がない場合は最後にデフォルトで処理されます)。//user-login/user-logout/notices/contact/register に一致するリクエストは、認証なしでアクセスできます。その他のリクエストでは、他のフィルターチェーンによって明示的に許可または保護されていない URL にアクセスするには、ユーザーの認証が必要です。

カスタム DSL

Spring Security で独自のカスタム DSL を提供できます。

  • Java

  • Kotlin

public class MyCustomDsl extends AbstractHttpConfigurer<MyCustomDsl, HttpSecurity> {
	private boolean flag;

	@Override
	public void init(HttpSecurity http) throws Exception {
		// any method that adds another configurer
		// must be done in the init method
		http.csrf().disable();
	}

	@Override
	public void configure(HttpSecurity http) throws Exception {
		ApplicationContext context = http.getSharedObject(ApplicationContext.class);

		// here we lookup from the ApplicationContext. You can also just create a new instance.
		MyFilter myFilter = context.getBean(MyFilter.class);
		myFilter.setFlag(flag);
		http.addFilterBefore(myFilter, UsernamePasswordAuthenticationFilter.class);
	}

	public MyCustomDsl flag(boolean value) {
		this.flag = value;
		return this;
	}

	public static MyCustomDsl customDsl() {
		return new MyCustomDsl();
	}
}
class MyCustomDsl : AbstractHttpConfigurer<MyCustomDsl, HttpSecurity>() {
    var flag: Boolean = false

    override fun init(http: HttpSecurity) {
        // any method that adds another configurer
        // must be done in the init method
        http.csrf().disable()
    }

    override fun configure(http: HttpSecurity) {
        val context: ApplicationContext = http.getSharedObject(ApplicationContext::class.java)

        // here we lookup from the ApplicationContext. You can also just create a new instance.
        val myFilter: MyFilter = context.getBean(MyFilter::class.java)
        myFilter.setFlag(flag)
        http.addFilterBefore(myFilter, UsernamePasswordAuthenticationFilter::class.java)
    }

    companion object {
        @JvmStatic
        fun customDsl(): MyCustomDsl {
            return MyCustomDsl()
        }
    }
}

これは、実際に HttpSecurity.authorizeHttpRequests() などのメソッドが実装される方法です。

その後、カスタム DSL を使用できます。

  • Java

  • Kotlin

@Configuration
@EnableWebSecurity
public class Config {
	@Bean
	public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
		http
			.with(MyCustomDsl.customDsl(), (dsl) -> dsl
				.flag(true)
			)
			// ...
		return http.build();
	}
}
@Configuration
@EnableWebSecurity
class Config {

    @Bean
    fun filterChain(http: HttpSecurity): SecurityFilterChain {
        http
            .with(MyCustomDsl.customDsl()) {
                flag = true
            }
            // ...

        return http.build()
    }
}

コードは次の順序で呼び出されます。

  • Config.filterChain メソッドのコードが呼び出されます

  • MyCustomDsl.init メソッドのコードが呼び出されます

  • MyCustomDsl.configure メソッドのコードが呼び出されます

必要に応じて、SpringFactories を使用して、デフォルトで HttpSecurity に MyCustomDsl を追加させることができます。例: META-INF/spring.factories という名前のクラスパスに、次の内容のリソースを作成できます。

META-INF/spring.factories
org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer = sample.MyCustomDsl

デフォルトを明示的に無効にすることもできます。

  • Java

  • Kotlin

@Configuration
@EnableWebSecurity
public class Config {
	@Bean
	public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
		http
			.with(MyCustomDsl.customDsl(), (dsl) -> dsl
				.disable()
			)
			...;
		return http.build();
	}
}
@Configuration
@EnableWebSecurity
class Config {

    @Bean
    fun filterChain(http: HttpSecurity): SecurityFilterChain {
        http
            .with(MyCustomDsl.customDsl()) {
                disable()
            }
            // ...
        return http.build()
    }

}

構成済みオブジェクトの後処理

Spring Security の Java 構成は、構成するすべてのオブジェクトのすべてのプロパティを公開するわけではありません。これにより、大多数のユーザーの構成が簡素化されます。結局のところ、すべてのプロパティが公開されている場合、ユーザーは標準の Bean 構成を使用できます。

すべてのプロパティを直接公開しないのには十分な理由がありますが、ユーザーはさらに高度な構成オプションを必要とする場合があります。この課題に対処するために、Spring Security は ObjectPostProcessor の概念を導入します。これは、Java 構成によって作成された Object インスタンスの多くを変更または置換するために使用できます。例: FilterSecurityInterceptor で filterSecurityPublishAuthorizationSuccess プロパティを構成するには、次を使用できます。

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
	http
		.authorizeHttpRequests(authorize -> authorize
			.anyRequest().authenticated()
			.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
				public <O extends FilterSecurityInterceptor> O postProcess(
						O fsi) {
					fsi.setPublishAuthorizationSuccess(true);
					return fsi;
				}
			})
		);
	return http.build();
}