Kotlin 配置
Spring Security Kotlin 配置自 Spring Security 5.3 版本起已可用。它允许用户通过使用原生的 Kotlin DSL 来配置 Spring Security。
Spring Security 提供了一个示例应用程序来演示 Spring Security Kotlin 配置的使用。
HttpSecurity
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基本身份验证进行身份验证
请注意,此配置与 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()
}
}
按常规方式配置身份验证。
创建一个包含
@Order注解的SecurityFilterChain实例,以指定应优先考虑哪个SecurityFilterChain。http.securityMatcher()声明此HttpSecurity仅适用于以/api/开头的 URL。创建另一个
SecurityFilterChain实例。如果 URL 不以/api/开头,则使用此配置。由于它的@Order值在1之后(没有@Order注解则默认为最后),此配置将在apiFilterChain之后被考虑。
选择 securityMatcher 或 requestMatchers
一个常见的问题是:
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()
}
}
以
/secured/开头的请求将受到保护,但其他任何请求不受保护。对
/secured/user的请求需要ROLE_USER权限。对
/secured/admin的请求需要ROLE_ADMIN权限。任何其他请求(例如
/secured/other)仅需要经过身份验证的用户。
建议提供一个不指定任何 securityMatcher 的 SecurityFilterChain,以确保整个应用程序都受到保护,如前面的示例所示。
请注意,requestMatchers 方法仅适用于单个授权规则。其中列出的每个请求还必须匹配用于创建此 SecurityFilterChain 的特定 HttpSecurity 实例的总体 securityMatcher。在此示例中,使用 anyRequest() 将匹配此特定 SecurityFilterChain 内的所有其他请求(这些请求必须以 /secured/ 开头)。
有关 requestMatchers 的更多信息,请参阅授权 HttpServletRequests。
SecurityFilterChain 端点
SecurityFilterChain 中的多个过滤器直接提供端点,例如由 http.formLogin() 设置的 UsernamePasswordAuthenticationFilter 提供了 POST /login 端点。在上述示例中,/login 端点未被 http.securityMatcher("/secured/**") 匹配,因此该应用程序将没有任何 GET /login 或 POST /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()
}
}
以
/secured/开头的请求将受此过滤器链保护。以
/secured/开头的请求需要经过身份验证的用户。自定义表单登录,将 URL 前缀设置为
/secured/。自定义注销,将 URL 前缀设置为
/secured/。所有其他请求将被拒绝。
此示例自定义了登录和注销页面,这会禁用 Spring Security 自动生成的页面。你必须为 GET /secured/login 和 GET /secured/logout 提供自己的自定义端点。请注意,Spring Security 仍会为你提供 POST /secured/login 和 POST /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()
}
}
首先配置认证设置。
定义一个带有
@Order(1)的SecurityFilterChain实例,这意味着此过滤器链将拥有最高优先级。此过滤器链仅适用于以/accounts/approvals/、/loans/approvals/或/credit-cards/approvals/开头的请求。对此过滤器链的请求需要ROLE_ADMIN权限,并允许 HTTP 基本认证。接下来,创建另一个带有
@Order(2)的SecurityFilterChain实例,它将作为第二优先级被考虑。此过滤器链仅适用于以/accounts/、/loans/、/credit-cards/或/balances/开头的请求。请注意,由于此过滤器链是第二个,任何包含/approvals/的请求都将匹配前一个过滤器链,而不会被此过滤器链匹配。对此过滤器链的请求需要ROLE_USER权限。此过滤器链未定义任何认证方式,因为下一个(默认)过滤器链包含了该配置。最后,创建一个没有
@Order注解的额外SecurityFilterChain实例。此配置将处理其他过滤器链未覆盖的请求,并将最后被处理(没有@Order注解默认排在最后)。匹配/、/user-login、/user-logout、/notices、/contact和/register的请求允许未经认证即可访问。任何其他请求都要求用户经过认证才能访问未被其他过滤器链明确允许或保护的任何 URL。
模块化 HttpSecurityDsl 配置
许多用户倾向于将Spring Security配置集中管理,并选择在单个SecurityFilterChain实例中进行配置。然而,有时用户可能希望将配置模块化。这可以通过以下方式实现:
由于 Spring Security Kotlin DSL (HttpSecurityDsl) 使用 HttpSecurity,所有 Java 的模块化 Bean 定制都会在模块化 HttpSecurity 配置之前应用。
HttpSecurityDsl.() → Unit Bean
若您希望将安全配置模块化,可将逻辑置于 HttpSecurityDsl.() → Unit 类型的 Bean 中。例如,以下配置将确保所有 HttpSecurityDsl 实例均被配置为:
@Bean
fun httpSecurityDslBean(): HttpSecurityDsl.() -> Unit {
return {
headers {
contentSecurityPolicy {
1
policyDirectives = "object-src 'none'"
}
}
2
redirectToHttps { }
}
}
将 内容安全策略 设置为
object-src 'none'
顶层安全DSL Bean
如果您希望进一步模块化您的安全配置,Spring Security 将自动应用任何顶层的 Security Dsl Bean。
顶级安全领域特定语言(Security Dsl)可概括为任何符合 public HttpSecurityDsl.*(<Dsl>) 模式的类 Dsl 类。这意味着任何作为 HttpSecurityDsl 公共方法单参数的安全领域特定语言均属于此类。
几个例子可以帮助澄清这一点。如果 ContentTypeOptionsDsl.() → Unit 被发布为一个 Bean,它不会自动应用,因为它是 HeadersDsl#contentTypeOptions(ContentTypeOptionsDsl.() → Unit) 的参数,而不是 HttpSecurityDsl 上定义的方法的参数。然而,如果 HeadersDsl.() → Unit 被发布为一个 Bean,它将会自动应用,因为它是 HttpSecurityDsl.headers(HeadersDsl.() → Unit) 的参数。
例如,以下配置确保所有 HttpSecurityDsl 实例均被配置为:
@Bean
fun headersSecurity(): HeadersDsl.() -> Unit {
return {
contentSecurityPolicy {
1
policyDirectives = "object-src 'none'"
}
}
}
将内容安全策略设置为
object-src 'none'
DSL Bean 排序
首先,应用所有模块化 HttpSecurity 配置,因为 Kotlin DSL 使用了 HttpSecurity Bean。
其次,每个 HttpSecurityDsl.() → Unit Bean 都通过 ObjectProvider#orderedStream() 进行应用。这意味着,如果存在多个 HttpSecurity.() → Unit Bean,可以在 Bean 定义上添加 @Order 注解来控制它们的执行顺序。
接下来,系统会查找每一个顶级安全DSL Bean类型,并使用ObjectProvider#orderedStream()方法依次应用它们。如果存在不同类型的顶级安全Bean(例如HeadersDsl.() → Unit和HttpsRedirectDsl.() → Unit),那么每种DSL类型的调用顺序是未定义的。然而,同一顶级安全Bean类型的每个实例的顺序由ObjectProvider#orderedStream()定义,并且可以通过在Bean定义上使用@Order注解来控制。
最后,HttpSecurityDsl Bean 被注入为一个 Bean。所有 *Dsl.() → Unit Bean 会在 HttpSecurityDsl Bean 创建之前应用。这允许覆盖由 *Dsl.() → Unit Bean 提供的自定义配置。
以下是一个展示排序方式的示例:
// All of the Java Modular Configuration is applied first // <1>
@Bean 5
fun springSecurity(http: HttpSecurity): SecurityFilterChain {
http {
authorizeHttpRequests {
authorize(anyRequest, authenticated)
}
}
return http.build()
}
@Bean
@Order(Ordered.LOWEST_PRECEDENCE) 3
fun userAuthorization(): HttpSecurityDsl.() -> Unit {
return {
authorizeHttpRequests {
authorize("/users/**", hasRole("USER"))
}
}
}
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE) // <2>
fun adminAuthorization(): HttpSecurityDsl.() -> Unit {
return {
authorizeHttpRequests {
authorize("/admins/**", hasRole("ADMIN"))
}
}
}
4
@Bean
fun contentSecurityPolicy(): HeadersDsl.() -> Unit {
return {
contentSecurityPolicy {
policyDirectives = "object-src 'none'"
}
}
}
@Bean
fun contentTypeOptions(): HeadersDsl.() -> Unit {
return {
contentTypeOptions { }
}
}
@Bean
fun httpsRedirect(): HttpsRedirectDsl.() -> Unit {
return { }
}
所有模块化 HttpSecurity 配置都会被应用,因为 Kotlin DSL 使用了一个
HttpSecurityBean。所有
HttpSecurity.() → Unit实例都会被应用。adminAuthorizationBean 拥有最高的@Order值,因此它首先被应用。如果HttpSecurity.() → UnitBean 上没有@Order注解,或者@Order注解的值相同,那么HttpSecurity.() → Unit实例的应用顺序是未定义的。接下来应用
userAuthorization,因为它是一个HttpSecurity.() → Unit实例。*Dsl.() → Unit类型的应用顺序是未定义的。在这个例子中,contentSecurityPolicy、contentTypeOptions和httpsRedirect的顺序是未定义的。如果给contentTypeOptions添加了@Order(Ordered.HIGHEST_PRECEDENCE)注解,那么我们可以知道contentTypeOptions在contentSecurityPolicy之前(它们是同一类型),但我们无法知道httpsRedirect是在HeadersDsl.() → UnitBean 之前还是之后。在所有
*Dsl.() → UnitBean 被应用之后,HttpSecurityDsl作为一个 Bean 被传入。