简介
Spring Security:是一个提供身份验证,授权和保护以防止常见攻击的框架。 凭借对命令式和反应式应用程序的一流支持,它为Spring应用程序的安全提供实际标准。
特性
Spring Security为身份验证,授权和针对常见漏洞的防护提供了全面的支持。 它还集成了三方库,以简化其使用。
认证
Spring Security为身份验证提供了全面的支持。 身份验证是我们验证谁试图访问特定资源的身份的方法。 验证用户身份的常用方法是要求用户输入用户名和密码。 一旦执行了身份验证,我们就会知道身份并可以执行授权。
认证支持
Spring Security提供了用于验证用户的内置支持。 分为两大主体:基于Servlet和WebFlux的身份验证。
密码加密存储
Spring Security的PasswordEncoder接口用于对密码进行单向转换,以使密码可以安全地存储。
基于PasswordEncoder接口的实现类如下:
DelegatingPasswordEncoder:即委托密码编码器,兼容多种不同加密方式存储密码。主要用于新旧数据的加密方式的兼容,做到平滑迁移,例如旧数据使用NoOpPasswordEncoder,新数据使用BCryptPasswordEncoder加密。
BCryptPasswordEncoder:基于bcrypt算法的编码器,为了使其更能抵御密码破解,bcrypt故意降低了速度,与其他自适应单向功能一样,应将其调整为大约1秒钟,以验证系统上的密码。BCryptPasswordEncoder的默认实现使用强度10。
Argon2PasswordEncoder:基于 Argon2算法的编码器,Argon2是“密码哈希竞赛”的获胜者。 为了克服自定义硬件上的密码破解问题,Argon2是一种故意慢速的算法,需要大量内存。 与其他自适应单向功能一样,应将其调整为大约1秒钟,以验证系统上的密码。 Argon2PasswordEncoder的当前实现需要BouncyCastle。
Pbkdf2PasswordEncoder:基于 PBKDF2算法的编码器,为了克服密码破解问题,PBKDF2是一种故意缓慢的算法。 与其他自适应单向功能一样,应将其调整为大约1秒钟,以验证系统上的密码。 当需要FIPS认证时,此算法是一个不错的选择。
SCryptPasswordEncoder:基于 scrypt算法的编码器, 为了克服自定义硬件scrypt上的密码破解问题,这是一种故意缓慢的算法,需要大量内存。 与其他自适应单向功能一样,应将其调整为大约1秒钟,以验证系统上的密码。
防止漏洞利用
CSRF:Cross Site Request Forgery,即跨站请求伪造。Spring提供了两种机制来防御CSRF攻击:1、基于token表单验证(表单增加_csrf字段);2、在会话Cookie上指定SameSite属性;
-
安全的HTTP响应头:
- Cache Control:Spring Security的默认禁用缓存以保护用户的内容,
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
; - Content Type Options:禁用内容嗅探
X-Content-Type-Options: nosniff
防止XSS攻击; - HTTP Strict Transport Security (HSTS):严格的安全传输HTTP,将http请求自动转为https请求,
Strict-Transport-Security: max-age=31536000 ; includeSubDomains ; preload
; - X-Frame-Options:Spring Security默认禁用iframe页面,
X-Frame-Options: DENY
; - X-XSS-Protection:通常浏览器在检测到XSS攻击时应采取的措施。 例如,过滤器可能会尝试以最小侵入性的方式更改内容以仍然呈现所有内容。 有时,这种替换本身可能会成为XSS漏洞。 相反,最好是阻止内容,而不要尝试对其进行修复。 Spring Security默认阻止内容:
X-XSS-Protection: 1; mode=block
; - Content Security Policy (CSP):内容安全策略是Web应用程序可以用来缓解诸如跨站点脚本(XSS)之类的内容注入漏洞的机制。
- Referrer Policy:
Referrer Policy: strict-origin-when-cross-origin
;
- Cache Control:Spring Security的默认禁用缓存以保护用户的内容,
认证原理分析(Servlet)
过滤器
DelegatingFilterProxy
Spring提供了一个名为DelegatingFilterProxy的Filter实现,可以在Servlet容器的生命周期和Spring的ApplicationContext之间进行桥接。 Servlet容器允许使用自己的标准注册Filters,但是它不知道Spring定义的Bean。 DelegatingFilterProxy可以通过标准的Servlet容器机制进行注册,然后将所有工作委托给实现Filter的Spring Bean。
如上图所示,DelegatingFilterProxy是一个标准的Servlet Filter,当调用链路到DelegatingFilterProxy,DelegatingFilterProxy会找到达Spring管理的Filter,然后发起调用。
FilterChainProxy
如上图所示,FilterChainProxy是Spring Security提供的特殊过滤器,允许通过SecurityFilterChain委派许多过滤器实例。
SecurityFilterChain
如上图所示,FilterChainProxy使用SecurityFilterChain确定应对此请求调用哪些Spring Security过滤器。
Security Filter是注册到FilterChainProxy而不是DelegatingFilterProxy的。原因如下:
- 它为Spring Security的所有Servlet支持提供了一个起点。如果想对Spring Security的Servlet支持进行故障排除,那么在FilterChainProxy中添加调试点是一个很好的起点;
- 它清除SecurityContext以避免内存泄漏;
- 它使用Spring Security的HttpFirewall来保护应用程序免受某些类型的攻击;
- 它在确定何时应调用SecurityFilterChain时提供了更大的灵活性。 在Servlet容器中,仅根据URL调用过滤器。 但是,FilterChainProxy可以利用RequestMatcher接口,根据HttpServletRequest中的任何内容确定调用。
-
FilterChainProxy可用于确定应使用哪个SecurityFilterChain。 这允许应用程序的不同部分提供完全独立的配置。
如上图所示,FilterChainProxy利用RequestMatcher接口确定调用哪个SecurityFilterChain。举个例子:如果访问/api/getMsg,则调用SecurityFilterChain0。
Security Filter
Spring Security提供了以下Security Filter(包含顺序,通过FilterComparator配置相关顺序):
- ChannelProcessingFilter
- WebAsyncManagerIntegrationFilter
- SecurityContextPersistenceFilter
- HeaderWriterFilter
- CorsFilter
- CsrfFilter
- LogoutFilter
- OAuth2AuthorizationRequestRedirectFilter
- Saml2WebSsoAuthenticationRequestFilter
- X509AuthenticationFilter
- AbstractPreAuthenticatedProcessingFilter
- CasAuthenticationFilter
- OAuth2LoginAuthenticationFilter
- Saml2WebSsoAuthenticationFilter
- UsernamePasswordAuthenticationFilter
- OpenIDAuthenticationFilter
- DefaultLoginPageGeneratingFilter
- DefaultLogoutPageGeneratingFilter
- ConcurrentSessionFilter
- DigestAuthenticationFilter
- BearerTokenAuthenticationFilter
- BasicAuthenticationFilter
- RequestCacheAwareFilter
- SecurityContextHolderAwareRequestFilter
- JaasApiIntegrationFilter
- RememberMeAuthenticationFilter
- AnonymousAuthenticationFilter
- OAuth2AuthorizationCodeGrantFilter
- SessionManagementFilter
- ExceptionTranslationFilter
- FilterSecurityInterceptor
- SwitchUserFilter
异常处理
如上图表示的是异常处理过滤器ExceptionTranslationFilter的工作原理:
- ExceptionTranslationFilter 调用 FilterChain.doFilter(request, response),抛出Security Exception;
- 如果用户未通过身份验证或它是AuthenticationException,则启动身份验证:
- 清除SecurityContextHolder;
- 将HttpServletRequest保存在RequestCache中;
- 进入AuthenticationEntryPoint(重定向到登录页或者返回401);
- 另外一种异常就是,访问被拒绝,则交给AccessDeniedHandler来处理。
private void handleSpringSecurityException(HttpServletRequest request, HttpServletResponse response,
FilterChain chain, RuntimeException exception) throws IOException, ServletException {
if (exception instanceof AuthenticationException) {
handleAuthenticationException(request, response, chain, (AuthenticationException) exception);
}
else if (exception instanceof AccessDeniedException) {
handleAccessDeniedException(request, response, chain, (AccessDeniedException) exception);
}
}
认证
认证相关组件
SecurityContextHolder:用于存储SecurityContext;
SecurityContext:用于存储Authentication;
- Authentication:用户存储用户认证详细信息;
该图为SecurityContextHolder、SecurityContext、Authentication三者之间的关系。
AuthenticationManager:统一管理执行身份验证;
ProviderManager:AuthenticationManager最常见的实现;
-
AuthenticationProvider:用于执行特定类型的身份验证,由ProviderManager统一管理。
如上图所示,ProviderManager作为AuthenticationManager最常见的实现,ProviderManager认证时,将认证逻辑委托给AuthenticationProvider列表,不同AuthenticationProvider的执行不同的认证逻辑。
如果没有AuthenticationProvider可以执行身份验证,使用该父AuthenticationManager(通常是ProviderManager实例)进行认证。
一个父AuthenticationManager可能存在多个ProviderManager实例。 即具有相同身份验证(共享父AuthenticationManager)但又具有不同身份验证机制(不同ProviderManager实例)。
AuthenticationEntryPoint:认证入口,用户未登录时,重定向到登录页面或者发送401、WWW-Authenticate响应头;ExceptionTranslationFilter 抛出AuthenticationException的时候,调用sendStartAuthentication方法时进入AuthenticationEntryPoint逻辑。
- AbstractAuthenticationProcessingFilter:作为身份验证的基本过滤器(formLogin表单登录)。 当用户提交身份凭据(例如,账号密码)时过滤。
上图表示用户提交身份凭据的认证逻辑:
- 假设用户在登录页提交表单(账号密码)进行登录;
- AbstractAuthenticationProcessingFilter判断该请求是提交身份凭据认证请求,则调用attemptAuthentication方法也就是上图中的①;
- attemptAuthentication将认证逻辑委托给AuthenticationManager就是上图中的②;
- 认证失败会做一些清理动作就是上图中的③;
- 认证成功,作Session相关的逻辑、将Authentication存储到SecurityContextHolder、RememberMe相关处理、认证成功事件的发布、认证成功的后置处理。
- SessionAuthenticationStrategy:旨在身份验证时对HttpSession相关行为进行管理,如会话固定保护,csrf防护,session并发控制等。
如上图所示,两种登录方式:UsernamePasswordAuthenticationFilter(基于表单)与BasicAuthenticationFilter(基于Rest)。有两条请求链路:
- 客户端请求经过UsernamePasswordAuthenticationFilter过滤器,验证用户登录凭证后,调用SessionAuthenticationStrategy.onAuthentication()基于session的验证;
-
客户端请求经过BasicAuthenticationFilter过滤器,验证用户登录凭证后,需要再经过SessionManagementFilter过滤器,SessionManagementFilter接着将session验证委托给SessionAuthenticationStrategy。
上图SessionAuthenticationStrategy的调用逻辑:
- 首先进入CompositeSessionAuthenticationStrategy,该策略意为混合session认证策略,它把session认证逻辑循环委托给其他具体的策略;
- ConcurrentSessionControlAuthenticationStrategy,并发session控制,用于同一个用户多次登录管理,默认行为会剔除(标记过期SessionInformation.expireNow())最近最久没有使用的登录,
maximumSessions=1
配置一个账号最多几个登录共存; - ChangeSessionIdAuthenticationStrategy,顾名思义变更当前sessionId,目的为了会话固定防护,该类继承了AbstractSessionFixationProtectionStrategy。
- RegisterSessionAuthenticationStrategy,为当前的新的sessionId注册一个SessionInformation(为了支持并发session控制);
- CsrfAuthenticationStrategy,支持csrf防护,生成新的token。
讲到这里大家可能会有疑问,刚才提到SessionInformation是干么用的呢?
SessionInformation是为了支持session并发控制,ConcurrentSessionControlAuthenticationStrategy只是将SessionInformation标记为过期,等下次请求的时候会经过ConcurrentSessionFilter过滤器,此时会判断当前session对应的SessionInformation是否被标记过期了,如果是则调用session销毁动作,真正地登出。如果不是,得刷新更新SessionInformation的lastRequest(最近一次请求时间),ConcurrentSessionControlAuthenticationStrategy是基于该时间去剔除最近最久没有使用的登录。
认证机制
- Username and Password
- Remember Me
- CAS
- OAuth 2.0
- SAML 2.0
- JAAS Authentication
- OpenID
- Pre-Authentication Scenarios
- X509 Authentication
Username and Password认证
验证用户身份的最常见方法之一是验证用户名和密码, Spring Security为使用用户名和密码进行身份验证提供了全面的支持。
认证方式
-
Form Login:基于表单登录认证;
上图表示未登录用户第一次访问web系统Spring Security处理流程:
- 用户在浏览器发起请求web系统私有资源
/private
; - SecurityFilterChain过滤器链路到达FilterSecurityInterceptor,并抛出访问被拒绝的异常AccessDeniedException,ExceptionTranslationFilter捕获该异常并通过sendStartAuthentication方法进入LoginUrlAuthenticationEntryPoint(AuthenticationEntryPoint的实现类);
- LoginUrlAuthenticationEntryPoint设置了一个重定向到
/login
页面的响应头; - 浏览器重定向到
/login
,并获取到login.html
,即登录主页面。
上图表示用户在表单提交用户名、密码的验证流程:
-
用户提交表单,进入UsernamePasswordAuthenticationFilter过滤器,通过调用attemptAuthentication方法将用户名、密码包装成UsernamePasswordAuthenticationToken对象,并传给AuthenticationManager;
上图表示AuthenticationManager工作流程:
- 将UsernamePasswordAuthenticationToken传给AuthenticationManager;
- ProviderManager将认证逻辑委托为DaoAuthenticationProvider;
- DaoAuthenticationProvider调用UserDetailsService的loadUserByUsername方法获取UserDetails,通过PasswordEncoder比较用户请求传递过来UsernamePasswordAuthenticationToken上的密码与UserDetails存储的密码是否一致;
- 认证成功,将UserDetails作为principal,UserDetails的authorities作为authorities包装成UsernamePasswordAuthenticationToken返回。
- 认证失败会做一些清理动作;
- 认证成功,作Session相关的逻辑、将Authentication存储到SecurityContextHolder、RememberMe相关处理、认证成功事件的发布、认证成功的后置处理。
-
Basic Authentication:基础认证,基本HTTP REST的身份验证。
上图表示未登录用户第一次访问web系统Spring Security处理流程:
- 用户在浏览器发起请求web系统私有资源
/private
; - SecurityFilterChain过滤器链路到达FilterSecurityInterceptor,并抛出访问被拒绝的异常AccessDeniedException,ExceptionTranslationFilter捕获该异常并通过sendStartAuthentication方法进入BasicAuthenticationEntryPoint(AuthenticationEntryPoint的实现类);
- BasicAuthenticationEntryPoint设置了
WWW-Authenticate
响应头及401
响应码并返回。
上图表示当客户端收到WWW-Authenticate响应头时,使用用户名和密码登录的流程:
- 当用户提交其用户名和密码时,BasicAuthenticationFilter通过从HttpServletRequest中提取用户名和密码来创建UsernamePasswordAuthenticationToken;
- 将UsernamePasswordAuthenticationToken传递到AuthenticationManager进行身份验证(这边的AuthenticationManager的认证逻辑与Form Login相同);
- 认证失败会做一些清理动作;
- 认证成功,将Authentication存储到SecurityContextHolder、RememberMe相关处理、调用FilterChain.doFilter(request,response)继续请求逻辑。
- Digest Authentication:摘要式身份验证;不建议在现代应用程序中使用摘要式身份验证,因为它不安全。 它必须以纯文本,加密或MD5格式存储密码,这些存储格式都被认为是不安全的。不支持的单向自适应密码哈希(即bCrypt,PBKDF2,SCrypt等)存储凭据。
Remember-Me认证
Remember-Me(记住我),主要用于在一段很长的时间内(通常15天),用户只需要登录一次,就无需再登录了(前提是用户名、密码、秘钥不变的情况)。
原理:当用户登录成功时,服务端会向浏览器额外发送一个cookie(name = remember-me, value = token值),之后的请求都会携带这个cookie,当用户session失效时(比如2小时过后),该cookie携带到服务端触发自动登录。
当然,Remember-Me会存在一些安全问题,Remember-Me的token可以被用户代理捕获到,可以轻松通过该token去修改密码。因此在一些安全性重要的应用上面,不建议开启Remember-Me。
存储机制
- In-Memory:内存存储;Spring Security的InMemoryUserDetailsManager实现了UserDetailsService,用来管理内存中的用户名/密码。
- JDBC:数据库存储;使用JdbcUserDetailsManager基于JDBC的用户名/密码身份验证。
- 自定义存储:使用UserDetailsService可自定义存储方式;例如:
// CustomUserDetailsService是UserDetailsService的实现类
@Bean
CustomUserDetailsService customUserDetailsService() {
return new CustomUserDetailsService();
}
- LDAP:LDAP存储,有兴趣可以自行查阅官方文档;
授权原理分析(Servlet)
FilterSecurityInterceptor授权
FilterSecurityInterceptor为HttpServletRequests提供授权,它作为安全筛选器之一插入到FilterChainProxy中(除此之外,Spring Security支持服务层方法授权还有域对象授权)。
- FilterSecurityInterceptor可以从SecurityContextHolder获取Authentication;
- FilterSecurityInterceptor将HttpServletRequest、HttpServletResponse、FilterChain包装成FilterInvocation对象,调用父类AbstractSecurityInterceptor.beforeInvocation(),进入前置处理阶段;
- AbstractSecurityInterceptor从SecurityMetadataSource获取配置信息
ConfigAttribute(例如:hasRole('ROLE_USER')
);调用AbstractSecurityInterceptor.authenticateIfRequired()执行认证相关逻辑; - 调用AccessDecisionManager.decide()方法,进行访问决策处理,该方法将决策委托给AccessDecisionVoter访问决策投票器,进行投票处理,AccessDecisionManager将投票的结果进行整合;
- 如果AccessDecisionManager决策被拒绝,则抛出AccessDeniedException,ExceptionTranslationFilter捕获到这个异常,并交给AuthenticationEntryPoint进行相应的处理,例如跳转登录页、返回403状态等;
- 如果AccessDecisionManager决策成功,则方法正常调用chain.doFilter();
- 方法正常调用完成后,通过AbstractSecurityInterceptor.afterInvocation()进入后置处理阶段(某些应用程序需要修改实际返回的对象);
Authorities权限列表
回顾下前面学到的知识,用户登录认证成功后,会为当前用户生成一个Authentication对象,该对象包含了Principal、Credentials和Authorities;
该Authorities由GrantedAuthority(默认实现:SimpleGrantedAuthority)集合组成,GrantedAuthority接口只有一个方法,如下:
String getAuthority();
一般情况下GrantedAuthority由String表示,如果GrantedAuthority无法精确地表示为String,则GrantedAuthority被视为“复杂”,并且getAuthority()必须返回null。
前置处理阶段
刚刚讲到Spring Security调用AbstractSecurityInterceptor.beforeInvocation()进入前置处理阶段,该阶段的一个重点就是进行访问决策处理,由AccessDecisionManager相关实现来完成,AccessDecisionManager接口包含三个方法:
// 决策处理逻辑
void decide(Authentication authentication, Object secureObject,
Collection attrs) throws AccessDeniedException;
// 项目启动校验配置是否正确
boolean supports(ConfigAttribute attribute);
// 项目启动校验配置是否正确
boolean supports(Class clazz);
如上图所示,Spring Security提供了三个决策处理器AccessDecisionManager的实现类(AffirmativeBased、ConsensusBased、UnanimousBased),代表三种不同的决策处理器,当然也可以自定义决策处理器。决策处理器将决策逻辑委托给多个投票器AccessDecisionVoter(具体实现有:AuthenticatedVoter、RoleVoter、WebExpressionVoter等),接着AccessDecisionManager将投票结果进行整合,返回拒绝或者成功。
AccessDecisionManager实现类的具体描述如下:
- AffirmativeBased:只要有一票通过,返回成功;
- ConsensusBased:通过票数 > 不通过票数,返回成功;如果通过票数 = 不通过票数,这个时候配置
allowIfEqualGrantedDeniedDecisions=true
,即可返回成功; - UnanimousBased:与上面两个不同的是,UnanimousBased需要轮询每个ConfigAttribute,然后投票器对每个ConfigAttribute进行投票,只要有一票不通过,则返回失败。
AccessDecisionVoter通过返回int来表示投票的结果,有ACCESS_ABSTAIN(0,弃权),ACCESS_DENIED(-1,不通过)和ACCESS_GRANTED(1,通过),AccessDecisionVoter主要的实现类如下:
- RoleVoter:角色投票器,最常用的投票器。如果任何ConfigAttribute以前缀ROLE_开头,它将进行投票,否则投票者将弃权ACCESS_ABSTAIN,如果存在GrantedAuthority可以返回一个字符串表示形式(通过getAuthority()方法),该字符串表示形式完全等于一个或多个以前缀ROLE_开头的ConfigAttributes,则它将投票通过ACCESS_GRANTED,否则投票不通过ACCESS_DENIED。
- AuthenticatedVoter:用来区分匿名,完全认证和记住我的认证用户。 许多站点允许使用“记住我”身份验证,但是某些资源确要求完全认证方式(用户登录)才能访问。
- WebExpressionVoter:web表达式投票器,基于Spring EL进行解析,例如:
hasRole('ROLE_USER')
。表达式根对象的基类是SecurityExpressionRoot,提供了Web和方法安全性中都可用的一些常用表达式。详情参考:https://docs.spring.io/spring-security/site/docs/5.4.1/reference/html5/#el-access
后置处理阶段
某些应用程序需要一种修改返回的对象的方法,因此Spring Security提供了一个方便的挂钩AfterInvocationManager,通过AfterInvocationManager来修改返回对象。
如上图所示,AfterInvocationManager有一个具体的实现AfterInvocationProviderManager,它轮询AfterInvocationProvider的列表。 每个AfterInvocationProvider都可以修改返回对象或引发AccessDeniedException。 实际上,由于前一个提供程序的结果将传递到列表中的下一个,因此多个提供程序可以修改该对象。