Getting Started
SpringBoot Maven项目添加Spring Security依赖。
$ curl -i http://localhost:8080/some/path
HTTP/1.1 401
...
就会返回401 Unauthorized
Tip 如果你在浏览器中请求该URL,你会看到一个默认的登录页。
如果加上验证信息,即用户名为user,密码为console中打印的generated security password:。
$ curl -i -u user:8e557245-73e2-4286-969a-ff57fe326336 http://localhost:8080/some/path
HTTP/1.1 404
...
就可以访问到该接口,这里的404是由于我们未定义该接口。
默认行为
这里我们将介绍Spring Security Starter所提供的默认行为:
- 对于所有endpoint 都需要验证登录信息
- 注册了一个默认的用户,其生成的密码在启动的时候会打印到日志
- 使用BCrypt以及其他方式保护密码存储
- 提供了基于表单的登录 登出流程
- 提供了基于表单或者http basic的验证
- 提供了内容协商功能;对于web请求 会重定向到登录页,对于service请求,返回401
- 对CSRF攻击的防范 (Mitigates CSRF attacks)
- 对Session Fixation的防范
- Writes Strict-Transport-Security to ensure HTTPS
- Writes X-Content-Type-Options to mitigate sniffing attacks
- Writes Cache Control headers that protect authenticated resources
- Writes X-Frame-Options to mitigate Clickjacking
- 集成了
HttpServletRequest's authentication methods - 对认证成功事件或者失败事件的广播
下面是Spring Security Starter自动配置的简化示例,来方便说明
@EnableWebSecurity
@Configuration
public class DefaultSecurityConfig {
@Bean
@ConditionalOnMissingBean(UserDetailsService.class)
InMemoryUserDetailsManager inMemoryUserDetailsManager() {
String generatedPassword = // ...;
return new InMemoryUserDetailsManager(User.withUsername("user")
.password(generatedPassword).roles("USER").build());
}
@Bean
@ConditionalOnMissingBean(AuthenticationEventPublisher.class)
DefaultAuthenticationEventPublisher defaultAuthenticationEventPublisher(ApplicationEventPublisher delegate) {
return new DefaultAuthenticationEventPublisher(delegate);
}
}
添加了@EnableWebSecurity注解(将Spring Security默认的 filter chain 注册为了bean)
创建了UserDetailsService bean,创建了一个username为user以及一个随机生成的password,该password会在console中打印出来
创建了一个AuthenticationEventPublisher bean用来发布authentication事件
Note
只要将Filter声明为bean,spring Boot就会将其加入到应用的filter chain中。这意味着,在Spring Boot使用@EnableWebSecurity注解会自动注册Spring Security的filter chain。
Spring Security的架构
Spring Security在Servlet的实现是基于Servlet Filter。所以先大体浏览下Filter。下面图片展示了对于单个HTTP请求的电影处理分层。
Client发送请求到应用,容器会创建一条FilterChain,其中包含Filter实例和用来处理HttpServletRequest的Servlet。在Spring MVC应用中,Servlet是DispatcherService的实例,只有一个Servlet来处理一个HttpServletRequest和一个HttpServletResponse。但是可以使用多个Filter:
- 避免下游的
Filter或Servlet被执行。对于这种情况,Filter通常会写入HttpServletResponse - 修改下游
Filter和Servlet所用到的HttpServletRequestorHttpServletResponse
FilterChain Usage Example
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
// do something before the rest of the application
chain.doFilter(request, response); // invoke the rest of the application
// do something after the rest of the application
}
由于Filter只影响下游的Filter和Servlet,所以Filter的顺序非常重要。
DelegatingFilterProxy
Spring提供一个名为DelegatingFilterProxy的Filter实现,该Filter在Servlet 容器的生命周期和Spring的ApplicationContext之间建立了桥梁。因为普通Servlet容器的Filter访问不到Spring容器的bean。你可以注册DelegatingFilterProxy到标准的Servlet容器中,然后实际逻辑转交给实现Filter的Spring Bean。
下图是DelegatingFilterProxy如何融入进Filter 实例和FilterChain的。
DelegatingFilterProxy会从ApplicationContext中寻找Bean Filter0,并执行它。下面就是这段逻辑的伪代码
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
Filter delegate = getFilterBean(someBeanName);
delegate.doFilter(request, response);
}
FilterChainProxy
Spring Security对Servlet支持是通过FilterChainProxy实现的。FilterChainProxy是一个由Spring Security提供的Filter,可以让SecurityFilterChain委托多个Filter。由于FilterChainProxy是一个Bean,所以它通常会被DelegatingFilterProxy包装。
SecurityFilterChain
SecurityFilterChain用来确定当前request应该调用哪些Spring Security Filter。
SecurityFilterChain中的Security Filter通常是一些bean,但他们通过FilterChainProxy注册而不是通过DelegatingFilterProxy。使用FilterChainProxy的优点是:
- 它是Spring Security Servlet的入口,所以当你对Spring Security 对 servlet的支持有疑问时,可以打个断点从这里排查
- 因为
FilterChainProxy是Spring Security使用的核心,它可以执行类似清除SecurityContext来避免内存泄漏的任务,它还可以应用Spring Security的HttpFireWall来保护应用免受一些攻击。 - 在确定应该执行哪条
SecurityFilterChain上,它提供了非常大的灵活性。在Servlet容器中,Filter是否执行取决于URL。然而,FilterChainProxy可以根据HttpServletRequest中的任何东西来进行选择
下图展示了多个SecurityFilterChain的情况
在上图中,FilterChainProxy来决定应该用哪个SecurityFilterChain。注意 只有第一个匹配的SecurityFilterChain会被执行。比如,请求的url为/api/message,第一个匹配的SecurityFilterChain即chain0会被执行。如果请求的url为/message,chain n就会被执行。
请注意SecurityFilterChain 0只有三个Security Filter实例,SecurityFilterChain n有4个Filter实例。理解 每个chain的配置都是隔离的非常重要。实际上,SecurityFilterChain可以不配置Filter,这样应用就可以让Spring Security忽略特定request。
Security FIlters
Security Filters是通过SecuritFilterChain Api添加到 FIlterChainProxy中的。这些Filter用来实现认证 授权 防范攻击的作用,并且这些Filter按照特定的顺序执行来保证他们在正确的时间被调用,认证的Filter需要再授权的Filter执行之前调用。通常情况你不需要知道Spring Security的Filter的顺序,需要时可查阅FilterOrderRegistration
如下例
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(Customizer.withDefaults())
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults())
.formLogin(Customizer.withDefaults());
return http.build();
}
}
上面的配置会产生Filter的执行顺序如下面所示
| Filter | Added by |
|---|---|
| CsrfFilter | HttpSecurity#csrf |
| UsernamePasswordAuthenticationFilter | HttpSecurity#formLogin |
| BasicAuthenticationFilter | HttpSecurity#httpBasic |
| AuthorizationFilter | HttpSecurity#authorizeHttpRequests |
Note
除了上边的Filter,还有其他的一些Filter,如果你想看一个请求执行了哪些Filter,你可以打印出来
Printing the Security Filters
Filter的列表会在应用启动的时候打印出来,log层级为INFO。你可以在console中看到下面的输出
2023-06-14T08:55:22.321-03:00 INFO 76975 --- [ main] o.s.s.web.DefaultSecurityFilterChain : Will secure any request with [
org.springframework.security.web.session.DisableEncodeUrlFilter@404db674,
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@50f097b5,
org.springframework.security.web.context.SecurityContextHolderFilter@6fc6deb7,
org.springframework.security.web.header.HeaderWriterFilter@6f76c2cc,
org.springframework.security.web.csrf.CsrfFilter@c29fe36,
org.springframework.security.web.authentication.logout.LogoutFilter@ef60710,
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@7c2dfa2,
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@4397a639,
org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@7add838c,
org.springframework.security.web.authentication.www.BasicAuthenticationFilter@5cc9d3d0,
org.springframework.security.web.savedrequest.RequestCacheAwareFilter@7da39774,
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@32b0876c,
org.springframework.security.web.authentication.AnonymousAuthenticationFilter@3662bdff,
org.springframework.security.web.access.ExceptionTranslationFilter@77681ce4,
org.springframework.security.web.access.intercept.AuthorizationFilter@169268a7]
这样你就能看到为每个filter chain的Security Filter。
你可以配置你的应用来为每个请求打印每个filter的执行,这在当你检查你添加的filter是否在请求中执行时非常有用。
logging.level.org.springframework.security=TRACE
Adding a Custom Filter to the Filter Chain
大多数情况下,默认的Security Filter能为你的应用提供足够的安全保障。但Spring Security也提供了加入自定义Filter的功能。
例如,想加一个filter 可以获取tanant id header,并检查当前用户是否有访问这个tenant的权限。通过上面的描述,我们应该知道大概需要将该Filter放在哪个位置上,因为我们需要知道当前的登录用户,所以我们需要把它加载认证的filter之后。
import java.io.IOException;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.AccessDeniedException;
public class TenantFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String tenantId = request.getHeader("X-Tenant-Id"); (1)
boolean hasAccess = isUserAllowed(tenantId); (2)
if (hasAccess) {
filterChain.doFilter(request, response); (3)
return;
}
throw new AccessDeniedException("Access denied"); (4)
}
}
Tip
除了实现Filter,你还继承OncePerRequestFilter,该基类对每次请求只执行一次,还提供了有 HttpServletRequest 和HttpServletResponse参数的doFilterInternal方法。
如下面代码所示,将Filter添加到Security filter chain中
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// ...
.addFilterBefore(new TenantFilter(), AuthorizationFilter.class);
return http.build();
}
通过在AuthorizationFilter前添加该Filter,我们确保了TenantFilter在认证filter后执行。
当你声明你的filter为Spring bean时,无论是通过@Component注解还是在configuration中声明为bean,此时你就需要当心了,因为这样Spring Boot会自动将其注册到Servlet容器中,这会导致Filter执行两次,一次由容器执行,一次由Spring Security执行。
但如果就是想自定义的filter注册为bean,来使用spring 的依赖注入等特性,还想避免重复执行,你可以让spring boot不将该filter注册到容器中,通过声明一个FilterRegistrationBean并设置其enable属性为false:
@Bean
public FilterRegistrationBean<TenantFilter> tenantFilterRegistration(TenantFilter filter) {
FilterRegistrationBean<TenantFilter> registration = new FilterRegistrationBean<>(filter);
registration.setEnabled(false);
return registration;
}
Handling Security Exception
ExceptionTranslationFilter可以将AccessDeniedException 和 AuthenticationException转为HTTP响应。
下图展示了ExceptionTranslationFilter和其他组件之间的关系
-
首先ExceptionTranslationFilter会调用FilterChain.doFilter(request, response)来执行应用的剩余部分
-
如果用户未认证或者是AuthenticationException,就开始认证流程
-
保存
HttpServletRequest以便它可以用来重放原始的请求,当认证成功时 -
AuthenticationEntryPoint 用来请求client的认证信息,例如,重定向到一个login page,或者发送一个
WWW-Authenticateheader
-
要么就是
AccessDeniedException,那么就会调用AccessDeniedHandler来处理访问拒绝的事件
Note
如果应用没抛出AccessDeniedException或者AuthenticationException,那么ExceptionTranslationFilter就什么都没做。
ExceptionTranslationFilter的伪代码如下所示
try {
filterChain.doFilter(request, response);
} catch (AccessDeniedException | AuthenticationException ex) {
if (!authenticated || ex instanceof AuthenticationException) {
startAuthentication();
} else {
accessDenied();
}
}
Saving Request Between Authentication
如在Handling Security Exceptions中介绍的,当一个请求未认证且访问的资源需要认证时,需要将该request保存以便在认证成功后进行再次请求。在Spring Security中,这是通过使用RequestCache的实现来完成保存的
RequestCache
HttpServletRequest是保存在RequestCache中。当用户认证成功时,RequestCache用来重放原始的请求。RequestCacheAwareFilter使用RequestCache来获取已保存的HttpServletRequest 在认证之后,ExceptionTranslationFilter使用RequestCache来保存HttpServletRequest 在检测到AuthenticationException。
默认情况下,使用的是HttpSessionRequestCache,下面展示了如果自定义Request实现只有参数有continue出现是 才会检查HttpSession来获取保存的request。
@Bean
DefaultSecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
HttpSessionRequestCache requestCache = new HttpSessionRequestCache();
requestCache.setMatchingRequestParameterName("continue");
http
// ...
.requestCache((cache) -> cache
.requestCache(requestCache)
);
return http.build();
}
Prevent the Request From Being Saved
可能因为一些原因,你不想存储用户的未认证请求,此时你可以使用 NullRequestCache
@Bean
SecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
RequestCache nullRequestCache = new NullRequestCache();
http
// ...
.requestCache((cache) -> cache
.requestCache(nullRequestCache)
);
return http.build();
}