Spring Security认证流程分析(6)

打印 上一主题 下一主题

主题 993|帖子 993|积分 2979

1.认证流程分析

  Spring Security中默认的一套登录流程是非常完善并且严谨的。但是项目需求非常多样化, 很多时候,我们可能还需要对Spring Secinity登录流程进行定制,定制的前提是开发者先深刻理解Spring Security登录流程,然后在此基础之上,完成对登录流程的定制。本文将从头梳理 Spring Security登录流程,并通过几个常见的登录定制案例,深刻地理解Spring Security登录流程。
  本章涉及的主要知识点有:

  • 登录流程分析。
  • 配置多个数据源。
  • 添加登录验证码。
  1.1登录流程分析

  要搞清楚Spring Security认证流程,我们得先认识与之相关的三个基本组件(Authentication 对象在前面文章种己经做过介绍,这里不再赘述):AuthenticationManager、ProviderManager以及AuthenticationProvider,同时还要去了解接入认证功能的过滤器 AbstractAuthenticationProcessingFilter,这四个类搞明白了,基本上认证流程也就清楚了,下面我们逐个分析一下。
  1.1.1 AuthenticationManager

  从名称上可以看出,AuthenticationManager是一个认证管理器,它定义了 Spring Security 过滤器要如何执行认证操作。AuthenticationManager在认证成功后,会返回一个Authentication对象,这个Authentication对象会被设置到SecurityContextHolder中。如果开发者不想用Spring Security提供的一套认证机制,那么也可以自定义认证流程,认证成功后,手动将Authentication 存入 SecurityContextHolder 中。
  1. public interface AuthenticationManager {
  2.     Authentication authenticate(Authentication var1) throws AuthenticationException;
  3. }
复制代码
  从 AuthenticationManager 的源码中可以看到,AuthenticationManager 对传入的 Authentication对象进行身份认证,此时传入的Authentication参数只有用户名/密码等简单的属性,如果认证成功,返回的Authentication的属性会得到完全填充,包括用户所具备的角色信息。AuthenticationManager是一个接口,它有着诸多的实现类,开发者也可以自定义 AuthenticationManager的实现类,不过在实际应用中,我们使用最多的是ProviderManager,在 Spring S ecurity 框架中,默认也是使用 ProviderManager。
  1.1.2 AuthenticationProvider

  Spring Security支持多种不同的认证方式,不同的认证方式对应不同的身份 类型,AuthenticationProvider就是针对不同的身份类型执行具体的身份认证。例如,常见的 DaoAuthenticationProvider 用来支持用户名/密码登录认证,RememberMeAuthenticationProvider 用来支持“记住我”的认证。
  1. public interface AuthenticationProvider {
  2.     Authentication authenticate(Authentication var1) throws AuthenticationException;
  3.     boolean supports(Class<?> var1);
  4. }
复制代码

  • authenticate方法用来执行具体的身份忧证。
  • supports方法用来判断当前AuthenticationProvider是否支持对应的身份类型。
  当使用用户名/密码的方式登录时,对应的AuthenticationProvider实现类是 DaoAuthenticationProvider , 而 DaoAuthenticationProvider 继承自 AbstractUserDetailsAuthenticationProvider并且没有重写authenticate方法,所以具体的认证逻辑在AbstractUserDetailsAuthenticationProvider 的 authenticate 方法中。我们就从 AbstractUserDetailsAuthenticationProvider开始看起:
  
