跳到主要内容
版本:7.0.2

Java 配置

DeepSeek V3 中英对照 Java Configuration

自 Spring 3.1 起,Spring Framework 开始全面支持 Java 配置。随后在 Spring Security 3.2 中引入了 Java 配置功能,使用户无需使用任何 XML 即可配置 Spring Security。

如果你熟悉安全命名空间配置,你会发现它与 Spring Security Java 配置之间有许多相似之处。

备注

Spring Security 提供了大量示例应用来演示 Spring Security Java 配置的使用。

Hello Web Security Java 配置

第一步是创建我们的Spring Security Java配置。该配置会创建一个名为springSecurityFilterChain的Servlet过滤器,它负责应用程序中的所有安全功能(保护应用URL、验证提交的用户名和密码、重定向到登录表单等)。以下示例展示了Spring Security Java配置的最基础示例:

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.context.annotation.*;
import org.springframework.security.config.annotation.authentication.builders.*;
import org.springframework.security.config.annotation.web.configuration.*;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig {

@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withDefaultPasswordEncoder().username("user").password("password").roles("USER").build());
return manager;
}
}

这个配置并不复杂或庞大,但它能完成很多工作:

AbstractSecurityWebApplicationInitializer

下一步是将 springSecurityFilterChain 注册到 WAR 文件中。在 Servlet 3.0+ 环境中,你可以通过 Java 配置,利用 Spring 的 WebApplicationInitializer 支持 来完成。不出所料,Spring Security 提供了一个基类 (AbstractSecurityWebApplicationInitializer) 来确保 springSecurityFilterChain 被正确注册。我们使用 AbstractSecurityWebApplicationInitializer 的方式,取决于我们是否已经在使用 Spring,或者 Spring Security 是否是我们应用程序中唯一的 Spring 组件。

摘要:无现有 Spring 环境下的 SecurityWebApplicationInitializer

如果你没有使用 Spring 或 Spring MVC,需要将 WebSecurityConfig 传递给父类,以确保配置被正确加载:

import org.springframework.security.web.context.*;

public class SecurityWebApplicationInitializer
extends AbstractSecurityWebApplicationInitializer {

public SecurityWebApplicationInitializer() {
super(WebSecurityConfig.class);
}
}

SecurityWebApplicationInitializer

  • 自动为应用程序中的每个URL注册 springSecurityFilterChain 过滤器。
  • 添加一个 ContextLoaderListener,用于加载 WebSecurityConfig

使用 Spring MVC 的 AbstractSecurityWebApplicationInitializer

如果我们在应用程序的其他地方使用Spring,很可能已经有一个WebApplicationInitializer在加载Spring配置。如果采用之前的配置,将会出现错误。此时,我们应该将Spring Security注册到现有的ApplicationContext中。例如,如果使用Spring MVC,我们的SecurityWebApplicationInitializer可以按以下方式配置:

import org.springframework.security.web.context.*;

public class SecurityWebApplicationInitializer
extends AbstractSecurityWebApplicationInitializer {

}

这仅为应用程序中的每个URL注册了springSecurityFilterChain。之后,我们需要确保WebSecurityConfig已加载到现有的ApplicationInitializer中。例如,如果使用Spring MVC,则需将其添加到getServletConfigClasses()方法中:

public class MvcWebApplicationInitializer extends
AbstractAnnotationConfigDispatcherServletInitializer {

@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[] { WebSecurityConfig.class, WebMvcConfig.class };
}

// ... other overrides ...
}

这是因为Spring Security需要能够检查某些Spring MVC配置,以便正确配置底层请求匹配器,因此它们需要位于同一个应用上下文中。将Spring Security置于getRootConfigClasses中会将其放入父应用上下文,而该上下文可能无法找到Spring MVC的PathPatternParser

配置多个Spring MVC调度器

如果需要,任何与Spring MVC无关的Spring Security配置都可以放在不同的配置类中,如下所示:

public class MvcWebApplicationInitializer extends
AbstractAnnotationConfigDispatcherServletInitializer {

@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[] { NonWebSecurityConfig.class };
}

@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[] { WebSecurityConfig.class, WebMvcConfig.class };
}

// ... other overrides ...
}

