对当前项目中使用到的Spring Security做一个简单的理解总结,方便以后查阅。文章有疏漏之处,欢迎指正。
Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访 问控制解决方案的安全框架。它提供了一组可以在 Spring 应用上下文中配置的 Bean,充分利用了Spring IOC和AOP功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。应用程序层面的安全大概可以归为两类:身份认证和授权,Spring Security 在架构设计上就将两者分开了,在每个架构上都留有扩展点。
1 身份认证架构
1.1 AuthenticationManager
身份认证的核心接口,只包含一个方法:
public interface AuthenticationManager { Authentication authenticate(Authentication authentication) throws AuthenticationException; }
authenticate 方法可能产生三种结果:
a) 如果身份认证成功,返回完备的 Authentication 对象(一般来说,
authenticated=true)
b) 如果身份认证失败,抛出 AuthenticationException
c) 如果无法认证,则返回 null
1.2 ProviderManager
是最常用的 AuthenticationManager 接口的实现类,它将工作委托给 AuthenticationProvider 链。AuthenticationProvider 的接口定义类似于 AuthenticationManager,只不过多了个方法让调用者判断其是否支持传入的 Authentication 类型。
ProviderManager 会遍历 AuthenticationProvider 链,先判 断其是否支持传入的 Authentication 类型,如果支持,则调用 authenticate 方法, 如返回不为 null 的 Authentication,则身份认证成功。
for (AuthenticationProvider provider : getProviders()) { if (!provider.supports(toTest)) { continue; } if (debug) { logger.debug("Authentication attempt using " + provider.getClass().getName()); } try { result = provider.authenticate(authentication); if (result != null) { copyDetails(authentication, result); break; } } catch (AccountStatusException e) { prepareException(e, authentication); // SEC-546: Avoid polling additional providers if auth failure is due to // invalid account status throw e; } catch (InternalAuthenticationServiceException e) { prepareException(e, authentication); throw e; } catch (AuthenticationException e) { lastException = e; } }
2 授权架构
2.1 AccessDecisionManager
授权的核心接口,包含三个方法:
public interface AccessDecisionManager { void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException; boolean supports(ConfigAttribute attribute); boolean supports(Class<?> clazz); }
decide 方法决定是否允许访问被保护的对象,传入的参数中,authentication 是被身份认证通过后的完备对象,object是被保护的对象, configAttributes是被保护对象的属性信息。
2.2 AbstractAccessDecisionManager
实现了 AccessDecisionManager 接口的抽象类,它管理了一个 AccessDecisionVoter 列表,在执行授权操作时,调用每个 AccessDecisionVoter 的 vote 方法得到单独投票结果,对每个 voter 投票结果的仲裁则由其子类来完成。
Spring Security 提供了 3 个子类,每个子类的仲裁逻辑如下:
AffirmativeBased:只要有 voter投了赞成票,则授权成功;在没有赞成票的情况下,只要有反对票,则授权失败;在全部弃权的情况下,根据 isAllowIfAllAbstainDecisions 方法的返回值决定是否授权。
ConsensusBased: 根据少数服从多数的原则决定是否授权,如果赞成票和反对票相等,根据 isAllowIfAllAbstainDecisions 方法的返回值决定是否授权。
UnanimousBased:只有全部是赞成票或者弃权才授权成功,只要有反对票则授权失败。如果全部弃权,根据 isAllowIfAllAbstainDecisions 方法的返回值决定是否授权。
2.3 AccessDecisionVoter
对是否授权进行投票,核心方法是 vote, 该方法只能返回 3 种 int 值: ACCESS_GRANTED(1),ACCESS_ABSTAIN(0),ACCESS_DENIED(-1)。
这儿就是一个扩展点,项目可以实现自己的voter,例如: SimpleDecisionVoter implements AccessDecisionVoter<FilterInvocation>
3 Web 层安全
Spring Security 在 web 层的应用是基于 Servlet Filter 实现的,具体的实现类是 DelegatingFilterProxy,该代理类又会委托 Spring 容器管理的 FilterChainProxy 来处理,
FilterChainProxy 维护了一系列内部的 filter, 正是这些内部的 filter 实现 了全部的安全逻辑。关系图如下:
在web.xml在配置即可:
<filter> <filter-name>springSecurityFilterChain</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> </filter> <filter-mapping> <filter-name>springSecurityFilterChain</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
3.1 核心过滤器
以我所在的项目组对Spring Security的使用为例,一次web请求的filter chain 如下:
3.1.1 SecurityContextPersistenceFilter
此filter先尝试从servlet session中获取 SecurityContext ,如果没有获取到,则创建一个空的 SecurityContext 对象,SecurityContextHolder 是 SecurityContext 的存放容器,使用 ThreadLocal 存储并将此对象跟当前线程关联。
下图显示了整个交互过程:
3.1.2 LogoutFilter
先判断请求的 url 是否匹配 logout-url,如果匹配则重定向到指定的 url,否则直接调用下一个 filter。下图显示了整个交互过程:
属性名 | 作用 |
logout-url | 表示此请求做为退出登录的默认地址 |
invalidate-session | 表示是否要在退出登录后让当前session失效,默认为true。 |
delete-cookies | 指定退出登录后需要删除的cookie名称,多个cookie之间以逗号分隔。 |
logout-success-url | 指定成功退出登录后要重定向的URL。需要注意的是对应的URL应当是不需要登录就可以访问的。 |
success-handler-ref | 指定用来处理成功退出登录的LogoutSuccessHandler的引用。 |
简单看一下LogoutFilter的源码
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; if (requiresLogout(request, response)) { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (logger.isDebugEnabled()) { logger.debug("Logging out user \'" + auth + "\' and transferring to logout destination"); } this.handler.logout(request, response, auth); logoutSuccessHandler.onLogoutSuccess(request, response, auth); return; } chain.doFilter(request, response); }
如果 requiresLogout(request, response)为true,则分别调用 CompositeLogoutHandler 的 logout(request, response, auth) 方法和 LogoutSuccessHandler 的 onLogoutSuccess(request, response, auth);
在这两个方法中,logout 和 onLogoutSuccess 除了执行Spring Security自己的一些内部方法,比如 SecurityContextLogoutHandler 的 logout,我们也可以自己定义自己的方法,退出登录需要删除自定义的cookie等。
3.1.3 UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter
先看看 AbstractAuthenticationProcessingFilter 的 doFilter 方法
1 public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) 2 throws IOException, ServletException { 3 4 HttpServletRequest request = (HttpServletRequest) req; 5 HttpServletResponse response = (HttpServletResponse) res; 6 7 if (!requiresAuthentication(request, response)) { 8 chain.doFilter(request, response); 9 10 return; 11 } 12 13 if (logger.isDebugEnabled()) { 14 logger.debug("Request is to process authentication"); 15 } 16 17 Authentication authResult; 18 19 try { 20 authResult = attemptAuthentication(request, response); 21 if (authResult == null) { 22 // return immediately as subclass has indicated that it hasn\'t completed 23 // authentication 24 return; 25 } 26 sessionStrategy.onAuthentication(authResult, request, response); 27 } 28 catch (InternalAuthenticationServiceException failed) { 29 logger.error( 30 "An internal error occurred while trying to authenticate the user.", 31 failed); 32 unsuccessfulAuthentication(request, response, failed); 33 34 return; 35 } 36 catch (AuthenticationException failed) { 37 // Authentication failed 38 unsuccessfulAuthentication(request, response, failed); 39 40 return; 41 } 42 43 // Authentication success 44 if (continueChainBeforeSuccessfulAuthentication) { 45 chain.doFilter(request, response); 46 } 47 48 successfulAuthentication(request, response, chain, authResult); 49 }
20行的 attemptAuthentication(request, response) 就是 UsernamePasswordAuthenticationFilter 是尝试认证过程。
--------------------------------------UsernamePasswordAuthenticationFilter 分析开始---------------------------------------------------------------
身份认证过程:根据 form 表单中的用户名获取用户信息(包含密码),将数 据库中的密码和 form 表单中的密码做比对,若匹配则身份认证成功,并调用 AuthenticationSuccessHandler. onAuthenticationSuccess()。
下图显示的是认证成 功的交互过程:
因为真实项目登录除了校验用户名和密码外,可能还有有些额外的校验,比如验证码之类的,所有我们会自定义一个类去继承 UsernamePasswordAuthenticationFilter,然后重写 attemptAuthentication() 方法。
类 UsernamePasswordAuthenticationFilter 的 attemptAuthentication(HttpServletRequest request, HttpServletResponse response)方法:
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException( "Authentication method not supported: " + request.getMethod()); } String username = obtainUsername(request); String password = obtainPassword(request); if (username == null) { username = ""; } if (password == null) { password = ""; } username = username.trim(); UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken( username, password); // Allow subclasses to set the "details" property setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); }
然后执行类 ProviderManager 的 authenticate(Authentication authentication) 方法,已经到了身份认证架构部分,
继续调用抽象类 AbstractUserDetailsAuthenticationProvider 的 authenticate(Authentication authentication) 方法:
1 public Authentication authenticate(Authentication authentication) 2 throws AuthenticationException { 3 Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, 4 messages.getMessage( 5 "AbstractUserDetailsAuthenticationProvider.onlySupports", 6 "Only UsernamePasswordAuthenticationToken is supported")); 7 8 // Determine username 9 String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" 10 : authentication.getName(); 11 12 boolean cacheWasUsed = true; 13 UserDetails user = this.userCache.getUserFromCache(username); 14 15 if (user == null) { 16 cacheWasUsed = false; 17 18 try { 19 user = retrieveUser(username, 20 (UsernamePasswordAuthenticationToken) authentication); 21 } 22 catch (UsernameNotFoundException notFound) { 23 logger.debug("User \'" + username + "\' not found"); 24 25 if (hideUserNotFoundExceptions) { 26 throw new BadCredentialsException(messages.getMessage( 27 "AbstractUserDetailsAuthenticationProvider.badCredentials", 28 "Bad credentials")); 29 } 30 else { 31 throw notFound; 32 } 33 } 34 35 Assert.notNull(user, 36 "retrieveUser returned null - a violation of the interface contract"); 37 } 38 39 try { 40 preAuthenticationChecks.check(user); 41 additionalAuthenticationChecks(user, 42 (UsernamePasswordAuthenticationToken) authentication); 43 } 44 catch (AuthenticationException exception) { 45 if (cacheWasUsed) { 46 // There was a problem, so try again after checking 47 // we\'re using latest data (i.e. not from the cache) 48 cacheWasUsed = false; 49 user = retrieveUser(username, 50 (UsernamePasswordAuthenticationToken) authentication); 51 preAuthenticationChecks.check(user); 52 additionalAuthenticationChecks(user, 53 (UsernamePasswordAuthenticationToken) authentication); 54 } 55 else { 56 throw exception; 57 } 58 } 59 60 postAuthenticationChecks.check(user); 61 62 if (!cacheWasUsed) { 63 this.userCache.putUserInCache(user); 64 } 65 66 Object principalToReturn = user; 67 68 if (forcePrincipalAsString) { 69 principalToReturn = user.getUsername(); 70 } 71 72 return createSuccessAuthentication(principalToReturn, authentication, user); 73 }
19行 retrieveUser 方法的具体实现在类 DaoAuthenticationProvider 中,这儿就是从我们系统中根据 username,查询到一个 UserDetails 数据。
40行 preAuthenticationChecks.check(user); 则是为已经查询到的用户,附上在当前系统中的权限数据。
41行 additionalAuthenticationChecks 里面就是密码的校验,具体实现类还是 DaoAuthenticationProvider,自定义的 passwordEncoderHandler 实现了 PasswordEncoder 接口 isPasswordValid 方法,去比较密码是否匹配。
--------------------------------------UsernamePasswordAuthenticationFilter 分析结束---------------------------------------------------------------
继续 AbstractAuthenticationProcessingFilter 的 doFilter 方法分析,
32和38行 unsuccessfulAuthentication(request, response, failed) 是认证失败的处理逻辑,
认证失败时,首先 SecurityContextHolder.clearContext(),清除认证信息,然后在 SimpleUrlAuthenticationFailureHandler 可以处理自定义的认证失败逻辑
48行的 successfulAuthentication(request, response, chain, authResult) 则是认证成功的逻辑。
认证成功时,首先 SecurityContextHolder.getContext().setAuthentication(authentication),其次执行 AbstractRememberMeServices 的 loginSuccess 方法,然后执行 SavedRequestAwareAuthenticationSuccessHandler的 onAuthenticationSuccess方法。
RememberMeAuthenticationFilter
这个filter貌似是查询 当前SecurityContextHolder的Authentication是否为null,如果为空则继续走类似上一个filter的身份认证过程,如果不为空,继续下一个filter。
3.1.4 RequestCacheAwareFilter
此 filter 通常用作临时重定向,场景是这样的:在未登陆的情况下访问某个安全 url,会重定向到登陆页,在重定向之前先在 session 中保存访问此安全 url 的请求,在登陆成功后再继续处理它。下图显示的是交互过程:
图中 SAVED_REQUEST = "SPRING_SECURITY_SAVED_REQUEST"
3.1.5 SecurityContextHolderAwareRequestFilter
将 servlet request 封装到 SecurityContextHolderAwareRequestWrapper 类中,此 包装类继承自 HttpServletRequestWrapper,并提供了额外的跟安全相关的方法。
3.1.6 AnonymousAuthenticationFilter
如果到了这步 SecurityContext 中的 Authentication 对象仍然为null, 则创建一个 AnonymousAuthenticationToken,这个对象可以类比网站的匿名用户。下图展示 的是交互过程:
3.1.7 SessionManagementFilter
这个过滤器看名字就知道是管理 session 的了,提供两大类功能: session 固化 保护(通过 session-fixation-protection 配置),session 并发控制(通过 concurrency-control 配置)。
3.1.8 ExceptionTranslationFilter
当请求到达这里时,会将对后续过滤器的调用封装在 try..catch 块中,如果捕获 到 AuthenticationException 异常,则调用 authenticationEntryPoint.commence 方法开启新的身份认证流程(一般来说是跳转到登陆页),
如果捕获到 AccessDeniedException 异常,调用 accessDeniedHandler.handle 方法处理(比如: 展示未授权的错误页面)。下图显示的是交互过程:
3.1.9 FilterSecurityInterceptor
此过滤器是整个过滤器链的最后一环,用于保护 Http 资源的,它需要一个 AccessDecisionManager 和一个 AuthenticationManager 的引用。它会从 SecurityContextHolder 获取 Authentication,
然后通过 SecurityMetadataSource 可以得知当前请求是否在请求受保护的资源。对于请求那些受保护的资源,如果 Authentication.isAuthenticated()返回 false 或者 FilterSecurityInterceptor 的 alwaysReauthenticate 属性为 true ,
那么将会使用其引用的 AuthenticationManager 再认证一次,认证之后再使用认证后的 Authentication替换 SecurityContextHolder 中拥有的那个。然后就是利用 AccessDecisionManager 进行权限的检查,也就进行到了授权架构部分。
下图显示的是交互过程:
3.2其他过滤器
3.2.1 CsrfFilter
该过滤器通过 token 防范 CSRF 攻击,将从 tokenRepository 获取的 token 与从 请求参数或者 header 参数中获取的 token 做比较,如果不匹配,调用 accessDeniedHandler.handle 方法来处理。下图展示的是交互过程:
3.2.2 HeaderWriterFilter
该过滤器通过写响应安全头的方式来保护浏览器端的安全,下面简单介绍下各种 http 安全头。
content-security-policy:通过定义内容来源来预防 XSS 攻击或者代码植入攻击,下面 的例子只允许当前域或者 google-analytics.com 的脚本执行。
X-XSS-Protection: 又称 XSS 过滤器,通过指示浏览器阻止含有恶意脚本的响应来预防 XSS 攻击。
strict-transport-security(HSTS):该头指示浏览器只能使用 https 访问 web server。
X-Frame-Options:来确保自己网站的内容没有被嵌到别人的网站中去,也从而避免了点击 劫持 (clickjacking) 的攻击。X-Frame-Options 有三个值: DENY(表示该页面不允许 在 frame 中展示,即便是在相同域名的页面中嵌套也不允许),
SAMEORIGIN(表示该页面可 以在相同域名页面的 frame 中展示),ALLOW-FROM uri(表示该页面可以在指定来源的 frame 中展示)。
X-Content-Type-Options:如果服务器发送响应头 "X-Content-Type-Options: nosniff",则 script 和 styleSheet 元素会拒绝包含错误的 MIME 类型的响应。这是一 种安全功能,有助于防止基于 MIME 类型混淆的攻击。