查看代码
  1.  public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware {
  2.     protected final Log logger = LogFactory.getLog(this.getClass());
  3.     protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
  4.     private UserCache userCache = new NullUserCache();
  5.     private boolean forcePrincipalAsString = false;
  6.     protected boolean hideUserNotFoundExceptions = true;
  7.     private UserDetailsChecker preAuthenticationChecks = new AbstractUserDetailsAuthenticationProvider.DefaultPreAuthenticationChecks();
  8.     private UserDetailsChecker postAuthenticationChecks = new AbstractUserDetailsAuthenticationProvider.DefaultPostAuthenticationChecks();
  9.     private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();
  10.     public AbstractUserDetailsAuthenticationProvider() {
  11.     }
  12.     protected abstract void additionalAuthenticationChecks(UserDetails var1, UsernamePasswordAuthenticationToken var2) throws AuthenticationException;
  13.     public final void afterPropertiesSet() throws Exception {
  14.         Assert.notNull(this.userCache, "A user cache must be set");
  15.         Assert.notNull(this.messages, "A message source must be set");
  16.         this.doAfterPropertiesSet();
  17.     }
  18.     public Authentication authenticate(Authentication authentication) throws AuthenticationException {
  19.         Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> {
  20.             return this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported");
  21.         });
  22.         String username = authentication.getPrincipal() == null ? "NONE_PROVIDED" : authentication.getName();
  23.         boolean cacheWasUsed = true;
  24.         UserDetails user = this.userCache.getUserFromCache(username);
  25.         if (user == null) {
  26.             cacheWasUsed = false;
  27.             try {
  28.                 user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
  29.             } catch (UsernameNotFoundException var6) {
  30.                 this.logger.debug("User '" + username + "' not found");
  31.                 if (this.hideUserNotFoundExceptions) {
  32.                     throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
  33.                 }
  34.                 throw var6;
  35.             }
  36.             Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
  37.         }
  38.         try {
  39.             this.preAuthenticationChecks.check(user);
  40.             this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
  41.         } catch (AuthenticationException var7) {
  42.             if (!cacheWasUsed) {
  43.                 throw var7;
  44.             }
  45.             cacheWasUsed = false;
  46.             user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
  47.             this.preAuthenticationChecks.check(user);
  48.             this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
  49.         }
  50.         this.postAuthenticationChecks.check(user);
  51.         if (!cacheWasUsed) {
  52.             this.userCache.putUserInCache(user);
  53.         }
  54.         Object principalToReturn = user;
  55.         if (this.forcePrincipalAsString) {
  56.             principalToReturn = user.getUsername();
  57.         }
  58.         return this.createSuccessAuthentication(principalToReturn, authentication, user);
  59.     }
  60.     protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
  61.         UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal, authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
  62.         result.setDetails(authentication.getDetails());
  63.         return result;
  64.     }
  65.     protected void doAfterPropertiesSet() throws Exception {
  66.     }
  67.     public UserCache getUserCache() {
  68.         return this.userCache;
  69.     }
  70.     public boolean isForcePrincipalAsString() {
  71.         return this.forcePrincipalAsString;
  72.     }
  73.     public boolean isHideUserNotFoundExceptions() {
  74.         return this.hideUserNotFoundExceptions;
  75.     }
  76.     protected abstract UserDetails retrieveUser(String var1, UsernamePasswordAuthenticationToken var2) throws AuthenticationException;
  77.     public void setForcePrincipalAsString(boolean forcePrincipalAsString) {
  78.         this.forcePrincipalAsString = forcePrincipalAsString;
  79.     }
  80.     public void setHideUserNotFoundExceptions(boolean hideUserNotFoundExceptions) {
  81.         this.hideUserNotFoundExceptions = hideUserNotFoundExceptions;
  82.     }
  83.     public void setMessageSource(MessageSource messageSource) {
  84.         this.messages = new MessageSourceAccessor(messageSource);
  85.     }
  86.     public void setUserCache(UserCache userCache) {
  87.         this.userCache = userCache;
  88.     }
  89.     public boolean supports(Class<?> authentication) {
  90.         return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
  91.     }
  92.     protected UserDetailsChecker getPreAuthenticationChecks() {
  93.         return this.preAuthenticationChecks;
  94.     }
  95.     public void setPreAuthenticationChecks(UserDetailsChecker preAuthenticationChecks) {
  96.         this.preAuthenticationChecks = preAuthenticationChecks;
  97.     }
  98.     protected UserDetailsChecker getPostAuthenticationChecks() {
  99.         return this.postAuthenticationChecks;
  100.     }
  101.     public void setPostAuthenticationChecks(UserDetailsChecker postAuthenticationChecks) {
  102.         this.postAuthenticationChecks = postAuthenticationChecks;
  103.     }
  104.     public void setAuthoritiesMapper(GrantedAuthoritiesMapper authoritiesMapper) {
  105.         this.authoritiesMapper = authoritiesMapper;
  106.     }
  107.     private class DefaultPostAuthenticationChecks implements UserDetailsChecker {
  108.         private DefaultPostAuthenticationChecks() {
  109.         }
  110.         public void check(UserDetails user) {
  111.             if (!user.isCredentialsNonExpired()) {
  112.                 AbstractUserDetailsAuthenticationProvider.this.logger.debug("User account credentials have expired");
  113.                 throw new CredentialsExpiredException(AbstractUserDetailsAuthenticationProvider.this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.credentialsExpired", "User credentials have expired"));
  114.             }
  115.         }
  116.     }
  117.     private class DefaultPreAuthenticationChecks implements UserDetailsChecker {
  118.         private DefaultPreAuthenticationChecks() {
  119.         }
  120.         public void check(UserDetails user) {
  121.             if (!user.isAccountNonLocked()) {
  122.                 AbstractUserDetailsAuthenticationProvider.this.logger.debug("User account is locked");
  123.                 throw new LockedException(AbstractUserDetailsAuthenticationProvider.this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.locked", "User account is locked"));
  124.             } else if (!user.isEnabled()) {
  125.                 AbstractUserDetailsAuthenticationProvider.this.logger.debug("User account is disabled");
  126.                 throw new DisabledException(AbstractUserDetailsAuthenticationProvider.this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.disabled", "User is disabled"));
  127.             } else if (!user.isAccountNonExpired()) {
  128.                 AbstractUserDetailsAuthenticationProvider.this.logger.debug("User account is expired");
  129.                 throw new AccountExpiredException(AbstractUserDetailsAuthenticationProvider.this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.expired", "User account has expired"));
  130.             }
  131.         }
  132.     }
  133. }
复制代码

  • 一开始先声明一个用户缓存对象userCache,默认情况下没有启用缓存对象。
  • hideUserNotFoundExceptions表示是否隐藏用户名查找失败的异常,默认为true,为了确保系统安全,用户在登录失败时只会给出一个模糊提示,例如“用户名或密码输入错误“ 在Spring Security内部,如果用户名查找失败,则会抛出UsernameNotFoundException异常, 但是该异常会被自动隐藏,转而通过一个BadCredentialsException异常来代替它,这样,开发者在处理登录失败异常时,无论是用户名输入错误还是密码输入错误,收到的总是 BadCredentialsException,这样做的一个好处是可以避免新手程序员将用户名输入错误和密码输入错误两个异常分开提示。
  • forcePrincipalAsString表示是否强制将Principal对象当成字符串来处理,默认是falser,Authentication中的Principal属性类型是一个Object,正常来说,通过Principal属性可以获取到当前登录用户对象(即UserDetails),但是如果forcePrincipalAsString设置为true,则 Authentication中的Principal属性返回就是当前登录用户名,而不是用户对象。
  • preAuthenticationChecks对象则是用于做用户状态检査,在用户认证过程中,需要检验用户状态是否正常,例如账户是否被锁定、账户是否可用、账户是否过期等。
  • postAuthenticationChecks对象主要负责在密码校验成功后,检査密码是否过期。
  • additionalAuthenticationChecks是一个抽象方法,主要就是校验密码,具体的实现在 DaoAuthenticationProvider 中。
  • authenticate方法就是核心的校验方法了。在方法中,首先从登录数据中获取用户名, 然后根据用户名去缓存中查询用户对象,如果査询不到,则根据用户名调用retrieveUser方法从数据库中加载用户;如果没有加载到用户,则抛出异常(用户不存在异常会被隐藏)。拿到用户对象之后,首先调用check方法进行用户状态检査,然后调用 additionalAuthenticationChecks 方法进行密码的校验操作,最后调用 postAuthenticationChecks.check方法检査密码是否过期,当所有步骤都顺利完成后,调用createSuccessAuthentication 方法创建一个认证后的 UsernamePasswordAuthenticationToken 对象并返回,认证后的对象中包含了认证主体、凭证以及角色等信息。
  这就是 AbstractUserDetailsAuthenticationProvider类的工作流程,有几个抽象方法是在 DaoAuthenticationProvider 中实现的,我们再来看一下 DaoAuthenticationProvider中的定义:
  
查看代码
  1.  public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
  2.     private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";
  3.     private PasswordEncoder passwordEncoder;
  4.     private volatile String userNotFoundEncodedPassword;
  5.     private UserDetailsService userDetailsService;
  6.     private UserDetailsPasswordService userDetailsPasswordService;
  7.     public DaoAuthenticationProvider() {
  8.         this.setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
  9.     }
  10.     protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
  11.         if (authentication.getCredentials() == null) {
  12.             this.logger.debug("Authentication failed: no credentials provided");
  13.             throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
  14.         } else {
  15.             String presentedPassword = authentication.getCredentials().toString();
  16.             if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
  17.                 this.logger.debug("Authentication failed: password does not match stored value");
  18.                 throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
  19.             }
  20.         }
  21.     }
  22.     protected void doAfterPropertiesSet() {
  23.         Assert.notNull(this.userDetailsService, "A UserDetailsService must be set");
  24.     }
  25.     protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
  26.         this.prepareTimingAttackProtection();
  27.         try {
  28.             UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
  29.             if (loadedUser == null) {
  30.                 throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
  31.             } else {
  32.                 return loadedUser;
  33.             }
  34.         } catch (UsernameNotFoundException var4) {
  35.             this.mitigateAgainstTimingAttack(authentication);
  36.             throw var4;
  37.         } catch (InternalAuthenticationServiceException var5) {
  38.             throw var5;
  39.         } catch (Exception var6) {
  40.             throw new InternalAuthenticationServiceException(var6.getMessage(), var6);
  41.         }
  42.     }
  43.     protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
  44.         boolean upgradeEncoding = this.userDetailsPasswordService != null && this.passwordEncoder.upgradeEncoding(user.getPassword());
  45.         if (upgradeEncoding) {
  46.             String presentedPassword = authentication.getCredentials().toString();
  47.             String newPassword = this.passwordEncoder.encode(presentedPassword);
  48.             user = this.userDetailsPasswordService.updatePassword(user, newPassword);
  49.         }
  50.         return super.createSuccessAuthentication(principal, authentication, user);
  51.     }
  52.     private void prepareTimingAttackProtection() {
  53.         if (this.userNotFoundEncodedPassword == null) {
  54.             this.userNotFoundEncodedPassword = this.passwordEncoder.encode("userNotFoundPassword");
  55.         }
  56.     }
  57.     private void mitigateAgainstTimingAttack(UsernamePasswordAuthenticationToken authentication) {
  58.         if (authentication.getCredentials() != null) {
  59.             String presentedPassword = authentication.getCredentials().toString();
  60.             this.passwordEncoder.matches(presentedPassword, this.userNotFoundEncodedPassword);
  61.         }
  62.     }
  63.     public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
  64.         Assert.notNull(passwordEncoder, "passwordEncoder cannot be null");
  65.         this.passwordEncoder = passwordEncoder;
  66.         this.userNotFoundEncodedPassword = null;
  67.     }
  68.     protected PasswordEncoder getPasswordEncoder() {
  69.         return this.passwordEncoder;
  70.     }
  71.     public void setUserDetailsService(UserDetailsService userDetailsService) {
  72.         this.userDetailsService = userDetailsService;
  73.     }
  74.     protected UserDetailsService getUserDetailsService() {
  75.         return this.userDetailsService;
  76.     }
  77.     public void setUserDetailsPasswordService(UserDetailsPasswordService userDetailsPasswordService) {
  78.         this.userDetailsPasswordService = userDetailsPasswordService;
  79.     }
  80. }
