注:本文基于
Spring Boot 3.2.1
以及Spring Security 6.2.1
Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC、DI(依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
此外,Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架,致力于为Java应用程序提供身份验证和授权的能力。
其两大核心功能是用户认证和用户授权。用户认证是验证某个用户是否为系统中的合法主体,即用户能否访问该系统,一般要求用户提供用户名和密码。用户授权则是验证某个用户是否有权限执行某个操作。
Spring Security的前身是Acegi Security,是Spring项目组中用来提供安全认证服务的框架。Spring Security为JavaEE企业级开发提供了全面的安全防护,采用“安全层”的概念,使每一层都尽可能安全,连续的安全层可以达到全面的防护。Spring Security可以在Controller层、Service层、DAO层等以加注解的方式来保护应用程序的安全。
请注意,一个系统的安全还需要考虑传输层和系统层的安全,例如采用Https协议、服务器部署防火墙等。此外,将工程重新部署到一个新的服务器上时,不需要为Spring Security做额外的工作。
参考 Spring Security Architecture
Spring Security的Servlet支持基于Servlet过滤器,因此首先查看过滤器的作用通常会很有帮助。下图展示了单个HTTP请求的处理程序的典型分层。
客户端发送请求到应用程序,容器创建一个FilterChain,其中包含Filter
实例和应该处理HttpServletRequest
的Servlet,基于请求URI的路径。
在Spring MVC应用程序中,Servlet是DispatcherServlet
的一个实例。最多只能有一个Servlet处理单个HttpServletRequest
和HttpServletResponse
。然而,可以使用多个Filter
来:
Filter
实例或Servlet
。在这种情况下,Filter
通常会写入HttpServletResponse
。Filter
实例和Servlet
使用的HttpServletRequest
或HttpServletResponse
。Filter的强大之处在于传递给它的FilterChain。
Spring提供了一个名为DelegatingFilterProxy
的Filter实现,允许在Servlet容器的生命周期和Spring的ApplicationContext之间建立桥接。Servlet容器允许使用自己的标准注册Filter实例,但它不知道Spring定义的Bean。你可以通过标准的Servlet容器机制注册DelegatingFilterProxy
,但将所有工作委托给实现了Filter
接口的Spring Bean。
以下是DelegatingFilterProxy如何适配到Filter实例和FilterChain的示意图。
Spring Security的Servlet支持包含在FilterChainProxy
中。FilterChainProxy
是Spring Security提供的一个特殊Filter,通过SecurityFilterChain
允许委派给多个Filter实例。由于FilterChainProxy
是一个Bean,通常会被包装在DelegatingFilterProxy
中。
SecurityFilterChain
被FilterChainProxy
用于确定哪些Spring Security Filter实例应该被调用处理当前请求。
以下图片展示了SecurityFilterChain
的作用。
SecurityFilterChain
中的安全过滤器通常是Bean,但它们是通过FilterChainProxy
注册的,而不是通过DelegatingFilterProxy。FilterChainProxy
相比直接注册到Servlet容器或DelegatingFilterProxy具有许多优势。首先,它为所有Spring Security的Servlet支持提供了一个起点。因此,如果您尝试排查Spring Security的Servlet支持问题,在FilterChainProxy
中添加一个调试点是一个很好的起点。
其次,由于FilterChainProxy
对于Spring Security的使用至关重要,它可以执行一些被视为必要的任务。例如,它清除SecurityContext以避免内存泄漏。它还应用了Spring Security的HttpFirewall
来保护应用程序免受某些类型的攻击。
此外,它在确定何时调用SecurityFilterChain
时提供了更多灵活性。在Servlet容器中,Filter实例仅基于URL进行调用。但是,FilterChainProxy
可以使用RequestMatcher接口根据HttpServletRequest中的任何内容确定调用。
以下图片显示了多个SecurityFilterChain实例:
Security Filters通过SecurityFilterChain API插入到FilterChainProxy中。这些过滤器可以用于许多不同的目的,如身份验证、授权、防范利用漏洞等。这些过滤器按特定顺序执行,以确保它们在正确的时间被调用,例如,执行身份验证的过滤器应该在执行授权的过滤器之前被调用。通常情况下,不需要知道Spring Security的过滤器的顺序。然而,有时候知道顺序是有益的,如果你想知道它们,可以查看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 | Added by |
---|---|
CsrfFilter | HttpSecurity#csrf |
UsernamePasswordAuthenticationFilter | HttpSecurity#formLogin |
BasicAuthenticationFilter | HttpSecurity#httpBasic |
AuthorizationFilter | HttpSecurity#authorizeHttpRequests |
首先,CsrfFilter被调用以防止跨站请求伪造(CSRF)攻击。
其次,身份验证过滤器被调用以对请求进行身份验证。
最后,AuthorizationFilter被调用以授权请求。
大多数情况下,默认的安全过滤器足以为您的应用程序提供安全保障。然而,有时您可能想要将自定义过滤器添加到安全过滤器链中。
例如,假设您想要添加一个过滤器,该过滤器获取租户ID标头并检查当前用户是否有权限访问该租户。前面的描述已经为我们提供了在哪里添加过滤器的线索,因为我们需要知道当前用户,所以我们需要在身份验证过滤器之后添加它。
首先,让我们创建过滤器:
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)
}
}
不需要实现Filter
接口,您可以扩展OncePerRequestFilter
,它是一种基类过滤器,每个请求只调用一次,并提供了带有HttpServletRequest
和HttpServletResponse
参数的doFilterInternal
方法。
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.filter.OncePerRequestFilter;
public class CustomFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// Your custom logic here
// For example, you can access request headers, retrieve the tenant id header,
// and check if the current user has access to that tenant
// Proceed to the next filter in the chain
filterChain.doFilter(request, response);
}
}
现在,我们需要将过滤器添加到安全过滤器链中。
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// ...
.addFilterBefore(new TenantFilter(), AuthorizationFilter.class);
return http.build();
}
异常转换过滤器(ExceptionTranslationFilter)允许将AccessDeniedException
和AuthenticationException
转换为HTTP响应。
异常转换过滤器被插入到FilterChainProxy
中作为安全过滤器之一。
以下图片展示了异常转换过滤器与其他组件的关系:
第一,异常转换过滤器调用FilterChain.doFilter(request, response)
来调用应用程序的其余部分。
第二,如果用户未经过身份验证或者出现了AuthenticationException
,则开始进行身份验证。
SecurityContextHolder
被清除。HttpServletRequest
被保存,以便在身份验证成功后重新执行原始请求。AuthenticationEntryPoint
请求客户端提供凭据。例如,它可能重定向到登录页面或发送WWW-Authenticate头部。第三,否则,如果是AccessDeniedException
,则拒绝访问。调用AccessDeniedHandler
来处理访问被拒绝的情况。
参考 Authentication
认证机制
这些机制为开发者提供了多种身份验证选项,以满足不同应用的安全需求和用户体验要求。例如,对于大多数基于 Web 的应用程序,基本的用户名和密码验证是足够的。然而,对于需要第三方身份验证提供者支持的应用程序,OAuth 2.0 和 OpenID Connect 是理想的选择。SAML 2.0 通常用于企业环境中的单点登录(SSO)解决方案。
“记住我”功能通过在用户浏览器中存储持久化令牌来实现,使用户在关闭浏览器或会话过期后无需重新登录即可保持登录状态。这可以提高用户体验,但也可能增加安全风险,因此应谨慎实现。
JAAS(Java Authentication and Authorization Service)是 Java 平台提供的一个安全框架,它允许开发者灵活地实现身份验证和授权。预身份验证场景是指使用外部的身份验证机制(如 SiteMinder 或 Java EE 安全性),然后再利用 Spring Security 进行授权和防止常见的安全漏洞。
最后,X509 身份验证是一种基于公钥和私钥证书的身份验证机制,通常用于需要高级别安全保证的场景,如 SSL/TLS 通信。
总的来说,这些身份验证机制为 Spring Security 提供了强大的功能,使开发者能够根据应用的需求和安全要求选择最适合的身份验证方式。
参考 Servlet Authentication Architecture
这个讨论扩展了关于Servlet安全性的概述,以描述在Servlet身份验证中使用的Spring Security的主要架构组件。如果你需要具体的流程来解释这些组件如何协同工作,请查看关于身份验证机制的特定部分。
SecurityContextHolder
- SecurityContextHolder是Spring Security存储已认证用户详细信息的地方。SecurityContext
- 从SecurityContextHolder获取,包含当前已认证用户的身份验证信息。Authentication
- 可以是提供给AuthenticationManager的输入,以提供用户为进行身份验证所提供的凭据,或者是从SecurityContext中获取的当前用户。GrantedAuthority
- 在身份验证上授予主体的权限(例如,角色、范围等)。AuthenticationManager
- 定义Spring Security的过滤器如何执行身份验证的API。ProviderManager
- AuthenticationManager的最常见实现。AuthenticationProvider
- 由ProviderManager使用来执行特定类型的身份验证。AbstractAuthenticationProcessingFilter
- 用于身份验证的基础过滤器。这也很好地说明了身份验证的高级流程和各个组件如何协同工作。这些组件共同构成了Spring Security在Servlet身份验证中的核心架构。通过理解每个组件的作用和它们如何相互协作,开发者可以更好地配置和实现适合其应用程序需求的身份验证策略。
Spring Security的认证模型的核心是SecurityContextHolder。SecurityContextHolder包含了SecurityContext。
默认情况下,SecurityContextHolder
使用ThreadLocal
来存储这些细节,这意味着对于同一个线程中的方法,SecurityContext总是可用的,即使SecurityContext没有被明确地作为参数传递给这些方法。如果你注意在处理当前主体的请求后清理线程,这样使用ThreadLocal是相当安全的。Spring Security的FilterChainProxy确保SecurityContext总是被清除。
由于某些应用程序与线程的工作方式特定,因此它们可能不完全适合使用ThreadLocal。例如,Swing客户端可能希望Java虚拟机中的所有线程都使用相同的安全上下文。你可以在启动时配置SecurityContextHolder
的策略,以指定你希望如何存储上下文。对于独立应用程序,你会使用SecurityContextHolder.MODE_GLOBAL
策略。其他应用程序可能希望由安全线程生成的新线程也采用相同的安全标识。你可以通过使用SecurityContextHolder.MODE_INHERITABLETHREADLOCAL
来实现这一点。你可以通过两种方式从默认的SecurityContextHolder.MODE_THREADLOCAL
更改模式。第一种是设置系统属性。第二种是调用SecurityContextHolder上的静态方法。大多数应用程序无需更改默认值。
可以通过如下图所示配置JVM参数-Dspring.security.strategy=MODE_GLOBAL
来配置SecurityContextHolder
的策略
SecurityContext是从SecurityContextHolder中获取的。SecurityContext包含一个Authentication对象。
在Spring Security中,Authentication接口主要有两个用途:
作为AuthenticationManager的输入,提供用户为进行身份验证而提供的凭据。在这种情况下,isAuthenticated()返回false。
代表当前已验证的用户。你可以从SecurityContext中获取当前的Authentication。
Authentication包含以下信息:
principal:标识用户。当使用用户名/密码进行身份验证时,这通常是UserDetails的实例。
credentials:通常是密码。在许多情况下,用户在身份验证后此字段会被清除,以确保不会泄露密码。
authorities:GrantedAuthority实例表示用户被授予的高级权限。两个例子是角色和范围。
GrantedAuthority
实例是用户被授予的高级权限。
你可以通过Authentication.getAuthorities()
方法获取GrantedAuthority
实例。这个方法提供了一组GrantedAuthority对象的集合。GrantedAuthority,顾名思义,是授予主体的权限。这些权限通常是“角色”,如ROLE_ADMINISTRATOR或ROLE_HR_SUPERVISOR。这些角色随后被配置用于Web授权、方法授权和域对象授权。Spring Security的其他部分会解释这些权限并期望它们存在。在使用基于用户名/密码的身份验证时,GrantedAuthority实例通常由UserDetailsService
加载。
通常,GrantedAuthority对象是应用程序级别的权限。它们不特定于给定的域对象。因此,你不太可能有一个GrantedAuthority来表示对Employee对象编号54的权限,因为如果有数千个这样的权限,你会很快耗尽内存(或者至少会导致应用程序花费很长时间来验证用户)。当然,Spring Security是专门设计来处理这种常见需求的,但你应该使用项目的域对象安全功能来满足这一目的。
AuthenticationManager是定义Spring Security过滤器如何执行身份验证的API。控制器(即Spring Security过滤器实例)在调用AuthenticationManager后,会将返回的Authentication设置到SecurityContextHolder中。如果你不与Spring Security过滤器实例集成,你可以直接设置SecurityContextHolder,并不需要使用AuthenticationManager。
虽然AuthenticationManager的实现可以是任何形式,但最常见的实现是ProviderManager。
ProviderManager是AuthenticationManager最常用的实现。ProviderManager委托给一组AuthenticationProvider实例。每个AuthenticationProvider都有机会表示身份验证应该成功、失败,或者表示无法做出决定并让下游的AuthenticationProvider来决定。如果配置的所有AuthenticationProvider实例都无法进行身份验证,那么身份验证将失败并抛出ProviderNotFoundException异常,这是一个特殊的AuthenticationException异常,表示ProviderManager没有被配置为支持传入的Authentication类型。
AbstractAuthenticationProcessingFilter
是一个基础过滤器,用于验证用户的凭证。在凭证可以被验证之前,Spring Security 通常使用 AuthenticationEntryPoint
来请求这些凭证。
接下来,AbstractAuthenticationProcessingFilter
可以验证提交给它的任何身份验证请求。
1)当用户提交他们的凭证时,AbstractAuthenticationProcessingFilter会从HttpServletRequest中创建一个Authentication对象以进行身份验证。创建的Authentication类型取决于AbstractAuthenticationProcessingFilter的子类。例如,UsernamePasswordAuthenticationFilter会从HttpServletRequest中提交的用户名和密码创建一个UsernamePasswordAuthenticationToken。
2)接下来,Authentication对象会被传递给AuthenticationManager进行身份验证。
3)如果身份验证失败,则进入失败处理流程。
SecurityContextHolder会被清空。
RememberMeServices.loginFail会被调用。如果未配置“记住我”功能,则此操作为空操作。请参见rememberme包。
AuthenticationFailureHandler会被调用。请参见AuthenticationFailureHandler接口。
4)如果身份验证成功,则进入成功处理流程。
SessionAuthenticationStrategy会收到新的登录通知。请参见SessionAuthenticationStrategy接口。
Authentication对象会被设置在SecurityContextHolder上。之后,如果你需要保存SecurityContext以便它可以在未来的请求中自动设置,必须显式调用SecurityContextRepository#saveContext。请参见SecurityContextHolderFilter类。
RememberMeServices.loginSuccess会被调用。如果未配置“记住我”功能,则此操作为空操作。请参见rememberme包。
ApplicationEventPublisher会发布一个InteractiveAuthenticationSuccessEvent事件。
AuthenticationSuccessHandler会被调用。请参见AuthenticationSuccessHandler接口。
在更详细的流程中:
AuthenticationEntryPoint: 当一个未经过身份验证的请求到达Spring Security时,AuthenticationEntryPoint
会被调用。它通常负责引导用户到登录页面或返回特定的HTTP状态码(如401 Unauthorized),以便客户端知道需要提交身份验证凭证。
提交凭证: 一旦用户通过登录表单或其他方式提供了凭证(如用户名和密码),这些凭证会被发送到服务器。这通常是通过一个POST请求到特定的URL(如 /login
)来完成的。
AbstractAuthenticationProcessingFilter: 这个过滤器被配置为拦截包含身份验证凭证的请求,并处理它们。它会尝试从请求中提取凭证(如从表单字段中获取用户名和密码),然后使用这些凭证来创建一个 Authentication
对象。然后,它会将 Authentication
对象传递给一个或多个 AuthenticationProvider
以进行验证。
AuthenticationProvider: AuthenticationProvider
负责验证 Authentication
对象中的凭证。它可能会查询数据库、LDAP服务器或其他数据存储来验证提供的用户名和密码是否匹配。如果凭证有效,AuthenticationProvider
将返回一个完全填充的、经过身份验证的 Authentication
对象。
SecurityContextHolder: 一旦 Authentication
对象被验证,它就会被设置到 SecurityContextHolder
中,这是一个线程内对象,存储了关于当前用户身份验证的信息。这允许后续的过滤器或控制器能够访问这个信息,以便根据用户的身份来执行授权决策。
总结来说,AbstractAuthenticationProcessingFilter
在Spring Security中扮演着关键角色,它处理用户提交的凭证,并将它们传递给 AuthenticationProvider
进行验证。如果验证成功,用户的身份验证状态将被存储在 SecurityContextHolder
中,供后续使用。