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.