复制代码

  • 首先定义了 USER_NOT_FOUND_PASSWORD常量,这个是当用户查找失败时的默认密码;passwordEncoder是一个密码加密和比对工具,这个在后面会有专门的介绍,这里先不做过多解释;userNotFoundEncodedPassword变量则用来保存默认密码加密后的值; userDetailsService是一个用户查找工具,userDetailsService在前面己经讲过,这里不再赘述; userDetailsPasswordService则用来提供密码修改服务。


  • 在DaoAuthenticationProvider的构造方法中,默认就会指定PasswordEncoder,当然开发者也可以通过set方法自定义PasswordEncoder。
  • additionalAuthenticationchecks方法主要进行密码校验,该方法第一个参数userDetails 是从数据库中查询出来的用户对象,第二个参数authentication则是登录用户输入的参数。从这两个参数中分别提取出来用户密码,然后调用passwordEncoder.matches方法进行密码比对。
  •  retrieveUser方法则是获取用户对象的方法,具体做法就是调用 UserDetailsService#loadUserByUsername 方法去数据库中查询。


  • 在retrieveUser方法中,有一个值得关注的地方。在该方法一开始,首先会调用 prepareTimingAttackProtection 方法,该方法的作用是使用 PasswordEncoder 对常量 USER_NOT_FOUND_PASSWORD 进行加密,将加密结果保存在 userNotFoundEncodedPassword变量中。当根据用户名查找用户时如果抛出了 UsernameNotFoundException异常, 则调用mitigateAgainstTimingAttack方法进行密码比对由有读者会说,用户都没查找到,怎么 比对密码?需要注意,在调用mitigateAgainstTimingAttack方法进行密码比对时,使用了 userNotFoundEncodedPassword变量作为默认密码和登录请求传来的用户密码进行比对,这是 一个一开始就注定要失败的密码比对,那么为什么还要进行比对呢?这主要是为了避免旁道攻击(Side-channel attack)。如果根据用户名査找用户失败,就直接抛出异常而不进行密码比对, 那么黑客经过大量的测试,就会发现有的请求耗费时间明显小于其他请求,那么进而可以得出该请求的用户名是一个不存在的用户名(因为用户名不存在,所以不需要密码比对,进而节省时间),这样就可以获取到系统信息。为了避免这一问题,所以当用户查找失败时,也会调用 mitigateAgainstTimingAttack方法进行密码比对,这样就可以迷惑黑客。
  • createSuccessAuthentication方法则是在登录成功后,创建一个全新的 UsernamePasswordAuthenticationToken对象,同时会判断是否需要进行密码升级,如果需要进行密码升级,就会在该方法中进行加密方案升級。通过对 AbstractUserDetailsAuthenticationProvider 和 DaoAuthenticationProvider 的讲解,相信你己经很明白AuthenticationProvider中的认证逻辑了。
  在密码学中,旁道攻击(Side-channel attack )又称侧信道攻击、边信道攻击。这种攻击方式不是暴力破解或者是研究加密算法的弱点。它是基于从密码系统的物理实现中获取信息, 比如时间、功率消耗、电磁泄漏等,这些信息可被利用于对系统的进一步破解。
  1.1.3 ProviderManager

  ProviderManager是AuthenticationManager的一个重要实现类,正在开始学习之前,我们先通过一幅图来了解一下ProviderManager和AuthenticationProvider之间的关系,如图3-1所示。
  
