Kotlin 配置

Spring Security 的 Kotlin 配置自 Spring Security 5.3 起可用。它允許使用者使用原生 Kotlin DSL 配置 Spring Security。

Spring Security 提供一個示例應用,以演示 Spring Security Kotlin 配置的使用。

HttpSecurity

Spring Security 如何知道我們需要所有使用者進行認證?Spring Security 如何知道我們想要支援基於表單的認證?後臺正在呼叫一個配置類(稱為 SecurityFilterChain)。它使用以下預設實現進行配置

import org.springframework.security.config.annotation.web.invoke

@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
    http {
        authorizeHttpRequests {
            authorize(anyRequest, authenticated)
        }
        formLogin { }
        httpBasic { }
    }
    return http.build()
}
確保匯入 org.springframework.security.config.annotation.web.invoke 函式以在類中啟用 Kotlin DSL,因為 IDE 不會總是自動匯入此方法,這會導致編譯問題。

預設配置(如前例所示)

  • 確保對我們應用的任何請求都要求使用者進行認證

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

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

請注意,此配置與 XML 名稱空間配置相似

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

多個 HttpSecurity 例項

為了在應用中有效管理某些區域需要不同保護的安全,我們可以採用多個過濾器鏈以及 securityMatcher DSL 方法。這種方法允許我們為應用的特定部分定義不同的安全配置,從而增強應用的整體安全性和控制。

我們可以配置多個 HttpSecurity 例項,就像在 XML 中可以有多個 <http> 塊一樣。關鍵是註冊多個 SecurityFilterChain @Bean。以下示例對以 /api/ 開頭的 URL 進行了不同的配置

import org.springframework.security.config.annotation.web.invoke

@Configuration
@EnableWebSecurity
class MultiHttpSecurityConfig {
    @Bean                                                            (1)
    open fun userDetailsService(): UserDetailsService {
        val users = User.withDefaultPasswordEncoder()
        val manager = 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)
    open fun apiFilterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            securityMatcher("/api/**")                               (3)
            authorizeHttpRequests {
                authorize(anyRequest, hasRole("ADMIN"))
            }
            httpBasic { }
        }
        return http.build()
    }

    @Bean                                                            (4)
    open fun formLoginFilterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            authorizeHttpRequests {
                authorize(anyRequest, authenticated)
            }
            formLogin { }
        }
        return http.build()
    }
}
1 像往常一樣配置認證。
2 建立一個包含 @OrderSecurityFilterChain 例項,用於指定應首先考慮哪個 SecurityFilterChain
3 http.securityMatcher() 表示此 HttpSecurity 僅適用於以 /api/ 開頭的 URL。
4 建立另一個 SecurityFilterChain 例項。如果 URL 不以 /api/ 開頭,則使用此配置。此配置在 apiFilterChain 之後被考慮,因為它有一個大於 1 的 @Order 值(沒有 @Order 預設為最後)。

選擇 securityMatcherrequestMatchers

一個常見問題是

http.securityMatcher() 方法和用於請求授權的 requestMatchers()(即 http.authorizeHttpRequests() 內部)有什麼區別?

為了回答這個問題,有必要了解用於構建 SecurityFilterChain 的每個 HttpSecurity 例項都包含一個用於匹配傳入請求的 RequestMatcher。如果請求與優先順序較高的 SecurityFilterChain(例如 @Order(1))不匹配,則可以嘗試優先順序較低的過濾器鏈(例如沒有 @Order)。

多個過濾器鏈的匹配邏輯由FilterChainProxy執行。

預設的 RequestMatcher 匹配任何請求,以確保 Spring Security 預設保護所有請求。

指定 securityMatcher 會覆蓋此預設行為。

如果沒有過濾器鏈匹配特定的請求,則該請求不會受到 Spring Security 的保護。

以下示例演示了一個僅保護以 /secured/ 開頭的請求的單個過濾器鏈

import org.springframework.security.config.annotation.web.invoke

@Configuration
@EnableWebSecurity
class PartialSecurityConfig {
	@Bean
	open fun userDetailsService(): UserDetailsService {
		// ...
	}