如果你有多个 AbstractAnnotationConfigDispatcherServletInitializer 实例,并且不想在它们之间重复通用的安全配置,这会很有帮助。

HttpSecurity

到目前为止,我们的 WebSecurityConfig 仅包含关于如何验证用户身份的信息。Spring Security 是如何知道我们需要所有用户都经过身份验证的呢?它又是如何知道我们想要支持基于表单的身份验证的呢?实际上,背后调用了一个配置类(称为 SecurityFilterChain),它通过以下默认实现进行配置:

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults())
.httpBasic(Customizer.withDefaults());
return http.build();
}

默认配置(如前面的示例所示):

  • 确保对我们应用程序的任何请求都要求用户已通过身份验证

  • 允许用户通过基于表单的登录进行身份验证

  • 允许用户通过 HTTP Basic 身份验证进行身份验证

请注意,此配置与 XML 命名空间配置相对应:

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

多个 HttpSecurity 实例

为了在应用中有效管理安全性,特别是当不同区域需要不同级别的保护时,我们可以采用多个过滤器链,并结合使用 securityMatcher DSL 方法。这种方法允许我们为应用的特定部分定义不同的安全配置,从而增强整体应用的安全性和控制力。

我们可以配置多个 HttpSecurity 实例,就像在 XML 中可以配置多个 <http> 块一样。关键在于注册多个 SecurityFilterChain @Bean。以下示例对以 /api/ 开头的 URL 采用了不同的配置:

@Configuration
@EnableWebSecurity
public class MultiHttpSecurityConfig {
@Bean 1
public UserDetailsService userDetailsService() throws Exception {
UserBuilder users = User.withDefaultPasswordEncoder();
InMemoryUserDetailsManager manager = new 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
public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**") 3
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().hasRole("ADMIN")
)
.httpBasic(Customizer.withDefaults());
return http.build();
}

@Bean 4
public SecurityFilterChain formLoginFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults());
return http.build();
}
}
  • 按常规方式配置身份验证。

  • 创建一个包含 @Order 注解的 SecurityFilterChain 实例,以指定应优先考虑哪个 SecurityFilterChain

  • http.securityMatcher() 声明此 HttpSecurity 仅适用于以 /api/ 开头的 URL。

  • 创建另一个 SecurityFilterChain 实例。如果 URL 不以 /api/ 开头,则使用此配置。由于它的 @Order 值在 1 之后(没有 @Order 注解则默认为最后),此配置将在 apiFilterChain 之后被考虑。

选择 securityMatcherrequestMatchers

一个常见的问题是:

http.securityMatcher() 方法与用于请求授权的 requestMatchers()(即在 http.authorizeHttpRequests() 内部使用)之间有什么区别?

要回答这个问题,需要理解每个用于构建 SecurityFilterChainHttpSecurity 实例都包含一个 RequestMatcher 来匹配传入的请求。如果一个请求未能匹配到更高优先级的 SecurityFilterChain(例如 @Order(1)),那么该请求就可以尝试匹配优先级较低的过滤器链(例如没有 @Order 注解的)。

备注

多个过滤器链的匹配逻辑由 FilterChainProxy 执行。

默认的 RequestMatcher 会匹配所有请求,以确保 Spring Security 默认保护所有请求

备注

指定 securityMatcher 会覆盖此默认设置。

注意

如果没有任何过滤器链匹配特定请求,则该请求不受 Spring Security 保护。

以下示例展示了一个仅保护以 /secured/ 开头的请求的单一过滤器链:

