Java 組態

Spring Framework 在 Spring 3.1 版本中加入了對 Java 組態 的一般支援。Spring Security 3.2 引入了 Java 組態,讓使用者無需使用任何 XML 即可設定 Spring Security。

如果您熟悉安全命名空間組態,您應該會發現它與 Spring Security Java 組態之間有許多相似之處。

Spring Security 提供了許多範例應用程式來示範 Spring Security Java 組態的使用方式。

Hello Web Security Java 組態

第一步是建立我們的 Spring Security Java 組態。此組態建立一個 Servlet 篩選器,稱為 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

下一步是在 WAR 檔案中註冊 springSecurityFilterChain。您可以使用 Spring 的 WebApplicationInitializer 支援在 Servlet 3.0+ 環境中以 Java 組態來完成。不出所料,Spring Security 提供了一個基底類別 (AbstractSecurityWebApplicationInitializer) 以確保 springSecurityFilterChain 為您註冊。我們使用 AbstractSecurityWebApplicationInitializer 的方式取決於我們是否已在使用 Spring,或者 Spring Security 是否是應用程式中唯一的 Spring 元件。

AbstractSecurityWebApplicationInitializer,不含現有 Spring

如果您未使用 Spring 或 Spring MVC,則需要將 WebSecurityConfig 傳遞給父類別,以確保組態被提取。

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

public class SecurityWebApplicationInitializer
	extends AbstractSecurityWebApplicationInitializer {

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

SecurityWebApplicationInitializer

  • 自動為應用程式中的每個 URL 註冊 springSecurityFilterChain 篩選器。

  • 新增一個 ContextLoaderListener,它會載入 WebSecurityConfig

AbstractSecurityWebApplicationInitializer,搭配 Spring MVC

如果我們在應用程式的其他地方使用 Spring,我們可能已經有一個正在載入 Spring 組態的 WebApplicationInitializer。如果我們使用先前的組態,則會收到錯誤。相反地,我們應該向現有的 ApplicationContext 註冊 Spring Security。例如,如果我們使用 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 Dispatcher 設定

如果需要,任何與 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(withDefaults())
		.httpBasic(withDefaults());
	return http.build();
}

預設組態(如前面的範例所示)

  • 確保對我們應用程式的任何請求都要求使用者經過身份驗證

  • 允許使用者使用表單式登入進行身份驗證

  • 允許使用者使用 HTTP 基本身份驗證進行身份驗證

請注意,此組態與 XML 命名空間組態平行

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

多個 HttpSecurity 實例

我們可以設定多個 HttpSecurity 實例,就像我們可以在 XML 中有多個 <http> 區塊一樣。關鍵是註冊多個 SecurityFilterChain @Bean。以下範例針對以 /api/ 開頭的 URL 具有不同的組態。

@Configuration
@EnableWebSecurity
public class MultiHttpSecurityConfig {
	@Bean                                                             (1)
	public UserDetailsService userDetailsService() throws Exception {
		// ensure the passwords are encoded properly
		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(withDefaults());
		return http.build();
	}

	@Bean                                                            (4)
	public SecurityFilterChain formLoginFilterChain(HttpSecurity http) throws Exception {
		http
			.authorizeHttpRequests(authorize -> authorize
				.anyRequest().authenticated()
			)
			.formLogin(withDefaults());
		return http.build();
	}
}
1 照常設定身份驗證。
2 建立 SecurityFilterChain 的實例,其中包含 @Order 以指定應首先考慮哪個 SecurityFilterChain
3 http.securityMatcher 聲明此 HttpSecurity 僅適用於以 /api/ 開頭的 URL。
4 建立另一個 SecurityFilterChain 的實例。如果 URL 不是以 /api/ 開頭,則使用此組態。由於此組態的 @Order 值在 1 之後(沒有 @Order 預設為最後一個),因此在 apiFilterChain 之後考慮此組態。

自訂 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 方法中的程式碼

如果您願意,可以使用 SpringFactoriesHttpSecurity 預設新增 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();
}