Spring Security 架构
Filter 过滤器回顾
Spring Security 对 Servlet 的支持基于 Servlet 过滤器,因此通常首先了解过滤器的作用会很有帮助。
客户端向应用程序发送了一个请求,容器创建了一个 FilterChain,其中包含了过滤器和 Servlet,它们应该根据请求 URI 的路径来处理 HttpServletRequest。在 Spring MVC 应用程序中,Servlet 是 DispatcherServlet 的一个实例。一个 Servlet 最多可以处理一个 HttpServletRequest 和 HttpServletResponse。然而,可以使用多个 Filter 来:
- 防止下游的 Filter 或 Servlet 被调用。在这种情况下,Filter 通常会写入 HttpServletResponse
- 修改下游的 Filter 和 Servlet 所使用的 HttpServletRequest 或 HttpServletResponse
1 | public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { |
由于一个 Filter 只影响下游的 Filter 和 Servlet,所以每个 Filter 的调用顺序是非常重要的。
DelegatingFilterProxy 委托过滤代理
Spring 提供了一个名为 DelegatingFilterProxy 的 Filter 实现,允许在 Servlet 容器的生命周期和 Spring 的 ApplicationContext 之间建立桥梁。Servlet 容器允许使用自己的标准来注册过滤器,但它不知道 Spring 定义的 Bean。DelegatingFilterProxy 可以通过标准的 Servlet 容器机制来注册,但将所有工作委托给实现 Filter 的 Spring Bean。
下面是 DelegatingFilterProxy 如何融入 Filter 和 FilterChain 的示例。
DelegatingFilterProxy 从 ApplicationContext 中查找 Bean Filter0,然后调用 Bean Filter0。
DelegatingFilterProxy 的另一个好处是,它允许延迟查找 Filter Bean 实例。这一点很重要,因为容器需要在容器启动之前注册 Filter 实例。然而,Spring 通常使用 ContextLoaderListener 来加载 Spring Bean,直到需要注册 Filter 实例之后 Spring 才会完成。
FilterChainProxy 过滤器链代理
Spring Security 的 Servlet 支持包含在 FilterChainProxy 中。FilterChainProxy 是由 Spring Security 提供的一个特殊的 Filter,它允许通过 SecurityFilterChain 委托许多 Filter 实例。由于 FilterChainProxy 是一个 Bean,它通常被包装在 DelegatingFilterProxy 中。
SecurityFilterChain
SecurityFilterChain 由 FilterChainProxy 使用,以确定该请求应调用哪些 SpringSecurity 过滤器。
SecurityFilterChain 中的 Security Filters 通常是 Bean,但它们是通过 FilterChainProxy 而不是 DelegatingFilterProxy 注册的。与直接向 Servlet 容器或 DelegatingFilterProxy 注册相比,FilterChainProxy 有很多优点。首先,它为 Spring Security 的所有 Servlet 支持提供了一个起点。出于这个原因,如果你试图对 Spring Security 的 Servlet 支持进行故障诊断,在 FilterChainProxy 中添加一个调试点是一个很好的开始。
其次,由于 FilterChainProxy 是 Spring Security 使用的核心,它可以执行一些不被视为可选的任务。例如,它清除了 SecurityContext 以避免内存泄漏。它还应用 Spring Security 的 HttpFirewall 来保护应用程序免受某些类型的攻击。
此外,它在确定何时应该调用 SecurityFilterChain 方面提供了更多的灵活性。在 Servlet 容器中,过滤器的调用仅基于 URL。然而,FilterChainProxy 可以通过利用 RequestMatcher 接口,根据 HttpServletRequest 中的任何内容确定调用。
在多安全过滤链图中,FilterChainProxy 决定应该使用哪个安全过滤链。只有第一个匹配的 SecurityFilterChain 0 才会被调用。假设没有其他的 SecurityFilterChain 实例与之匹配,就会调用 SecurityFilterChain n。
SecurityFilter
SecurityFilter 是通过 SecurityFilterChain API 插入 FilterChainProxy 中的。过滤器的顺序很重要。通常没有必要知道 Spring Security 的过滤器的顺序。然而,有时了解其顺序是有好处的
- ChannelProcessingFilter:确保请求投递到要求渠道。最常见的使用场景就是指定哪些请求必须使用 HTTPS 协议,哪些请求必须使用 HTTP 协议,哪些请求随便使用哪种协议均可。
- ConcurrentSessionFilter
- WebAsyncManagerIntegrationFilter:集成 SecurityContext 到 Spring Web 异步请求机制中的 WebAsyncManager
- SecurityContextPersistenceFilter:每次请求处理之前,从 Session(默认使用 HttpSessionSecurityContextRepository)中获取 SecurityContext,然后将其设置给到 SecurityContextHolder;在请求结束后,就会将 SecurityContextHolder 中存储的 SecurityContext 重新保存到 Session 中,并且清除 SecurityContextHolder 中的 SecurityContext。
- HeaderWriterFilter:该过滤器可为响应添加一些响应头,比如添加 X-Frame-Options,X-XSS-Protection 和 X-Content-Type-Options 等响应头,让浏览器开启保护功能。
- CorsFilter:理跨域资源共享 (CORS)。可以通过 HttpSecurity#cors() 来定制
- CsrfFilter:处理跨站请求伪造 (CSRF)。可以通过 HttpSecurity#csrf() 来开启或关闭。在前后端分离项目中,不需要使用 CSRF。
- LogoutFilter:处理退出登录请求。可以通过 HttpSecurity#logout()来定制退出逻辑
- OAuth2AuthorizationRequestRedirectFilter:用于构建 OAuth 2.0 认证请求,将用户请求重定向到该认证请求接口。
- Saml2WebSsoAuthenticationRequestFilter:基于 SAML 的 SSO 单点登录认证请求过滤器。
- X509AuthenticationFilter:X509 认证过滤器。可以通过 SecurityContext#X509()来启用和配置相关功能
- AbstractPreAuthenticatedProcessingFilter:认证预处理请求过滤器基类,其中认证主体已经由外部系统进行了身份验证。目的只是从传入请求中提取主体上的必要信息,而不是对它们进行身份验证。可以继承该类进行具体实现并通过 HttpSecurity#addFilter 方法来添加个性化
- CasAuthenticationFilter:用于处理 CAS 单点登录认证
- OAuth2LoginAuthenticationFilter:OAuth2.0 登录认证过滤器
- Saml2WebSsoAuthenticationFilter:基于 SAML 的 SSO 单点登录认证过滤器
- UsernamePasswordAuthenticationFilter:用于处理表单登录认证
- ConcurrentSessionFilter:主要用来判断 Session 是否过期以及更新最新访问时间
- OpenIDAuthenticationFilter:基于 OpenID 认证协议的认证过滤器
- DefaultLoginPageGeneratingFilter:如果没有配置登录页面,那么就会默认采用该过滤器生成一个登录表单页面
- DefaultLogoutPageGeneratingFilter:生成默认退出登录页面
- DigestAuthenticationFilter:用于处理 HTTP 头部显示的摘要式身份验证凭据
- BearerTokenAuthenticationFilter:处理 Token 认证
- BasicAuthenticationFilter:用于检测和处理 Http Baisc 认证
- RequestCacheAwareFilter:用于用户认证成功后,重新恢复因为登录被打断的请求
- SecurityContextHolderAwareRequestFilter:对请求对象进行包装,增加了一些安全相关方法
- JaasApiIntegrationFilter:适用于 JAAS (Java 认证授权服务)
- RememberMeAuthenticationFilter:检查请求里有没有包含 remember-me 的 cookie 信息。如果有,则解析出 cookie 里的验证信息,判断是否有权限
- AnonymousAuthenticationFilter:匿名认证过滤器
- OAuth2AuthorizationCodeGrantFilter:OAuth 2.0 授权码模式,用于处理 OAuth 2.0 授权码响应
- SessionManagementFilter:检测用户是否通过认证,如果已认证,就通过 SessionAuthenticationStrategy 进行 Session 相关管理操作
- ExceptionTranslationFilter:可以用于捕获 FilterChain 上所有的异常,但只处理 AccessDeniedException 和 AuthenticationException 异常
- FilterSecurityInterceptor:对 web 资源 进行一些安全保护操作
- SwitchUserFilter:主要用来作用户切换
处理 Security 异常
ExceptionTranslationFilter 允许将 AccessDeniedException 和 AuthenticationException 转换为 HTTP 响应.
ExceptionTranslationFilter 作为安全过滤器之一插入到 FilterChainProxy 中.
- 首先,ExceptionTranslationFilter 调用 FilterChain.doFilter(request, response)来调用应用程序的其他部分。
- 如果用户没有被认证,或者是一个 AuthenticationException,那么就开始认证。
- SecurityContextHolder 被清除掉了
- HttpServletRequest 被保存在 RequestCache 中。当用户成功认证后,RequestCache 被用来重放原始请求。
- AuthenticationEntryPoint 被用来向客户端请求证书。例如,它可能重定向到一个登录页面或发送一个 WWW-Authenticate 头。
- 否则,如果它是一个 AccessDeniedException,那么访问被拒绝。AccessDeniedHandler 被调用来处理拒绝访问。
如果应用程序没有抛出 AccessDeniedException 或 AuthenticationException,那么 ExceptionTranslationFilter 就不会做任何事情。
Spring Security 认证架构组件
Spring Security 用于 Servlet 认证的主要架构组件:
- SecurityContextHolder: 是 Spring Security 存储认证对象详细信息的地方。
- SecurityContext: 从 SecurityContextHolder 中获得,包含了当前被认证用户的认证,一个 Authentication 对象。。
- Authentication: 可以是 AuthenticationManager 的输入,以提供用户提供的认证凭证或来自 SecurityContext 的当前用户。
- GrantedAuthority: 在 Authentication 上授予委托人的权限(即角色、作用域等)。
- AuthenticationManager: 定义 Spring Security 的过滤器如何执行认证的 API。
- ProviderManager: AuthenticationManager 的最常见的实现。
- AuthenticationProvider: 由 ProviderManager 用来执行特定类型的认证。
- AuthenticationEntryPoint: 用于从客户端请求凭证(即重定向到登录页面,发送 WWW-Authenticate 响应,等等)。
- AbstractAuthenticationProcessingFilter: 一个用于认证的基本过滤器。这也给出了一个很好的概念,即认证的高层流程,以及各部分如何协同工作。
SecurityContextHolder
Spring Security 的认证模型的核心是 SecurityContextHolder。它包含 SecurityContext
SecurityContextHolder 是 Spring Security 存储谁被认证的细节的地方。Spring Security 并不关心 SecurityContextHolder 是如何被填充的。如果它包含一个值,那么它就被用作当前认证的用户。
访问当前认证的用户
1 | SecurityContext context = SecurityContextHolder.getContext(); |
默认情况下,SecurityContextHolder 使用 ThreadLocal 来存储这些细节,这意味着 SecurityContext 总是对同一线程中的方法可用,即使 SecurityContext 没有明确作为参数传递给这些方法。如果在处理完当前委托人的请求后注意清除线程,以这种方式使用 ThreadLocal 是相当安全的。Spring Security 的 FilterChainProxy 确保 SecurityContext 总是被清空。
有些应用程序并不完全适合使用 ThreadLocal,因为它们使用线程的方式很特别。例如,一个 Swing 客户端可能希望 Java 虚拟机中的所有线程都使用同一个安全上下文。SecurityContextHolder 可以在启动时配置一个策略,以指定你希望上下文如何被存储。对于一个独立的应用程序,你可以使用 SecurityContextHolder.MODE_GLOBAL 策略。其他应用程序可能想让安全线程所产生的线程也承担相同的安全身份。这可以通过使用 SecurityContextHolder.MODE_INHERITABLETHREADLOCAL 实现。你可以通过两种方式改变默认的 SecurityContextHolder.MODE_THREADLOCAL 的模式。第一个是设置一个系统属性,第二个是调用 SecurityContextHolder 的静态方法。
Authentication
Authentication 在 Spring Security 中主要有两个作用。
- 对 AuthenticationManager 的输入,以提供用户为验证所提供的凭证。在这种情况下使用时,isAuthenticated()返回 false。
- 代表当前认证的用户。当前的 Authentication 可以从 SecurityContext 中获得。
Authentication 包含
- principal - 识别用户。当用用户名 / 密码进行认证时,这通常是 UserDetails 的实例。
- credentials - 通常是密码。在许多情况下,这将在用户被认证后被清除,以确保它不会被泄露。
- authorities - GrantedAuthoritys 是授予用户的高层次权限。可以通过 Authentication.getAuthorities()方法获得。当使用基于用户名 / 密码的认证时,GrantedAuthoritys 通常由 UserDetailsService 加载。
AuthenticationManager
AuthenticationManager 是定义 Spring Security 的过滤器如何执行认证的 API。返回的认证是由调用 AuthenticationManager 的控制器(即 Spring Security 的 Filterss)在 SecurityContextHolder 上设置的。如果你没有与 Spring Security 的 Filterss 集成,你可以直接设置 SecurityContextHolder,不需要使用 AuthenticationManager。
虽然 AuthenticationManager 的实现可以是任何东西,但最常见的实现是 ProviderManager。
ProviderManager
ProviderManager 是最常用的 AuthenticationManager 的实现。ProviderManager 委托给一个 AuthenticationProvider S 列表。每个 AuthenticationProvider 都有机会表明认证应该成功、失败,或者表明它不能做出决定并允许下游的 AuthenticationProvider 做出决定。如果配置的 AuthenticationProviders 都不能认证,那么认证将以 ProviderNotFoundException 失败,这是一个特殊的 AuthenticationException,表明 ProviderManager 没有被配置为支持传入它的认证类型。
在实践中,每个 AuthenticationProvider 都知道如何执行特定类型的认证。例如,一个 AuthenticationProvider 可能能够验证一个用户名 / 密码,而另一个可能能够验证一个 SAML 断言。这允许每个 AuthenticationProvider 做一个非常具体的认证类型,同时支持多种类型的认证,并且只暴露一个 AuthenticationManager Bean。
ProviderManager 还允许配置一个可选的父级 AuthenticationManager,在没有 AuthenticationProvider 可以执行认证的情况下,它将被查阅。父级可以是任何类型的 AuthenticationManager,但它通常是 ProviderManager 的一个实例。
事实上,多个 ProviderManager 实例可能共享同一个父级 AuthenticationManager。这在有多个 SecurityFilterChain 实例的场景中有些常见,这些实例有一些共同的认证(共享的父认证管理器),但也有不同的认证机制(不同的 ProviderManager 实例)。
默认情况下,ProviderManager 将尝试从认证对象中清除任何敏感的凭证信息,该对象由成功的认证请求返回。这可以防止像密码这样的信息在 HttpSession 中保留超过必要的时间。
当你使用用户对象的缓存时,这可能会导致问题,例如,在一个无状态的应用程序中提高性能。如果 Authentication 包含对缓存中的对象的引用(比如 UserDetails 实例),而这个对象的证书被删除了,那么就不可能再对缓存的值进行认证了。如果你使用一个缓存,你需要考虑到这一点。一个显而易见的解决方案是先制作一个对象的副本,可以在缓存实现中,也可以在创建返回的认证对象的 AuthenticationProvider 中。另外,你可以禁用 ProviderManager 上的 eraseCredentialsAfterAuthentication 属性。更多信息请参见 Javadoc。
AuthenticationProvider
多个 AuthenticationProviders 可以被注入到 ProviderManager 中。每个 AuthenticationProvider 都执行一种特定类型的认证。例如,DaoAuthenticationProvider 支持基于用户名 / 密码的认证,而 JwtAuthenticationProvider 支持认证 JWT 令牌。
DaoAuthenticationProvider 是 AuthenticationProvider 实现, 它利用 UserDetailsService 和 PasswordEncoder 对用户名和密码进行身份验证.
AuthenticationEntryPoint
AuthenticationEntryPoint 用于发送 HTTP 响应,请求客户端的凭证。
有时,客户端会主动包含诸如用户名 / 密码之类的凭证来请求资源。在这些情况下,Spring Security 不需要提供要求客户端提供证书的 HTTP 响应,因为它们已经被包括在内。
在其他情况下,客户端将向他们未被授权访问的资源发出未经认证的请求。在这种情况下,AuthenticationEntryPoint 的实现被用来请求客户端的凭证。AuthenticationEntryPoint 的实现可以执行重定向到一个登录页面,用 WWW-Authenticate 头来响应,等等。
AbstractAuthenticationProcessingFilter
AbstractAuthenticationProcessingFilter 被用作验证用户凭证的基本过滤器。在认证凭证之前,Spring Security 通常使用 AuthenticationEntryPoint 请求凭证。
接下来,AbstractAuthenticationProcessingFilter 可以对提交给它的任何认证请求进行认证。
- 当用户提交其凭据时,AbstractAuthenticationProcessingFilter 会从 HttpServletRequest 中创建一个 Authentication。创建的身份验证类型取决于 AbstractAuthenticationProcessingFilter 的子类。例如,UsernamePasswordAuthenticationFilter 从 HttpServletRequest 中提交的用户名和密码创建一个 UsernamePasswordAuthenticationToken。
- 接下来,该认证被传递到 AuthenticationManager 中进行认证
- 如果认证失败,那么
- SecurityContextHolder 被清除掉。
- RememberMeServices.loginFail 被调用。如果 RememberMeServices 未配置, 则为空.
- AuthenticationFailureHandler 被调用。
- 如果认证成功,那么
- SessionAuthenticationStrategy 被通知有新的登录。
- Authentication 被设置在 SecurityContextHolder 上。之后 SecurityContextPersistenceFilter 将 SecurityContext 保存到 HttpSession 中。
- RememberMeServices.loginSuccess 被调用。如果 RememberMeServices 未配置,则为空。
- ApplicationEventPublisher 发布一个 InteractiveAuthenticationSuccessEvent。
- AuthenticationSuccessHandler 被调用。
Spring Security 授权架构
Authorities
所有的 Authentication 实现都存储了 GrantedAuthority 列表. 这些代表已授予主体的权限. GrantedAuthority 对象由 AuthenticationManager 插入 Authentication 对象, 并在以后做出授权决策时由 AccessDecisionManager 读取.
GrantedAuthority 接口只有一个方法:
1 | String getAuthority(); |
此方法使 AccessDecisionManager 可以获取 GrantedAuthority 的精确 String 表示形式. 通过以字符串形式返回, 大多数 AccessDecisionManager 都可以轻松地读取 GrantedAuthority. 如果 GrantedAuthority 无法精确地表示为 String, 则 GrantedAuthority 被视为 “complex” , 并且 getAuthority() 必须返回 null.
“complex” GrantedAuthority 的 示例将是一种实现, 该实现存储适用于不同客户帐号的一系列操作和权限阈值. 将复杂的 GrantedAuthority 表示为 String 会非常困难, 因此 getAuthority() 方法应返回 null. 这将向任何 AccessDecisionManager 指示它将需要特别支持 GrantedAuthority 实现, 以便理解其内容.
Spring Security 包含一个具体的 GrantedAuthority 实现, 即 SimpleGrantedAuthority. 这允许将任何用户指定的 String 转换为 GrantedAuthority. 安全体系结构中包含的所有 AuthenticationProvider 都使用 SimpleGrantedAuthority 来填充 Authentication 对象.
AuthorizationManager
AuthorizationManager 取代了 AccessDecisionManager 和 AccessDecisionVoter。
AuthorizationManagers 被 AuthorizationFilter 调用,负责做出最终的访问控制决策。该接口包含两个方法
1 | AuthorizationDecision check(Supplier<Authentication> authentication, Object secureObject); |
AuthorizationManager 的 check 方法被传递给它所需要的所有相关信息,以便做出授权决定。特别是,传递安全对象使那些包含在实际安全对象调用中的参数能够被检查到。例如,让我们假设安全对象是一个 MethodInvocation。查询 MethodInvocation 的任何客户参数是很容易的,然后在 AuthorizationManager 中实现某种安全逻辑以确保委托人被允许对该客户进行操作。如果访问被授予,实现应返回正的 AuthorizationDecision,如果访问被拒绝,应返回负的 AuthorizationDecision,如果不作出决定,则返回空的 AuthorizationDecision。
verify 调用 check,随后在出现负的授权决定时抛出 AccessDeniedException。
基于代理的 AuthorizationManager 实现
虽然用户可以实现他们自己的 AuthorizationManager 来控制授权的所有方面,但 Spring Security 提供了一个委托的 AuthorizationManager,可以与个别的 AuthorizationManagers 协作。
RequestMatcherDelegatingAuthorizationManager 将把请求与最合适的 Delegate AuthorizationManager 相匹配。对于方法安全,你可以使用 AuthorizationManagerBeforeMethodInterceptor 和 AuthorizationManagerAfterMethodInterceptor。
AuthorizationManager 的实现类:
AuthorityAuthorizationManager
Spring Security 提供的最常见的 AuthorizationManager 是 AuthorizationManager。它被配置为在当前认证中寻找一组给定的授权。如果认证包含任何配置的授权,它将返回正面的 AuthorizationDecision。否则,它将返回一个负面的授权决定。
AuthenticatedAuthorizationManager
另一个管理器是 AuthenticatedAuthorizationManager。它可以用来区分匿名、完全认证和记住我认证的用户。许多网站在 Remember-me 认证下允许某些有限的访问,但要求用户通过登录来确认他们的身份以获得完整的访问。
自定义 AuthorizationManager
很明显,你也可以实现一个自定义的 AuthorizationManager,你可以把你想要的任何访问控制逻辑放在里面。它可能是针对你的应用程序的(与业务逻辑有关),也可能实现一些安全管理逻辑。例如,你可以创建一个可以查询 Open Policy Agent 或你自己的授权数据库的实现。
分层的角色
使用角色层次结构允许你配置哪些角色(或权限)应该包括其他人。Spring Security 的 RoleVoter 的一个扩展版本,RoleHierarchyVoter,被配置了一个 RoleHierarchy,它从其中获得了用户被分配的所有 “可到达的授权”。一个典型的配置可能看起来像这样。
1 |
|
在这里,我们在一个层次结构中有四个角色 ROLE_ADMIN ⇒ ROLE_STAFF ⇒ ROLE_USER ⇒ ROLE_GUEST。一个被认证为 ROLE_ADMIN 的用户,当安全约束被评估为适应调用上述 RoleHierarchyVoter 的 AuthorizationManager 时,将表现得好像他们拥有所有四个角色。> 符号可以被认为是 “包括 “的意思。
Spring Security 基本使用
在 Spring MVC 中,Spring Security 的基础示例
1 |
|
- UserDetails:由 UserDetailsService 返回. DaoAuthenticationProvider 验证 UserDetails, 然后返回 Authentication , 该身份验证的主体是已配置的 UserDetailsService 返回的 UserDetails.
- UserDetailsService:DaoAuthenticationProvider 使用 UserDetailsService 检索用户名, 密码和其他用于使用用户名和密码进行身份验证的属性。可以通过将自定义 UserDetailsService 暴露为 bean 来定义自定义身份验证.
- PasswordEncoder:Spring Security 的 servlet 支持与 PasswordEncoder 集成来安全地存储密码. 可以通过 暴露一个 PasswordEncoder Bean 来定制 Spring Security 使用的 PasswordEncoder 实现.
Spring Security 的自定义配置类入口是 WebSecurityConfigurerAdapter
,此类中提供了默认配置,主要提供了三个配置方法:
configure(AuthenticationManagerBuilder auth):认证管理器配置方法
使用参数 AuthenticationManagerBuilder 就可以构建 AuthenticationManager。比如配置 UserDetails、PasswordEncoder
configure(WebSecurity web):核心过滤器配置方法
主要用于配置放行静态资源
configure(HttpSecurity http):安全过滤器链配置方法
对请求进行自定义配置安全访问策略
1 | protected void configure(HttpSecurity http) throws Exception { |
可以通过将多个子级添加到 http.authorizeRequests()方法中来为 URL 指定自定义要求。例如:
1 | protected void configure(HttpSecurity http) throws Exception { |