	@Bean
	open fun securedFilterChain(http: HttpSecurity): SecurityFilterChain {
		http {
			securityMatcher("/secured/**")                             (1)
			authorizeHttpRequests {
				authorize("/secured/user", hasRole("USER"))            (2)
				authorize("/secured/admin", hasRole("ADMIN"))          (3)
				authorize(anyRequest, authenticated)                   (4)
			}
			httpBasic { }
			formLogin { }
		}
		return http.build()
	}
}
1 /secured/ 開頭的請求將受到保護,而其他任何請求則不受保護。
2 /secured/user 的請求需要 ROLE_USER 許可權。
3 /secured/admin 的請求需要 ROLE_ADMIN 許可權。
4 其他任何請求(例如 /secured/other)僅需要認證使用者。

建議提供一個不指定任何 securityMatcherSecurityFilterChain,以確保整個應用都受到保護,如 earlier example 所示。

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

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

SecurityFilterChain 端點

SecurityFilterChain 中的幾個過濾器直接提供端點,例如由 http.formLogin() 設定並提供 POST /login 端點的 UsernamePasswordAuthenticationFilter。在 above example 中,/login 端點與 http.securityMatcher("/secured/**") 不匹配,因此該應用不會有任何 GET /loginPOST /login 端點。此類請求將返回 404 Not Found。這通常會讓使用者感到驚訝。

指定 http.securityMatcher() 會影響該 SecurityFilterChain 匹配哪些請求。但是,它不會自動影響過濾器鏈提供的端點。在這種情況下,您可能需要自定義您希望過濾器鏈提供的任何端點的 URL。

以下示例演示了一種配置,該配置保護以 /secured/ 開頭的請求並拒絕所有其他請求,同時還自定義了 SecurityFilterChain 提供的端點

import org.springframework.security.config.annotation.web.invoke

@Configuration
@EnableWebSecurity
class SecuredSecurityConfig {
	@Bean
	open fun userDetailsService(): UserDetailsService {
		// ...
	}

	@Bean
	@Order(1)
	open fun securedFilterChain(http: HttpSecurity): SecurityFilterChain {
		http {
			securityMatcher("/secured/**")                             (1)
			authorizeHttpRequests {
				authorize(anyRequest, authenticated)                   (2)
			}
			formLogin {                                                (3)
                loginPage = "/secured/login"
                loginProcessingUrl = "/secured/login"
                permitAll = true
			}
			logout {                                                   (4)
                logoutUrl = "/secured/logout"
                logoutSuccessUrl = "/secured/login?logout"
                permitAll = true
			}
		}
		return http.build()
	}

	@Bean
    open fun defaultFilterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            authorizeHttpRequests {
                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 端點。

實際示例

以下示例展示了一個稍微更貼近實際的配置,將所有這些元素組合在一起

import org.springframework.security.config.annotation.web.invoke

@Configuration
@EnableWebSecurity
class BankingSecurityConfig {
    @Bean                                                              (1)
    open fun userDetailsService(): UserDetailsService {
        val users = User.withDefaultPasswordEncoder()
        val manager = 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)
    open fun approvalsSecurityFilterChain(http: HttpSecurity): SecurityFilterChain {
        val approvalsPaths = arrayOf("/accounts/approvals/**", "/loans/approvals/**", "/credit-cards/approvals/**")
        http {
            securityMatcher(approvalsPaths)
            authorizeHttpRequests {
				authorize(anyRequest, hasRole("ADMIN"))
            }
            httpBasic { }
        }
        return http.build()
    }

    @Bean
    @Order(2)                                                          (3)
	open fun bankingSecurityFilterChain(http: HttpSecurity): SecurityFilterChain {
        val bankingPaths = arrayOf("/accounts/**", "/loans/**", "/credit-cards/**", "/balances/**")
		val viewBalancePaths = arrayOf("/balances/**")
        http {
            securityMatcher(bankingPaths)
            authorizeHttpRequests {
                authorize(viewBalancePaths, hasRole("VIEW_BALANCE"))
				authorize(anyRequest, hasRole("USER"))
            }
        }
        return http.build()
    }

    @Bean                                                              (4)
	open fun defaultSecurityFilterChain(http: HttpSecurity): SecurityFilterChain {
        val allowedPaths = arrayOf("/", "/user-login", "/user-logout", "/notices", "/contact", "/register")
        http {
            authorizeHttpRequests {
                authorize(allowedPaths, permitAll)
				authorize(anyRequest, authenticated)
            }
			formLogin {
                loginPage = "/user-login"
                loginProcessingUrl = "/user-login"
			}
			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。