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。