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;
}
}
這個配置雖然不復雜或不廣泛,但它做了很多事情
-
要求對應用程式中的每個 URL 進行認證
-
為您生成一個登入表單
-
允許 使用者名稱 為
user
且 密碼 為password
的使用者透過表單認證進行認證 -
允許使用者退出登入
-
CSRF 攻擊防護
-
會話固定攻擊防護
-
安全頭整合
-
用於安全請求的 HTTP Strict Transport Security
-
快取控制(您可以在應用程式中稍後覆蓋此設定以允許快取靜態資源)
-
X-Frame-Options 整合以幫助防止 點選劫持
-
-
與以下 Servlet API 方法整合
AbstractSecurityWebApplicationInitializer
下一步是將 springSecurityFilterChain
註冊到 WAR 檔案。在 Servlet 3.0+ 環境中,您可以使用 Spring 的 WebApplicationInitializer
支援 在 Java 配置中完成此操作。不出所料,Spring Security 提供了一個基類(AbstractSecurityWebApplicationInitializer
)來確保為您註冊 springSecurityFilterChain
。我們使用 AbstractSecurityWebApplicationInitializer
的方式取決於我們是否已在使用 Spring,或者 Spring Security 是否是我們應用程式中唯一的 Spring 元件。
-
不使用現有 Spring 的 AbstractSecurityWebApplicationInitializer - 如果您尚未在使用 Spring,請使用這些說明
-
與 Spring MVC 一起使用的 AbstractSecurityWebApplicationInitializer - 如果您已在使用 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。 -
新增一個載入 WebSecurityConfig 的
ContextLoaderListener
。
與 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 | 建立一個包含 @Order 的 SecurityFilterChain 例項,用於指定哪個 SecurityFilterChain 應該首先被考慮。 |
3 | http.securityMatcher() 表示此 HttpSecurity 僅適用於以 /api/ 開頭的 URL。 |
4 | 建立另一個 SecurityFilterChain 例項。如果 URL 不以 /api/ 開頭,則使用此配置。此配置在 apiFilterChain 之後考慮,因為它具有 @Order 值在 1 之後(沒有 @Order 預設排在最後)。 |
選擇 securityMatcher
或 requestMatchers
一個常見問題是
http.securityMatcher()
方法和用於請求授權的requestMatchers()
(即在http.authorizeHttpRequests()
內部)有什麼區別?
為了回答這個問題,瞭解用於構建 SecurityFilterChain
的每個 HttpSecurity
例項都包含一個 RequestMatcher
來匹配傳入請求會有幫助。如果一個請求不匹配具有更高優先順序的 SecurityFilterChain
(例如 @Order(1)
),該請求可以嘗試與具有較低優先順序的過濾器鏈(例如沒有 @Order
)進行匹配。
多個過濾器鏈的匹配邏輯由 |
預設的 RequestMatcher
匹配任何請求,以確保 Spring Security 預設保護所有請求。
指定 |
如果沒有過濾器鏈匹配特定請求,則該請求不受 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 )只需要已認證的使用者即可。 |
推薦提供一個不指定任何 |
請注意,requestMatchers
方法僅適用於單個授權規則。那裡列出的每個請求還必須與用於建立 SecurityFilterChain
的特定 HttpSecurity
例項的整體 securityMatcher
相匹配。本例中使用的 anyRequest()
匹配此特定 SecurityFilterChain
(必須以 /secured/
開頭)內的所有其他請求。
有關 |
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 生成的頁面。您必須為 |
實際示例
以下示例展示了一個更貼近實際的配置,將所有這些元素結合在一起
@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()
}
}
}
這實際上就是 |
然後您可以使用自定義 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
的資源,其內容如下
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();
}