@Configuration
@EnableWebSecurity
public class PartialSecurityConfig {

@Bean
public UserDetailsService userDetailsService() throws Exception {
// ...
}

@Bean
public SecurityFilterChain securedFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/secured/**") 1
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/secured/user").hasRole("USER") 2
.requestMatchers("/secured/admin").hasRole("ADMIN") 3
.anyRequest().authenticated() 4
)
.httpBasic(Customizer.withDefaults())
.formLogin(Customizer.withDefaults());
return http.build();
}
}
  • /secured/ 开头的请求将受到保护,但其他任何请求不受保护。

  • /secured/user 的请求需要 ROLE_USER 权限。

  • /secured/admin 的请求需要 ROLE_ADMIN 权限。

  • 任何其他请求(例如 /secured/other)仅需要经过身份验证的用户。

提示

建议提供一个不指定任何 securityMatcherSecurityFilterChain,以确保整个应用程序都受到保护,如前面的示例所示。

请注意,requestMatchers 方法仅适用于单个授权规则。其中列出的每个请求还必须匹配用于创建此 SecurityFilterChain 的特定 HttpSecurity 实例的总体 securityMatcher。在此示例中,使用 anyRequest() 将匹配此特定 SecurityFilterChain 内的所有其他请求(这些请求必须以 /secured/ 开头)。

备注

有关 requestMatchers 的更多信息,请参阅授权 HttpServletRequests

SecurityFilterChain 端点

SecurityFilterChain 中的多个过滤器直接提供端点,例如由 http.formLogin() 设置的 UsernamePasswordAuthenticationFilter 提供了 POST /login 端点。在上面的示例中,/login 端点未被 http.securityMatcher("/secured/**") 匹配,因此该应用程序将没有任何 GET /loginPOST /login 端点。此类请求将返回 404 Not Found。这常常让用户感到意外。

指定 http.securityMatcher() 会影响该 SecurityFilterChain 匹配哪些请求。然而,它不会自动影响该过滤器链提供的端点。在这种情况下,您可能需要自定义希望该过滤器链提供的任何端点的 URL。

以下示例演示了一个配置,该配置保护以 /secured/ 开头的请求并拒绝所有其他请求,同时还自定义了由 SecurityFilterChain 提供的端点:

@Configuration
@EnableWebSecurity
public class SecuredSecurityConfig {

@Bean
public UserDetailsService userDetailsService() throws Exception {
// ...
}

@Bean
@Order(1)
public SecurityFilterChain securedFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/secured/**") 1
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated() 2
)
.formLogin((formLogin) -> formLogin 3
.loginPage("/secured/login")
.loginProcessingUrl("/secured/login")
.permitAll()
)
.logout((logout) -> logout 4
.logoutUrl("/secured/logout")
.logoutSuccessUrl("/secured/login?logout")
.permitAll()
);
return http.build();
}

@Bean
public SecurityFilterChain defaultFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().denyAll() 5
);
return http.build();
}
}
  • /secured/ 开头的请求将受此过滤器链保护。

  • /secured/ 开头的请求需要经过身份验证的用户。

  • 自定义表单登录,将 URL 前缀设置为 /secured/

  • 自定义注销,将 URL 前缀设置为 /secured/

  • 所有其他请求将被拒绝。

备注

此示例自定义了登录和注销页面,这会禁用 Spring Security 自动生成的页面。你必须为 GET /secured/loginGET /secured/logout 提供自己的自定义端点。请注意,Spring Security 仍会为你提供 POST /secured/loginPOST /secured/logout 端点。

真实世界示例

以下示例展示了一个稍微更贴近实际场景的配置,它将所有这些元素整合在一起:

@Configuration
@EnableWebSecurity
public class BankingSecurityConfig {

@Bean 1
public UserDetailsService userDetailsService() {
UserBuilder users = User.withDefaultPasswordEncoder();
InMemoryUserDetailsManager manager = new 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
public SecurityFilterChain approvalsSecurityFilterChain(HttpSecurity http) throws Exception {
String[] approvalsPaths = { "/accounts/approvals/**", "/loans/approvals/**", "/credit-cards/approvals/**" };
http
.securityMatcher(approvalsPaths)
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().hasRole("ADMIN")
)
.httpBasic(Customizer.withDefaults());
return http.build();
}

@Bean
@Order(2) 3
public SecurityFilterChain bankingSecurityFilterChain(HttpSecurity http) throws Exception {
String[] bankingPaths = { "/accounts/**", "/loans/**", "/credit-cards/**", "/balances/**" };
String[] viewBalancePaths = { "/balances/**" };
http
.securityMatcher(bankingPaths)
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers(viewBalancePaths).hasRole("VIEW_BALANCE")
.anyRequest().hasRole("USER")
);
return http.build();
}

@Bean 4
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
String[] allowedPaths = { "/", "/user-login", "/user-logout", "/notices", "/contact", "/register" };
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers(allowedPaths).permitAll()
.anyRequest().authenticated()
)
.formLogin((formLogin) -> formLogin
.loginPage("/user-login")
.loginProcessingUrl("/user-login")
)
.logout((logout) -> 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。

自定义DSL

在Spring Security中,您可以提供自定义的DSL:

public class MyCustomDsl extends AbstractHttpConfigurer<MyCustomDsl, HttpSecurity> {
private boolean flag;

@Override
public void init(HttpSecurity http) {
// any method that adds another configurer
// must be done in the init method
http.csrf(csrf -> csrf.disable());
}

@Override
public void configure(HttpSecurity http) {
ApplicationContext context = http.getSharedObject(ApplicationContext.class);

// here we lookup from the ApplicationContext. You can also just create a new instance.
MyFilter myFilter = context.getBean(MyFilter.class);
myFilter.setFlag(flag);
http.addFilterBefore(myFilter, UsernamePasswordAuthenticationFilter.class);
}

public MyCustomDsl flag(boolean value) {
this.flag = value;
return this;
}

public static MyCustomDsl customDsl() {
return new MyCustomDsl();
}
}
备注

实际上,HttpSecurity.authorizeHttpRequests() 这类方法的实现方式正是如此。

然后你就可以使用自定义的DSL:

@Configuration
@EnableWebSecurity
public class Config {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.with(MyCustomDsl.customDsl(), (dsl) -> dsl
.flag(true)
)
// ...
return http.build();
}
}

代码按以下顺序被调用:

  • Config.filterChain 方法中的代码被调用

  • MyCustomDsl.init 方法中的代码被调用

  • MyCustomDsl.configure 方法中的代码被调用

如果你愿意,可以通过使用 SpringFactoriesHttpSecurity 默认添加 MyCustomDsl。例如,你可以在类路径上创建一个名为 META-INF/spring.factories 的资源文件,其内容如下:

org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer = sample.MyCustomDsl

你也可以显式禁用默认设置:

@Configuration
@EnableWebSecurity
public class Config {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.with(MyCustomDsl.customDsl(), (dsl) -> dsl
.disable()
)
...;
return http.build();
}
}

模块化 HttpSecurity 配置

许多用户倾向于将 Spring Security 配置集中管理,并选择在单个 SecurityFilterChain 实例中进行配置。然而,有时用户可能希望将配置模块化。这可以通过以下方式实现:

备注

如果你正在使用 Spring Security 的 Kotlin 配置,那么你也可以按照模块化 HttpSecurityDsl 配置中概述的方式,将 *Dsl → Unit Bean 暴露出来。

Customizer<HttpSecurity> Bean

若您希望将安全配置模块化,可将逻辑置于 Customizer<HttpSecurity> Bean 中。例如,以下配置将确保所有 HttpSecurity 实例均被配置为:

@Bean
ThrowingCustomizer<HttpSecurity> httpSecurityCustomizer() {
return (http) -> http
.headers((headers) -> headers
.contentSecurityPolicy((csp) -> csp
1
.policyDirectives("object-src 'none'")
)
)
2
.redirectToHttps(Customizer.withDefaults());
}

顶级 HttpSecurity 自定义器 Bean

如果您希望对安全配置进行进一步的模块化,Spring Security 将自动应用任何顶层的 HttpSecurity Customizer Bean。

顶层的 HttpSecurity Customizer 类型可概括为任何符合 public HttpSecurity.*(Customizer<T>) 模式的 Customizer<T>。这表示任何作为 HttpSecurity 类中公共方法单一参数的 Customizer<T>

几个例子可以帮助阐明这一点。如果 Customizer<ContentTypeOptionsConfig> 被发布为 Bean,它不会自动应用,因为它是 HeadersConfigurer.contentTypeOptions(Customizer) 方法的参数,而该方法并非定义在 HttpSecurity 上。然而,如果 Customizer<HeadersConfigurer<HttpSecurity>> 被发布为 Bean,它将会自动应用,因为它是 HttpSecurity.headers(Customizer) 方法的参数。

例如,以下配置将确保将内容安全策略设置为 object-src 'none'

@Bean
Customizer<HeadersConfigurer<HttpSecurity>> headersSecurity() {
return (headers) -> headers
.contentSecurityPolicy((csp) -> csp
1
.policyDirectives("object-src 'none'")
);
}

自定义器 Bean 的排序

首先,每个 Customizer<HttpSecurity> Bean 会通过 ObjectProvider#orderedStream() 来应用。这意味着,如果有多个 Customizer<HttpSecurity> Bean,可以在 Bean 定义上添加 @Order 注解来控制它们的顺序。

接下来,查找每个顶级 HttpSecurity 自定义器 Bean 类型,并使用 ObjectProvider#orderedStream() 依次应用它们。如果存在两个 Customizer<HeadersConfigurer<HttpSecurity>> Bean 和两个 Customizer<HttpsRedirectConfigurer<HttpSecurity>> 实例,则每个 Customizer 类型的调用顺序是未定义的。然而,每个 Customizer<HttpsRedirectConfigurer<HttpSecurity>> 实例的顺序由 ObjectProvider#orderedStream() 定义,并且可以通过在 Bean 定义上使用 @Order 注解来控制。

最后,HttpSecurity Bean 被注入为一个 Bean。所有 Customizer 实例都在 HttpSecurity Bean 创建之前应用。这允许覆盖由 Customizer Bean 提供的自定义配置。

以下是一个展示排序方式的示例:

@Bean 4
SecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((requests) -> requests
.anyRequest().authenticated()
);
return http.build();
}

@Bean
@Order(Ordered.LOWEST_PRECEDENCE) 2
ThrowingCustomizer<HttpSecurity> userAuthorization() {
return (http) -> http
.authorizeHttpRequests((requests) -> requests
.requestMatchers("/users/**").hasRole("USER")
);
}

@Bean
@Order(Ordered.HIGHEST_PRECEDENCE) 1
ThrowingCustomizer<HttpSecurity> adminAuthorization() {
return (http) -> http
.authorizeHttpRequests((requests) -> requests
.requestMatchers("/admins/**").hasRole("ADMIN")
);
}

3

@Bean
Customizer<HeadersConfigurer<HttpSecurity>> contentSecurityPolicy() {
return (headers) -> headers
.contentSecurityPolicy((csp) -> csp
.policyDirectives("object-src 'none'")
);
}

@Bean
Customizer<HeadersConfigurer<HttpSecurity>> contentTypeOptions() {
return (headers) -> headers
.contentTypeOptions(Customizer.withDefaults());
}

@Bean
Customizer<HttpsRedirectConfigurer<HttpSecurity>> httpsRedirect() {
return Customizer.withDefaults();
}
  • 首先应用所有 Customizer<HttpSecurity> 实例。adminAuthorization Bean 的 @Order 值最高,因此最先被应用。如果 Customizer<HttpSecurity> Bean 上没有 @Order 注解,或者 @Order 注解的值相同,那么 Customizer<HttpSecurity> 实例的应用顺序是未定义的。

  • 接下来应用 userAuthorization,因为它是一个 Customizer<HttpSecurity> 实例。

  • Customizer 类型的应用顺序是未定义的。在此示例中,contentSecurityPolicycontentTypeOptionshttpsRedirect 的顺序是未定义的。如果给 contentTypeOptions 添加了 @Order(Ordered.HIGHEST_PRECEDENCE),那么我们可以知道 contentTypeOptionscontentSecurityPolicy 之前(它们是同一类型),但我们无法确定 httpsRedirect 是在 Customizer<HeadersConfigurer<HttpSecurity>> Bean 之前还是之后。

  • 在所有 Customizer Bean 应用完毕后,HttpSecurity 会作为一个 Bean 被传入。

后处理已配置对象

Spring Security的Java配置并未暴露其所配置的每个对象的全部属性。这为大多数用户简化了配置过程。毕竟,如果每个属性都暴露出来,用户就可以使用标准的Bean配置方式了。

尽管有充分理由不直接暴露每个属性,但用户可能仍需要更高级的配置选项。为解决这一问题,Spring Security 引入了 ObjectPostProcessor 的概念,可用于修改或替换 Java 配置创建的许多 Object 实例。例如,要配置 FilterSecurityInterceptor 上的 filterSecurityPublishAuthorizationSuccess 属性,可以使用以下方式:

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
public <O extends FilterSecurityInterceptor> O postProcess(
O fsi) {
fsi.setPublishAuthorizationSuccess(true);
return fsi;
}
})
);
return http.build();
}