Java 配置

Spring Framework 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 文件中。您可以在 Servlet 3.0+ 环境中使用Spring 的WebApplicationInitializer支持在 Java 配置中执行此操作。不出所料,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过滤器。

  • 添加一个加载WebSecurityConfigContextLoaderListener

带有 Spring MVC 的 AbstractSecurityWebApplicationInitializer

如果我们在应用程序的其他地方使用 Spring,我们可能已经有了一个正在加载我们的 Spring 配置的WebApplicationInitializer。如果我们使用之前的配置,我们会收到错误。相反,我们应该将 Spring Security 与现有的ApplicationContext注册。例如,如果我们使用 Spring MVC,我们的SecurityWebApplicationInitializer可能如下所示

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

public class SecurityWebApplicationInitializer
	extends AbstractSecurityWebApplicationInitializer {

}

这仅为应用程序中的每个 URL 注册springSecurityFilterChain。之后,我们需要确保在现有的ApplicationInitializer中加载了WebSecurityConfig。例如,如果我们使用 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 基本身份验证进行身份验证

请注意,此配置与 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 创建 SecurityFilterChain 的一个实例,其中包含 @Order 以指定哪个 SecurityFilterChain 应该首先被考虑。
3 http.securityMatcher 指出此 HttpSecurity 仅适用于以 /api/ 开头的 URL。
4 创建另一个 SecurityFilterChain 实例。如果 URL 不以 /api/ 开头,则使用此配置。此配置在 apiFilterChain 之后被考虑,因为它具有 @Order 值大于 1(没有 @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 方法中的代码

如果需要,您可以让 HttpSecurity 默认添加 MyCustomDsl,方法是使用 SpringFactories。例如,您可以在类路径上创建一个名为 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();
}