Catalogue
-
1.
前言
-
2.
核心过滤器链简介
-
3.
表单登录认证过程
-
3.1.
SecurityContextPersistenceFilter
-
3.2.
UsernamePasswordAuthenticationFilter
-
3.3.
AnonymousAuthenticationFilter
-
3.4.
ExceptionTranslationFilter
-
3.5.
FilterSecurityInterceptor
-
4.
小结
-
5.
参考资料 & 鸣谢
浅析Spring Security 核心组件
中介绍了Spring Security的基本组件,有了前面的基础,这篇文章就来详细分析下Spring Security的认证过程。
Spring Security 的核心之一就是它的过滤器链,我们就从它的过滤器链入手,下图是Spring Security 过滤器链的一个执行过程,本文将依照该过程来逐步的剖析其认证过程
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
|
public class SecurityContextPersistenceFilter extends GenericFilterBean {
static final String FILTER_APPLIED = "__spring_security_scpf_applied"; private SecurityContextRepository repo;
private boolean forceEagerSessionCreation = false;
public SecurityContextPersistenceFilter() { this(new HttpSessionSecurityContextRepository()); }
public SecurityContextPersistenceFilter(SecurityContextRepository repo) { this.repo = repo; }
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; if (request.getAttribute(FILTER_APPLIED) != null) { chain.doFilter(request, response); return; }
final boolean debug = logger.isDebugEnabled(); request.setAttribute(FILTER_APPLIED, Boolean.TRUE); if (forceEagerSessionCreation) { HttpSession session = request.getSession();
if (debug && session.isNew()) { logger.debug("Eagerly created session: " + session.getId()); } } HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response); SecurityContext contextBeforeChainExecution = repo.loadContext(holder);
try { SecurityContextHolder.setContext(contextBeforeChainExecution); chain.doFilter(holder.getRequest(), holder.getResponse());
} finally { SecurityContext contextAfterChainExecution = SecurityContextHolder .getContext(); SecurityContextHolder.clearContext(); repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse()); request.removeAttribute(FILTER_APPLIED);
if (debug) { logger.debug("SecurityContextHolder now cleared, as request processing completed"); } } }
public void setForceEagerSessionCreation(boolean forceEagerSessionCreation) { this.forceEagerSessionCreation = forceEagerSessionCreation; } }
|
文章
就介绍过
AuthenticationManager,它是身份认证的核心接口,它的实现类是
ProviderManager
,而
ProviderManager
又将请求委托给一个
AuthenticationProvider
列表,列表中的每一个 AuthenticationProvider将会被依次查询是否需要通过其进行验证,每个 provider的验证结果只有两个情况:抛出一个异常或者完全填充一个 Authentication对象的所有属性
下面来分析一个关键的
AuthenticationProvider
,它就是
DaoAuthenticationProvider
,它是框架最早的provider,也是最最常用的 provider。大多数情况下我们会依靠它来进行身份认证,它的父类是
AbstractUserDetailsAuthenticationProvider
,认证过程首先会调用父类的
authenticate
方法,核心源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
|
public Authentication authenticate(Authentication authentication) throws AuthenticationException { Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, messages.getMessage( "AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported"));
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName();
boolean cacheWasUsed = true; UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) { cacheWasUsed = false;
try { 1 user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); } catch (UsernameNotFoundException notFound) { logger.debug("User '" + username + "' not found");
if (hideUserNotFoundExceptions) { throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } else { throw notFound; } }
Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract"); }
try { 2 preAuthenticationChecks.check(user); 3 additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); } catch (AuthenticationException exception) { if (cacheWasUsed) { cacheWasUsed = false; user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); preAuthenticationChecks.check(user); additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); } else { throw exception; } } 4 postAuthenticationChecks.check(user);
if (!cacheWasUsed) { this.userCache.putUserInCache(user); }
Object principalToReturn = user;
if (forcePrincipalAsString) { principalToReturn = user.getUsername(); } 5 return createSuccessAuthentication(principalToReturn, authentication, user); }
|
从上面一大串源码中,提取几个关键的方法:
retrieveUser(…)
: 调用子类 DaoAuthenticationProvider 的 retrieveUser()方法获取 UserDetails
preAuthenticationChecks.check(user)
: 对从上面获取的UserDetails进行预检查,即判断用户是否锁定,是否可用以及用户是否过期
additionalAuthenticationChecks(user,authentication)
: 对UserDetails附加的检查,对传入的Authentication与获取的UserDetails进行密码匹配
postAuthenticationChecks.check(user)
: 对UserDetails进行后检查,即检查UserDetails的密码是否过期
createSuccessAuthentication(principalToReturn, authentication, user)
: 上面所有检查成功后,利用传入的Authentication 和获取的UserDetails生成一个成功验证的Authentication
retrieveUser(…)方法
接下来详细说说
retrieveUser(...)
方法, DaoAuthenticationProvider 的 retrieveUser() 源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
|
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { prepareTimingAttackProtection(); try { UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username); if (loadedUser == null) { throw new InternalAuthenticationServiceException( "UserDetailsService returned null, which is an interface contract violation"); } return loadedUser; } catch (UsernameNotFoundException ex) { mitigateAgainstTimingAttack(authentication); throw ex; } catch (InternalAuthenticationServiceException ex) { throw ex; } catch (Exception ex) { throw new InternalAuthenticationServiceException(ex.getMessage(), ex); } }
|
该方法最核心的部分就是调用内部的UserDetailsServices 加载 UserDetails,
UserDetailsServices
本质上就是加载UserDetails的接口,UserDetails包含了比Authentication更加详细的用户信息。
UserDetailsService常见的实现类有JdbcDaoImpl,InMemoryUserDetailsManager,前者从数据库加载用户,后者从内存中加载用户。我们也可以自己实现UserDetailsServices接口,比如我们是如果是基于数据库进行身份认证,那么我们可以手动实现该接口,而不用JdbcDaoImpl。
additionalAuthenticationChecks()
UserDetails的预检查和后检查比较简单,这里就不细说了,下面来看一下密码匹配校验,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { if (authentication.getCredentials() == null) { logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); }
String presentedPassword = authentication.getCredentials().toString(); if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) { logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } }
|
这个方法实际上是调用
DaoAuthenticationProvider
的
additionalAuthenticationChecks
方法,内部调用加密解密器进行密码匹配,如果匹配失败,则抛出一个
BadCredentialsException
异常
最后通过
createSuccessAuthentication(..)
方法生成一个成功认证的 Authentication,简单说就是组合获取的UserDetails和传入的Authentication,得到一个完全填充的Authentication。
该Authentication最终一步一步向上返回,到
AbstractAuthenticationProcessingFilter
过滤器中,将其设置到
SecurityContextHolder
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
|
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res;
try { chain.doFilter(request, response);
logger.debug("Chain processed normally"); } catch (IOException ex) { throw ex; } catch (Exception ex) { Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex); RuntimeException ase = (AuthenticationException) throwableAnalyzer .getFirstThrowableOfType(AuthenticationException.class, causeChain); if (ase == null) { ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType( AccessDeniedException.class, causeChain); }
if (ase != null) { if (response.isCommitted()) { throw new ServletException("Unable to handle the Spring Security Exception because the response is already committed.", ex); } handleSpringSecurityException(request, response, chain, ase); } else { if (ex instanceof ServletException) { throw (ServletException) ex; } else if (ex instanceof RuntimeException) { throw (RuntimeException) ex; }
throw new RuntimeException(ex); } } }
|