Java 配置

Spring Framework 在 Spring 3.1 中添加了對 Java 配置的普遍支援。Spring Security 3.2 引入了 Java 配置,允許使用者無需使用任何 XML 即可配置 Spring Security。

如果您熟悉 Security Namespace Configuration,您會發現它與 Spring Security Java 配置之間有很多相似之處。

Spring Security 提供了 許多示例應用程式 來演示 Spring Security Java 配置的使用。

你好 Web 安全 Java 配置

第一步是建立我們的 Spring Security Java 配置。該配置會建立一個名為 springSecurityFilterChain 的 Servlet Filter,它負責應用程式中的所有安全事務(保護應用程式 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 提供了一個基類(AbstractSecurityWebApplicationInitializer)來確保為您註冊 springSecurityFilterChain。我們使用 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 Filter。

  • 新增一個載入 WebSecurityConfigContextLoaderListener

與 Spring MVC 一起使用的 AbstractSecurityWebApplicationInitializer

如果我們在應用程式的其他地方使用了 Spring,我們可能已經有一個 WebApplicationInitializer 正在載入我們的 Spring 配置。如果使用前面的配置,將會出錯。相反,我們應該將 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 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(Customizer.withDefaults())
		.httpBasic(Customizer.withDefaults());
	return http.build();
}

預設配置(如上例所示)

  • 確保對應用程式的任何請求都需要使用者進行認證

  • 允許使用者透過基於表單的登入進行認證

  • 允許使用者透過 HTTP Basic 認證進行認證

請注意,此配置與 XML namespace 配置並行

<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 建立一個包含 @OrderSecurityFilterChain 例項,用於指定哪個 SecurityFilterChain 應該首先被考慮。
3 http.securityMatcher() 表示此 HttpSecurity 僅適用於以 /api/ 開頭的 URL。
4 建立另一個 SecurityFilterChain 例項。如果 URL 不以 /api/ 開頭,則使用此配置。此配置在 apiFilterChain 之後考慮,因為它具有 @Order 值在 1 之後(沒有 @Order 預設排在最後)。

選擇 securityMatcherrequestMatchers

一個常見問題是

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)只需要已認證的使用者即可。

推薦提供一個不指定任何 securityMatcherSecurityFilterChain,以確保整個應用程式受到保護,如前面示例所示。

請注意,requestMatchers 方法僅適用於單個授權規則。那裡列出的每個請求還必須與用於建立 SecurityFilterChain 的特定 HttpSecurity 例項的整體 securityMatcher 相匹配。本例中使用的 anyRequest() 匹配此特定 SecurityFilterChain(必須以 /secured/ 開頭)內的所有其他請求。

有關 requestMatchers 的更多資訊,請參閱授權 HttpServletRequests

SecurityFilterChain 端點

SecurityFilterChain 中的幾個過濾器直接提供端點,例如由 http.formLogin() 設定並提供 POST /login 端點的 UsernamePasswordAuthenticationFilter。在上面的示例中,/login 端點不被 http.securityMatcher("/secured/**") 匹配,因此該應用程式將沒有 GET /loginPOST /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/loginGET /secured/logout 提供自己的自定義端點。請注意,Spring Security 仍然會為您提供 POST /secured/loginPOST /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 Basic 認證。
3 接下來,建立另一個帶有 @Order(2)SecurityFilterChain 例項,它將被第二個考慮。此過濾器鏈僅適用於以 /accounts//loans//credit-cards//balances/ 開頭的請求。請注意,由於此過濾器鏈是第二個,任何包含 /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();
}