Java 配置

Spring 框架在 Spring 3.1 中添加了对 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 文件中。您可以在 Java 配置中使用 Spring 的WebApplicationInitializer支持 在 Servlet 3.0+ 环境中实现。毫不奇怪,Spring Security 提供了一个基类(AbstractSecurityWebApplicationInitializer)来确保springSecurityFilterChain为您注册。我们使用AbstractSecurityWebApplicationInitializer的方式取决于我们是否已经在使用 Spring 或 Spring Security 是否是我们应用程序中唯一的 Spring 组件。

没有现有 Spring 的 AbstractSecurityWebApplicationInitializer

如果您没有使用 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 的 HandlerMappingIntrospector

为多个 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 如何知道我们希望要求所有用户进行身份验证?Spring Security 如何知道我们希望支持基于表单的身份验证?实际上,有一个配置类(称为 SecurityFilterChain)在幕后被调用。它使用以下默认实现进行配置

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

默认配置(如上例所示)

  • 确保对我们应用程序的任何请求都需要用户进行身份验证

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

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

请注意,此配置与 XML 命名空间配置平行

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

多个 HttpSecurity 实例

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

@Configuration
@EnableWebSecurity
public class MultiHttpSecurityConfig {
	@Bean                                                             (1)
	public UserDetailsService userDetailsService() throws Exception {
		// ensure the passwords are encoded properly
		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(withDefaults());
		return http.build();
	}

	@Bean                                                            (4)
	public SecurityFilterChain formLoginFilterChain(HttpSecurity http) throws Exception {
		http
			.authorizeHttpRequests(authorize -> authorize
				.anyRequest().authenticated()
			)
			.formLogin(withDefaults());
		return http.build();
	}
}
1 照常配置身份验证。
2 创建包含 @OrderSecurityFilterChain 实例,以指定应首先考虑哪个 SecurityFilterChain
3 http.securityMatcher 指示此 HttpSecurity 仅适用于以 /api/ 开头的 URL。
4 创建另一个 SecurityFilterChain 实例。如果 URL 不以 /api/ 开头,则使用此配置。此配置在 apiFilterChain 之后被考虑,因为它具有 1 之后的 @Order 值(没有 @Order 默认情况下为最后)。

自定义 DSL

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

  • Java

  • Kotlin

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

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

	@Override
	public void configure(HttpSecurity http) throws Exception {
		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();
	}
}
class MyCustomDsl : AbstractHttpConfigurer<MyCustomDsl, HttpSecurity>() {
    var flag: Boolean = false

    override fun init(http: HttpSecurity) {
        // any method that adds another configurer
        // must be done in the init method
        http.csrf().disable()
    }

    override fun configure(http: HttpSecurity) {
        val context: ApplicationContext = http.getSharedObject(ApplicationContext::class.java)

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

    companion object {
        @JvmStatic
        fun customDsl(): MyCustomDsl {
            return MyCustomDsl()
        }
    }
}

这实际上是如何实现像 HttpSecurity.authorizeHttpRequests() 这样的方法。

然后您可以使用自定义 DSL

  • Java

  • Kotlin

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

    @Bean
    fun filterChain(http: HttpSecurity): SecurityFilterChain {
        http
            .with(MyCustomDsl.customDsl()) {
                flag = true
            }
            // ...

        return http.build()
    }
}

代码按以下顺序调用

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

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

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

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

META-INF/spring.factories
org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer = sample.MyCustomDsl

您也可以显式禁用默认值

  • Java

  • Kotlin

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

    @Bean
    fun filterChain(http: HttpSecurity): SecurityFilterChain {
        http
            .with(MyCustomDsl.customDsl()) {
                disable()
            }
            // ...
        return http.build()
    }

}

后处理配置对象

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();
}