图 3-1
  在Spring Security中,由于系统可能同时支持多种不同的认证方式,例如同时支持用户名 /密码认证、RememberMe认证、手机号码动态认证等,而不同的认证方式对应了不同的 AuthenticationProvider,所以一个完整的认证流程可能由多个AuthenticationProvider来提供。
  多个AuthenticationProvider将组成一个列表,这个列表将由ProviderManager代理。换句话说,在 ProviderManager 中存在一个 AuthenticationProvider 列表,在 ProviderManager 中遍历列表中的每一个AuthenticationProvider去执行身份认证,最终得到认证结果。
  ProviderManager 本身也可以再配置一个 AuthenticationManager 作为 parent,这样当 ProviderManager认证失败之后,就可以进入到parent中再次进行认证。理论上来说, ProviderManager的parent可以是任意类型的 AuthenticationManager,但是通常都是由 ProviderManager 来扮演 parent 的角色,也就是 ProviderManager 是 ProviderManager 的 parent。
  ProviderManager本身也可以有多个,多个ProviderManager共用同一个parent,当存在多个过滤器链的时候非常有用。当存在多个过滤器链时,不同的路径可能对应不同的认证方式, 但是不同路径可能又会同时存在一些共有的认证方式,这些共有的认证方式可以在parent中统 一处理。
  根据上面的介绍,图 3-2是ProviderManager和AuthenticationProvider关系图。
  
