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() 內部)有什麼區別?

為了回答這個問題,瞭解每個用於構建 SecurityFilterChainHttpSecurity 例項都包含一個 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,以確保整個應用程式受到保護,如 前面的示例 所示。

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

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

SecurityFilterChain 端點

SecurityFilterChain 中的幾個過濾器直接提供端點,例如 UsernamePasswordAuthenticationFilterhttp.formLogin() 設定並提供 POST /login 端點。在 上述示例 中,/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。

模組化 HttpSecurityDsl 配置

許多使用者更喜歡將他們的 Spring Security 配置集中在一個地方,並將選擇在一個 SecurityFilterChain 例項中配置它。然而,有時使用者可能希望模組化配置。這可以透過使用以下方法完成:

由於 Spring Security Kotlin Dsl (HttpSecurityDsl) 使用 HttpSecurity,因此所有 Java 模組化 Bean 自定義 都在 模組化 HttpSecurity 配置 之前應用。

HttpSecurityDsl.() → Unit Beans

如果您希望模組化您的安全配置,可以將邏輯放在 HttpSecurityDsl.() → Unit Bean 中。例如,以下配置將確保所有 HttpSecurityDsl 例項都配置為:

@Bean
fun httpSecurityDslBean(): HttpSecurityDsl.() -> Unit {
    return {
        headers {
            contentSecurityPolicy {
                (1)
                policyDirectives = "object-src 'none'"
            }
        }
        (2)
        redirectToHttps { }
    }
}
1 內容安全策略 設定為 object-src 'none'
2 將任何請求重定向到 https

頂級安全 Dsl Beans

如果您更喜歡進一步模組化您的安全配置,Spring Security 將自動應用任何頂級安全 Dsl Beans。

頂級安全 Dsl 可以概括為任何與 public HttpSecurityDsl.*(<Dsl>) 匹配的類 Dsl 類。這表示任何作為 HttpSecurityDsl 上公共方法的單個引數的安全 Dsl。

幾個例子可以幫助澄清。如果 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'"
        }
    }
}
1 內容安全策略 設定為 object-src 'none'

Dsl Bean 排序

首先,應用所有 模組化 HttpSecurity 配置,因為 Kotlin Dsl 使用 HttpSecurity Bean。

其次,每個 HttpSecurityDsl.() → Unit Beans 都使用 ObjectProvider#orderedStream() 應用。這意味著如果存在多個 HttpSecurity.() → Unit Beans,可以在 Bean 定義中新增 @Order 註解來控制排序。

接下來,查詢並應用每個 頂級安全 Dsl Beans 型別,並使用 ObjectProvider#orderedStream() 應用每個型別。如果存在不同型別的頂級安全 Beans(例如 HeadersDsl.() → UnitHttpsRedirectDsl.() → Unit),則每個 Dsl 型別被呼叫的順序是未定義的。但是,相同頂級安全 Bean 型別的每個例項的順序由 ObjectProvider#orderedStream() 定義,並且可以透過在 Bean 定義上使用 @Order 來控制。

最後,HttpSecurityDsl Bean 被注入為一個 Bean。所有 *Dsl.() → Unit Beans 都在 HttpSecurityDsl Bean 建立之前應用。這允許覆蓋 *Dsl.() → Unit Beans 提供的自定義。

您可以在下面找到一個說明排序的示例

// 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 { }
}
1 所有 模組化 HttpSecurity 配置 都已應用,因為 Kotlin Dsl 使用了一個 HttpSecurity Bean。
2 所有 HttpSecurity.() → Unit 例項都已應用。adminAuthorization Bean 具有最高的 @Order,因此它首先應用。如果 HttpSecurity.() → Unit Beans 上沒有 @Order 註解,或者 @Order 註解具有相同的值,則 HttpSecurity.() → Unit 例項的應用順序是未定義的。
3 由於是 HttpSecurity.() → Unit 的例項,userAuthorization 接下來應用。
4 *Dsl.() → Unit 型別的順序是未定義的。在此示例中,contentSecurityPolicycontentTypeOptionshttpsRedirect 的順序是未定義的。如果將 @Order(Ordered.HIGHEST_PRECEDENCE) 新增到 contentTypeOptions,那麼我們就會知道 contentTypeOptionscontentSecurityPolicy 之前(它們是相同的型別),但我們不知道 httpsRedirect 是在 HeadersDsl.() → Unit Beans 之前還是之後。
5 在所有 *Dsl.() → Unit Bean 應用後,HttpSecurityDsl 將作為 Bean 傳入。
© . This site is unofficial and not affiliated with VMware.