Java 配置
Spring Framework 在 Spring 3.1 中添加了对 Java 配置的普遍支持。Spring Security 3.2 引入了 Java 配置,允许用户无需使用任何 XML 即可配置 Spring Security。
如果您熟悉 Security Namespace Configuration,您会发现它与 Spring Security Java 配置之间有很多相似之处。
Spring Security 提供了 许多示例应用程序 来演示 Spring Security Java 配置的使用。 |
你好 Web 安全 Java 配置
第一步是创建我们的 Spring Security Java 配置。该配置会创建一个名为 springSecurityFilterChain
的 Servlet Filter,它负责应用程序中的所有安全事务(保护应用程序 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;
}
}
这个配置虽然不复杂或不广泛,但它做了很多事情
-
要求对应用程序中的每个 URL 进行认证
-
为您生成一个登录表单
-
允许 用户名 为
user
且 密码 为password
的用户通过表单认证进行认证 -
允许用户退出登录
-
CSRF 攻击防护
-
会话固定攻击防护
-
安全头集成
-
用于安全请求的 HTTP Strict Transport Security
-
缓存控制(您可以在应用程序中稍后覆盖此设置以允许缓存静态资源)
-
X-Frame-Options 集成以帮助防止 点击劫持
-
-
与以下 Servlet API 方法集成
AbstractSecurityWebApplicationInitializer
下一步是将 springSecurityFilterChain
注册到 WAR 文件。在 Servlet 3.0+ 环境中,您可以使用 Spring 的 WebApplicationInitializer
支持 在 Java 配置中完成此操作。不出所料,Spring Security 提供了一个基类(AbstractSecurityWebApplicationInitializer
)来确保为您注册 springSecurityFilterChain
。我们使用 AbstractSecurityWebApplicationInitializer
的方式取决于我们是否已在使用 Spring,或者 Spring Security 是否是我们应用程序中唯一的 Spring 组件。
-
不使用现有 Spring 的 AbstractSecurityWebApplicationInitializer - 如果您尚未在使用 Spring,请使用这些说明
-
与 Spring MVC 一起使用的 AbstractSecurityWebApplicationInitializer - 如果您已在使用 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
Filter。 -
添加一个加载 WebSecurityConfig 的
ContextLoaderListener
。
与 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 Dispatcher 配置
如果需要,任何与 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(Customizer.withDefaults())
.httpBasic(Customizer.withDefaults());
return http.build();
}
默认配置(如上例所示)
-
确保对应用程序的任何请求都需要用户进行认证
-
允许用户通过基于表单的登录进行认证
-
允许用户通过 HTTP Basic 认证进行认证
请注意,此配置与 XML namespace 配置并行
<http>
<intercept-url pattern="/**" access="authenticated"/>
<form-login />
<http-basic />
</http>
多个 HttpSecurity 实例
为了有效管理应用程序中某些区域需要不同保护的安全,我们可以使用多个过滤器链以及 securityMatcher
DSL 方法。这种方法允许我们定义针对应用程序特定部分的独特安全配置,从而增强应用程序整体的安全性和控制力。
我们可以像在 XML 中配置多个 <http>
块一样配置多个 HttpSecurity
实例。关键是注册多个 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();
}
}
1 | 像往常一样配置认证。 |
2 | 创建一个包含 @Order 的 SecurityFilterChain 实例,用于指定哪个 SecurityFilterChain 应该首先被考虑。 |
3 | http.securityMatcher() 表示此 HttpSecurity 仅适用于以 /api/ 开头的 URL。 |
4 | 创建另一个 SecurityFilterChain 实例。如果 URL 不以 /api/ 开头,则使用此配置。此配置在 apiFilterChain 之后考虑,因为它具有 @Order 值在 1 之后(没有 @Order 默认排在最后)。 |
选择 securityMatcher
或 requestMatchers
一个常见问题是
http.securityMatcher()
方法和用于请求授权的requestMatchers()
(即在http.authorizeHttpRequests()
内部)有什么区别?
为了回答这个问题,了解用于构建 SecurityFilterChain
的每个 HttpSecurity
实例都包含一个 RequestMatcher
来匹配传入请求会有帮助。如果一个请求不匹配具有更高优先级的 SecurityFilterChain
(例如 @Order(1)
),该请求可以尝试与具有较低优先级的过滤器链(例如没有 @Order
)进行匹配。
多个过滤器链的匹配逻辑由 |
默认的 RequestMatcher
匹配任何请求,以确保 Spring Security 默认保护所有请求。
指定 |
如果没有过滤器链匹配特定请求,则该请求不受 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();
}
}
1 | 以 /secured/ 开头的请求将受到保护,而其他任何请求则不受保护。 |
2 | 对 /secured/user 的请求需要 ROLE_USER 权限。 |
3 | 对 /secured/admin 的请求需要 ROLE_ADMIN 权限。 |
4 | 任何其他请求(例如 /secured/other )只需要已认证的用户即可。 |
推荐提供一个不指定任何 |
请注意,requestMatchers
方法仅适用于单个授权规则。那里列出的每个请求还必须与用于创建 SecurityFilterChain
的特定 HttpSecurity
实例的整体 securityMatcher
相匹配。本例中使用的 anyRequest()
匹配此特定 SecurityFilterChain
(必须以 /secured/
开头)内的所有其他请求。
有关 |
SecurityFilterChain
端点
SecurityFilterChain
中的几个过滤器直接提供端点,例如由 http.formLogin()
设置并提供 POST /login
端点的 UsernamePasswordAuthenticationFilter
。在上面的示例中,/login
端点不被 http.securityMatcher("/secured/**")
匹配,因此该应用程序将没有 GET /login
或 POST /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()
)
.formLogin(Customizer.withDefaults());
return http.build();
}
@Bean
public SecurityFilterChain defaultFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.anyRequest().denyAll() (5)
);
return http.build();
}
}
1 | 以 /secured/ 开头的请求将受此过滤器链保护。 |
2 | 以 /secured/ 开头的请求需要经过认证的用户。 |
3 | 自定义表单登录,将 URL 前缀设置为 /secured/ 。 |
4 | 自定义退出登录,将 URL 前缀设置为 /secured/ 。 |
5 | 所有其他请求将被拒绝。 |
此示例自定义了登录和退出页面,这将禁用 Spring Security 生成的页面。您必须为 |
实际示例
以下示例展示了一个更贴近实际的配置,将所有这些元素结合在一起
@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();
}
}
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。 |
自定义 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()
}
}
}
这实际上就是 |
然后您可以使用自定义 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
方法中的代码
如果需要,您可以使用 SpringFactories 使 HttpSecurity 默认添加 MyCustomDsl。例如,您可以在类路径上创建一个名为 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();
}