图 3-2
  我们重点看一下ProviderManager中的authenticate方法:
 
查看代码
  1.  public Authentication authenticate(Authentication authentication) throws AuthenticationException {
  2.         Class<? extends Authentication> toTest = authentication.getClass();
  3.         AuthenticationException lastException = null;
  4.         AuthenticationException parentException = null;
  5.         Authentication result = null;
  6.         Authentication parentResult = null;
  7.         boolean debug = logger.isDebugEnabled();
  8.         Iterator var8 = this.getProviders().iterator();
  9.         while(var8.hasNext()) {
  10.             AuthenticationProvider provider = (AuthenticationProvider)var8.next();
  11.             if (provider.supports(toTest)) {
  12.                 if (debug) {
  13.                     logger.debug("Authentication attempt using " + provider.getClass().getName());
  14.                 }
  15.                 try {
  16.                     result = provider.authenticate(authentication);
  17.                     if (result != null) {
  18.                         this.copyDetails(authentication, result);
  19.                         break;
  20.                     }
  21.                 } catch (InternalAuthenticationServiceException | AccountStatusException var13) {
  22.                     this.prepareException(var13, authentication);
  23.                     throw var13;
  24.                 } catch (AuthenticationException var14) {
  25.                     lastException = var14;
  26.                 }
  27.             }
  28.         }
  29.         if (result == null && this.parent != null) {
  30.             try {
  31.                 result = parentResult = this.parent.authenticate(authentication);
  32.             } catch (ProviderNotFoundException var11) {
  33.             } catch (AuthenticationException var12) {
  34.                 parentException = var12;
  35.                 lastException = var12;
  36.             }
  37.         }
  38.         if (result != null) {
  39.             if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) {
  40.                 ((CredentialsContainer)result).eraseCredentials();
  41.             }
  42.             if (parentResult == null) {
  43.                 this.eventPublisher.publishAuthenticationSuccess(result);
  44.             }
  45.             return result;
  46.         } else {
  47.             if (lastException == null) {
  48.                 lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound", new Object[]{toTest.getName()}, "No AuthenticationProvider found for {0}"));
  49.             }
  50.             if (parentException == null) {
  51.                 this.prepareException((AuthenticationException)lastException, authentication);
  52.             }
  53.             throw lastException;
  54.         }
  55.     }
