对当前项目中使用到的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;
}
}
View Code
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);
}
View Code
然后执行类 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 }
View Code
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) 则是认证成功的逻辑。
),其次执行 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 的脚本执行。
content-security-policy: script-src 'self' https://www.google-analytics.com
X-XSS-Protection: 又称 XSS 过滤器,通过指示浏览器阻止含有恶意脚本的响应来预防 XSS 攻击。
x-xss-protection: 1; mode=block
strict-transport-security(HSTS):该头指示浏览器只能使用 https 访问 web server。
strict-transport-security: max-age=31536000; includeSubDomains; preload
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 类型混淆的攻击。
4.参考文献
==================================================================
勇气是,尽管你感到害怕,但仍能迎难而上。
尽管你感觉痛苦,但仍能直接面对。
向前一步,也许一切都会不同。
==================================================================