复制代码

  • 首先通过requiresAuthentication方法来判断当前请求是不是登录认证请求,如果是认证请求,就执行接下来的认证代码;如果不是认证请求,则直接继续走剩余的过滤器即可。
  • 调用attemptAuthentication方法来获取一个经过认证后的Authentication对象, attemptAuthentication方法是一个抽象方法,具体实现在它的子类 UsernamePasswordAuthenticationFilter 中。
  • 认证成功后,通过sessionStrategy.onAuthentication方法来处理session并发问题。
  • continueChainBeforeSuccessfulAuthentication变量用来判断请求是否还需要继续向下走,默认情况下该参数的值为false,即认证成功后,后续的过滤器将不再执行了。
  • unsuccessfulAuthentication方法用来处理认证失败事宜,主要做了三件事:①从 SecurityContextHolder中清除数据;②清除Cookie等信息;③调用认证失败的回调方法。
  • successfulAuthentication方法主要用来处理认证成功事宜,主要做了四件事:①向 SecurityContextHolder中存入用户信息;②处理Cookie;③发布认证成功事件,这个事件类型 InteractiveAuthenticationSuccessEvent,表示通过一些自动交互的方式认证成功,例如通过 RememberMe的方式登录;④调用认证成功的回调方法。
  这就是 AbstractAuthenticationProcessingFilter大致上所做的事情,还有一个抽象方法 attemptAuthentication 是在它的继承类 UsernamePasswordAuthenticationFilter中实现的,接下来我们来看—下UsernamePasswordAuthenticationFilter类:
查看代码
  1.  public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
  2.                 implements ApplicationEventPublisherAware, MessageSourceAware {
  3.         protected ApplicationEventPublisher eventPublisher;
  4.         protected AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource();
  5.         private AuthenticationManager authenticationManager;
  6.         protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
  7.         private RememberMeServices rememberMeServices = new NullRememberMeServices();
  8.         private RequestMatcher requiresAuthenticationRequestMatcher;
  9.         private boolean continueChainBeforeSuccessfulAuthentication = false;
  10.         private SessionAuthenticationStrategy sessionStrategy = new NullAuthenticatedSessionStrategy();
  11.         private boolean allowSessionCreation = true;
  12.         private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
  13.         private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();
  14.         protected AbstractAuthenticationProcessingFilter(String defaultFilterProcessesUrl) {
  15.                 setFilterProcessesUrl(defaultFilterProcessesUrl);
  16.         }
  17.         protected AbstractAuthenticationProcessingFilter(
  18.                         RequestMatcher requiresAuthenticationRequestMatcher) {
  19.                 Assert.notNull(requiresAuthenticationRequestMatcher,
  20.                                 "requiresAuthenticationRequestMatcher cannot be null");
  21.                 this.requiresAuthenticationRequestMatcher = requiresAuthenticationRequestMatcher;
  22.         }
  23.         @Override
  24.         public void afterPropertiesSet() {
  25.                 Assert.notNull(authenticationManager, "authenticationManager must be specified");
  26.         }
  27.         public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
  28.                         throws IOException, ServletException {
  29.                 HttpServletRequest request = (HttpServletRequest) req;
  30.                 HttpServletResponse response = (HttpServletResponse) res;
  31.                 if (!requiresAuthentication(request, response)) {
  32.                         chain.doFilter(request, response);
  33.                         return;
  34.                 }
  35.                 if (logger.isDebugEnabled()) {
  36.                         logger.debug("Request is to process authentication");
  37.                 }
  38.                 Authentication authResult;
  39.                 try {
  40.                         authResult = attemptAuthentication(request, response);
  41.                         if (authResult == null) {
  42.                                 return;
  43.                         }
  44.                         sessionStrategy.onAuthentication(authResult, request, response);
  45.                 }
  46.                 catch (InternalAuthenticationServiceException failed) {
  47.                         logger.error(
  48.                                         "An internal error occurred while trying to authenticate the user.",
  49.                                         failed);
  50.                         unsuccessfulAuthentication(request, response, failed);
  51.                         return;
  52.                 }
  53.                 catch (AuthenticationException failed) {
  54.                         unsuccessfulAuthentication(request, response, failed);
  55.                         return;
  56.                 }
  57.                 if (continueChainBeforeSuccessfulAuthentication) {
  58.                         chain.doFilter(request, response);
  59.                 }
  60.                 successfulAuthentication(request, response, chain, authResult);
  61.         }
  62.         protected boolean requiresAuthentication(HttpServletRequest request,
  63.                         HttpServletResponse response) {
  64.                 return requiresAuthenticationRequestMatcher.matches(request);
  65.         }
  66.         public abstract Authentication attemptAuthentication(HttpServletRequest request,
  67.                         HttpServletResponse response) throws AuthenticationException, IOException,
  68.                         ServletException;
  69.         protected void successfulAuthentication(HttpServletRequest request,
  70.                         HttpServletResponse response, FilterChain chain, Authentication authResult)
  71.                         throws IOException, ServletException {
  72.                 if (logger.isDebugEnabled()) {
  73.                         logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
  74.                                         + authResult);
  75.                 }
  76.                 SecurityContextHolder.getContext().setAuthentication(authResult);
  77.                 rememberMeServices.loginSuccess(request, response, authResult);
  78.                 if (this.eventPublisher != null) {
  79.                         eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
  80.                                         authResult, this.getClass()));
  81.                 }
  82.                 successHandler.onAuthenticationSuccess(request, response, authResult);
  83.         }
  84.         protected void unsuccessfulAuthentication(HttpServletRequest request,
  85.                         HttpServletResponse response, AuthenticationException failed)
  86.                         throws IOException, ServletException {
  87.                 SecurityContextHolder.clearContext();
  88.                 if (logger.isDebugEnabled()) {
  89.                         logger.debug("Authentication request failed: " + failed.toString(), failed);
  90.                         logger.debug("Updated SecurityContextHolder to contain null Authentication");
  91.                         logger.debug("Delegating to authentication failure handler " + failureHandler);
  92.                 }
  93.                 rememberMeServices.loginFail(request, response);
  94.                 failureHandler.onAuthenticationFailure(request, response, failed);
  95.         }
  96. }
复制代码

  • 首先声明了默认情况下登录表单的用户名字段和密码字段,用户名字段的key默认是username,密码字段的key默认是password。当然,这两个字段也可以自定义,自定义的方式就是我们在 SecurityConfig 中配置的  .usernameParameter("uname")和  .passwordParameter("passwd")(参考前几节)
  • 在UsernamePasswordAuthenticationFilter过滤器构建的时候,指定了当前过滤器只用来处理登录请求,默认的登录请求是/login,当然开发者也可以自行配置。
  • 接下来就是最重要的attemptAuthentication方法了,在该方法中,首先确认请求是 post类型;然后通过obtainUsername和obtainPassword方法分别从请求中提取出用户名和密码, 具体的提取过程就是调用request.getParameter方法;拿到登录请求传来的用户名/密码之后, 构造出一个 authRequest,然后调用 getAuthenticationManager().authenticate 方法进行认证,这就进入到我们前面所说的ProviderManager的流程中了,具体认证过程就不再赘述了。
  以上就是整个认证流程。
  搞懂了认证流程,那么接下来如果想要自定义一些认证方式,就会非常容易了,比如定义多个数据源、添加登录校验码等。下面,我们将通过两个案例,来活学活用上面所讲的认证流程。


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

乌市泽哥

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表