Spring Security认证流程分析(6)

news2024/11/15 14:03:47

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 中。

public interface AuthenticationManager {
    Authentication authenticate(Authentication var1) throws AuthenticationException;
}

从 AuthenticationManager 的源码中可以看到,AuthenticationManager 对传入的 Authentication对象进行身份认证,此时传入的Authentication参数只有用户名/密码等简单的属性,如果认证成功,返回的Authentication的属性会得到完全填充,包括用户所具备的角色信息。AuthenticationManager是一个接口,它有着诸多的实现类,开发者也可以自定义 AuthenticationManager的实现类,不过在实际应用中,我们使用最多的是ProviderManager,在 Spring S ecurity 框架中,默认也是使用 ProviderManager。

1.1.2、AuthenticationProvider

Spring Security支持多种不同的认证方式,不同的认证方式对应不同的身份 类型,AuthenticationProvider就是针对不同的身份类型执行具体的身份认证。例如,常见的 DaoAuthenticationProvider 用来支持用户名/密码登录认证,RememberMeAuthenticationProvider 用来支持“记住我”的认证。

public interface AuthenticationProvider {
    Authentication authenticate(Authentication var1) throws AuthenticationException;
    boolean supports(Class<?> var1);
}
  • authenticate方法用来执行具体的身份忧证。
  • supports方法用来判断当前AuthenticationProvider是否支持对应的身份类型。

当使用用户名/密码的方式登录时,对应的AuthenticationProvider实现类是 DaoAuthenticationProvider , 而 DaoAuthenticationProvider 继承自 AbstractUserDetailsAuthenticationProvider并且没有重写authenticate方法,所以具体的认证逻辑在AbstractUserDetailsAuthenticationProvider 的 authenticate 方法中。我们就从 AbstractUserDetailsAuthenticationProvider开始看起:

 public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware {
    protected final Log logger = LogFactory.getLog(this.getClass());
    protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
    private UserCache userCache = new NullUserCache();
    private boolean forcePrincipalAsString = false;
    protected boolean hideUserNotFoundExceptions = true;
    private UserDetailsChecker preAuthenticationChecks = new AbstractUserDetailsAuthenticationProvider.DefaultPreAuthenticationChecks();
    private UserDetailsChecker postAuthenticationChecks = new AbstractUserDetailsAuthenticationProvider.DefaultPostAuthenticationChecks();
    private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();
 
    public AbstractUserDetailsAuthenticationProvider() {
    }
 
    protected abstract void additionalAuthenticationChecks(UserDetails var1, UsernamePasswordAuthenticationToken var2) throws AuthenticationException;
 
    public final void afterPropertiesSet() throws Exception {
        Assert.notNull(this.userCache, "A user cache must be set");
        Assert.notNull(this.messages, "A message source must be set");
        this.doAfterPropertiesSet();
    }
 
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> {
            return this.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 {
                user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
            } catch (UsernameNotFoundException var6) {
                this.logger.debug("User '" + username + "' not found");
                if (this.hideUserNotFoundExceptions) {
                    throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
                }
 
                throw var6;
            }
 
            Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
        }
 
        try {
            this.preAuthenticationChecks.check(user);
            this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
        } catch (AuthenticationException var7) {
            if (!cacheWasUsed) {
                throw var7;
            }
 
            cacheWasUsed = false;
            user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
            this.preAuthenticationChecks.check(user);
            this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
        }
 
        this.postAuthenticationChecks.check(user);
        if (!cacheWasUsed) {
            this.userCache.putUserInCache(user);
        }
 
        Object principalToReturn = user;
        if (this.forcePrincipalAsString) {
            principalToReturn = user.getUsername();
        }
 
        return this.createSuccessAuthentication(principalToReturn, authentication, user);
    }
 
    protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
        UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal, authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
        result.setDetails(authentication.getDetails());
        return result;
    }
 
    protected void doAfterPropertiesSet() throws Exception {
    }
 
    public UserCache getUserCache() {
        return this.userCache;
    }
 
    public boolean isForcePrincipalAsString() {
        return this.forcePrincipalAsString;
    }
 
    public boolean isHideUserNotFoundExceptions() {
        return this.hideUserNotFoundExceptions;
    }
 
    protected abstract UserDetails retrieveUser(String var1, UsernamePasswordAuthenticationToken var2) throws AuthenticationException;
 
    public void setForcePrincipalAsString(boolean forcePrincipalAsString) {
        this.forcePrincipalAsString = forcePrincipalAsString;
    }
 
    public void setHideUserNotFoundExceptions(boolean hideUserNotFoundExceptions) {
        this.hideUserNotFoundExceptions = hideUserNotFoundExceptions;
    }
 
    public void setMessageSource(MessageSource messageSource) {
        this.messages = new MessageSourceAccessor(messageSource);
    }
 
    public void setUserCache(UserCache userCache) {
        this.userCache = userCache;
    }
 
    public boolean supports(Class<?> authentication) {
        return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
    }
 
    protected UserDetailsChecker getPreAuthenticationChecks() {
        return this.preAuthenticationChecks;
    }
 
    public void setPreAuthenticationChecks(UserDetailsChecker preAuthenticationChecks) {
        this.preAuthenticationChecks = preAuthenticationChecks;
    }
 
    protected UserDetailsChecker getPostAuthenticationChecks() {
        return this.postAuthenticationChecks;
    }
 
    public void setPostAuthenticationChecks(UserDetailsChecker postAuthenticationChecks) {
        this.postAuthenticationChecks = postAuthenticationChecks;
    }
 
    public void setAuthoritiesMapper(GrantedAuthoritiesMapper authoritiesMapper) {
        this.authoritiesMapper = authoritiesMapper;
    }
 
    private class DefaultPostAuthenticationChecks implements UserDetailsChecker {
        private DefaultPostAuthenticationChecks() {
        }
 
        public void check(UserDetails user) {
            if (!user.isCredentialsNonExpired()) {
                AbstractUserDetailsAuthenticationProvider.this.logger.debug("User account credentials have expired");
                throw new CredentialsExpiredException(AbstractUserDetailsAuthenticationProvider.this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.credentialsExpired", "User credentials have expired"));
            }
        }
    }
 
    private class DefaultPreAuthenticationChecks implements UserDetailsChecker {
        private DefaultPreAuthenticationChecks() {
        }
 
        public void check(UserDetails user) {
            if (!user.isAccountNonLocked()) {
                AbstractUserDetailsAuthenticationProvider.this.logger.debug("User account is locked");
                throw new LockedException(AbstractUserDetailsAuthenticationProvider.this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.locked", "User account is locked"));
            } else if (!user.isEnabled()) {
                AbstractUserDetailsAuthenticationProvider.this.logger.debug("User account is disabled");
                throw new DisabledException(AbstractUserDetailsAuthenticationProvider.this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.disabled", "User is disabled"));
            } else if (!user.isAccountNonExpired()) {
                AbstractUserDetailsAuthenticationProvider.this.logger.debug("User account is expired");
                throw new AccountExpiredException(AbstractUserDetailsAuthenticationProvider.this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.expired", "User account has expired"));
            }
        }
    }
}
  • 一开始先声明一个用户缓存对象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中的定义:

 public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
    private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";
    private PasswordEncoder passwordEncoder;
    private volatile String userNotFoundEncodedPassword;
    private UserDetailsService userDetailsService;
    private UserDetailsPasswordService userDetailsPasswordService;
 
    public DaoAuthenticationProvider() {
        this.setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
    }
 
    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        if (authentication.getCredentials() == null) {
            this.logger.debug("Authentication failed: no credentials provided");
            throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
        } else {
            String presentedPassword = authentication.getCredentials().toString();
            if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
                this.logger.debug("Authentication failed: password does not match stored value");
                throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
            }
        }
    }
 
    protected void doAfterPropertiesSet() {
        Assert.notNull(this.userDetailsService, "A UserDetailsService must be set");
    }
 
    protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        this.prepareTimingAttackProtection();
 
        try {
            UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
            if (loadedUser == null) {
                throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
            } else {
                return loadedUser;
            }
        } catch (UsernameNotFoundException var4) {
            this.mitigateAgainstTimingAttack(authentication);
            throw var4;
        } catch (InternalAuthenticationServiceException var5) {
            throw var5;
        } catch (Exception var6) {
            throw new InternalAuthenticationServiceException(var6.getMessage(), var6);
        }
    }
 
    protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
        boolean upgradeEncoding = this.userDetailsPasswordService != null && this.passwordEncoder.upgradeEncoding(user.getPassword());
        if (upgradeEncoding) {
            String presentedPassword = authentication.getCredentials().toString();
            String newPassword = this.passwordEncoder.encode(presentedPassword);
            user = this.userDetailsPasswordService.updatePassword(user, newPassword);
        }
 
        return super.createSuccessAuthentication(principal, authentication, user);
    }
 
    private void prepareTimingAttackProtection() {
        if (this.userNotFoundEncodedPassword == null) {
            this.userNotFoundEncodedPassword = this.passwordEncoder.encode("userNotFoundPassword");
        }
 
    }
 
    private void mitigateAgainstTimingAttack(UsernamePasswordAuthenticationToken authentication) {
        if (authentication.getCredentials() != null) {
            String presentedPassword = authentication.getCredentials().toString();
            this.passwordEncoder.matches(presentedPassword, this.userNotFoundEncodedPassword);
        }
 
    }
 
    public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
        Assert.notNull(passwordEncoder, "passwordEncoder cannot be null");
        this.passwordEncoder = passwordEncoder;
        this.userNotFoundEncodedPassword = null;
    }
 
    protected PasswordEncoder getPasswordEncoder() {
        return this.passwordEncoder;
    }
 
    public void setUserDetailsService(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }
 
    protected UserDetailsService getUserDetailsService() {
        return this.userDetailsService;
    }
 
    public void setUserDetailsPasswordService(UserDetailsPasswordService userDetailsPasswordService) {
        this.userDetailsPasswordService = userDetailsPasswordService;
    }
}
  • 首先定义了 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方法:

 public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        Class<? extends Authentication> toTest = authentication.getClass();
        AuthenticationException lastException = null;
        AuthenticationException parentException = null;
        Authentication result = null;
        Authentication parentResult = null;
        boolean debug = logger.isDebugEnabled();
        Iterator var8 = this.getProviders().iterator();
 
        while(var8.hasNext()) {
            AuthenticationProvider provider = (AuthenticationProvider)var8.next();
            if (provider.supports(toTest)) {
                if (debug) {
                    logger.debug("Authentication attempt using " + provider.getClass().getName());
                }
 
                try {
                    result = provider.authenticate(authentication);
                    if (result != null) {
                        this.copyDetails(authentication, result);
                        break;
                    }
                } catch (InternalAuthenticationServiceException | AccountStatusException var13) {
                    this.prepareException(var13, authentication);
                    throw var13;
                } catch (AuthenticationException var14) {
                    lastException = var14;
                }
            }
        }
 
        if (result == null && this.parent != null) {
            try {
                result = parentResult = this.parent.authenticate(authentication);
            } catch (ProviderNotFoundException var11) {
            } catch (AuthenticationException var12) {
                parentException = var12;
                lastException = var12;
            }
        }
 
        if (result != null) {
            if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) {
                ((CredentialsContainer)result).eraseCredentials();
            }
 
            if (parentResult == null) {
                this.eventPublisher.publishAuthenticationSuccess(result);
            }
 
            return result;
        } else {
            if (lastException == null) {
                lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound", new Object[]{toTest.getName()}, "No AuthenticationProvider found for {0}"));
            }
 
            if (parentException == null) {
                this.prepareException((AuthenticationException)lastException, authentication);
            }
 
            throw lastException;
        }
    }

这段源码的逻辑还是非常清晰的,我们分析一下:

  • 首先获取authentication对象的类型。
  • 分别定义当前认证过程抛出的异常、parent中认证时抛出的异常、当前认证结果以及parent中认证结果对应的变量。
  • getProviders 方法用来获取当前 ProviderManager 所代理的所有 AuthenticationProvider 对象,遍历这些AuthenticationProvider对象进行身份认证。
  • 判断当前AuthenticationProvider是否支持当前Authentication对象,要是不支持,则继续处理列表中的下一个AuthenticationProvider对象.
  • 调用provider.authenticate方法进行身份认证,如果认证成功,返回认证后的 Authentication对象,同时调用copyDetails方法给Authentication对象的details属性赋值,由于可能是多个AuthenticationProvider执行认证操作,所以如果抛出异常,则通过lastException 变量来记录。
  • 在for循环执行完成后,如果result还是没有值,说明所有的AuthenticationProvider 都认证失败,此时如果parent不为空,则调用parent的authenticate方法进行认证。
  • 接下来,如果result不为空,就将result中的凭证擦除,防止泄漏,如果使用了用户名/密码的方式登录,那么所谓的擦除实际上就是将密码字段设置为null,同时将登录成功的事件发布出去(发布登录成功事件需要parentResult为null。如果parentResult不为null,表示在parent中已经认证成功了,认证成功的事件也己经在parent中发布出去了,这样会导致认证成功的事件重复发布)。如果用户认证成功,此时就将result返回,后面的代码也就不再执行了。
  • 如果前面没能返回result,说明认证失败。如果lastException为null,说明parent为 null或者没有认证亦或者认证失败了但是没有抛出异常,此时构造ProviderNotFoundException 异常赋值给lastException。
  • 如果parentException为null,发布认证失败事件(如果parentException不为null, 则说明认证失败事件已经发布过了)。
  • 最后抛出lastException异常。

这就是ProviderManager中authenticate方法的身份认证逻辑,其他方法的源码要相对简单很多,在这里就不一一解释了。

现在,大家已经熟悉了 Authentication、AuthenticationManager、AuthenticationProvider 以 及ProviderManager的工作原理了,接下来的问题就是这些组件如何跟登录关联起来?这就涉及一个重要的过滤器----------------------- AbstractAuthenticationProcessingFilter。

1.1.4、AbstractAuthenticationProcessingFilter

作为 Spring Security 过滤器链中的一环,AbstractAuthenticationProcessingFilter可以用来处理任何提交给它的身份认证,图3-3描述了 AbstractAuthenticationProcessingFilter的工作流程:
在这里插入图片描述
图 3-3

图中显示的流程是一个通用的架构。

AbstractAuthenticationProcessingFilter作为一个抽象类,如果使用用户名/密码的方式登录, 那么它对应的实现类是 UsernamePasswordAuthenticationFilter;构造出来的 Authentication 对象则是 UsernamePasswordAuthenticationToken。至于 AuthenticationManager,前面已经说过,一 般情况下它的实现类就是ProviderManager,这里在ProviderManager中进行认证,认证成功就会进入认证成功的回调,否则进入认证失败的回调。因此,我们可以对上面的流程图再做进一 步细化,如图3-4所示。
在这里插入图片描述
图 3-4

前面第2章中所涉及的认证流程基本上就是这样,我们来大致梳理一下:

  • 当用户提交登录请求时,UsernamePasswordAuthenticationFilter会从当前请求 HttpServletRequest中提取出登录用户名/密码,然后创建出一个 UsernamePasswordAuthenticationToken 对象。
  • UsernamePasswordAuthenticationToken 对象将被传入 ProviderManager 中进行具体的认证操作。
  • 如果认证失败,则SecurityContextHolder中相关信息将被清除,登录失败回调也会被调用.
  • 如果认证成功,则会进行登录信息存储、Session并发处理、登录成功事件发布以及登录成功方法回调等操作。

这是认证的一个大致流程。接下来我们结合 AbstractAuthenticationProcessingFilter和 UsernamePasswordAuthenticationFilter的源码来看一下。

先来看 AbstractAuthenticationProcessingFilter源码(部分核心代码):

 public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
		implements ApplicationEventPublisherAware, MessageSourceAware {
	protected ApplicationEventPublisher eventPublisher;
	protected AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource();
	private AuthenticationManager authenticationManager;
	protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
	private RememberMeServices rememberMeServices = new NullRememberMeServices();
	private RequestMatcher requiresAuthenticationRequestMatcher;
	private boolean continueChainBeforeSuccessfulAuthentication = false;
	private SessionAuthenticationStrategy sessionStrategy = new NullAuthenticatedSessionStrategy();
	private boolean allowSessionCreation = true;
	private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
	private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();
	protected AbstractAuthenticationProcessingFilter(String defaultFilterProcessesUrl) {
		setFilterProcessesUrl(defaultFilterProcessesUrl);
	}
 
	protected AbstractAuthenticationProcessingFilter(
			RequestMatcher requiresAuthenticationRequestMatcher) {
		Assert.notNull(requiresAuthenticationRequestMatcher,
				"requiresAuthenticationRequestMatcher cannot be null");
		this.requiresAuthenticationRequestMatcher = requiresAuthenticationRequestMatcher;
	}
 
	@Override
	public void afterPropertiesSet() {
		Assert.notNull(authenticationManager, "authenticationManager must be specified");
	}
 
	public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {
		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;
		if (!requiresAuthentication(request, response)) {
			chain.doFilter(request, response);
			return;
		}
		if (logger.isDebugEnabled()) {
			logger.debug("Request is to process authentication");
		}
		Authentication authResult;
		try {
			authResult = attemptAuthentication(request, response);
			if (authResult == null) {
				return;
			}
			sessionStrategy.onAuthentication(authResult, request, response);
		}
		catch (InternalAuthenticationServiceException failed) {
			logger.error(
					"An internal error occurred while trying to authenticate the user.",
					failed);
			unsuccessfulAuthentication(request, response, failed);
			return;
		}
		catch (AuthenticationException failed) {
			unsuccessfulAuthentication(request, response, failed);
			return;
		}
		if (continueChainBeforeSuccessfulAuthentication) {
			chain.doFilter(request, response);
		}
		successfulAuthentication(request, response, chain, authResult);
	}
 
	protected boolean requiresAuthentication(HttpServletRequest request,
			HttpServletResponse response) {
		return requiresAuthenticationRequestMatcher.matches(request);
	}
 
	public abstract Authentication attemptAuthentication(HttpServletRequest request,
			HttpServletResponse response) throws AuthenticationException, IOException,
			ServletException;
	protected void successfulAuthentication(HttpServletRequest request,
			HttpServletResponse response, FilterChain chain, Authentication authResult)
			throws IOException, ServletException {
		if (logger.isDebugEnabled()) {
			logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
					+ authResult);
		}
		SecurityContextHolder.getContext().setAuthentication(authResult);
		rememberMeServices.loginSuccess(request, response, authResult);
		if (this.eventPublisher != null) {
			eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
					authResult, this.getClass()));
		}
		successHandler.onAuthenticationSuccess(request, response, authResult);
	}
 
	protected void unsuccessfulAuthentication(HttpServletRequest request,
			HttpServletResponse response, AuthenticationException failed)
			throws IOException, ServletException {
		SecurityContextHolder.clearContext();
 
		if (logger.isDebugEnabled()) {
			logger.debug("Authentication request failed: " + failed.toString(), failed);
			logger.debug("Updated SecurityContextHolder to contain null Authentication");
			logger.debug("Delegating to authentication failure handler " + failureHandler);
		}
 
		rememberMeServices.loginFail(request, response);
 
		failureHandler.onAuthenticationFailure(request, response, failed);
	}
}
  • 首先通过requiresAuthentication方法来判断当前请求是不是登录认证请求,如果是认证请求,就执行接下来的认证代码;如果不是认证请求,则直接继续走剩余的过滤器即可。
  • 调用attemptAuthentication方法来获取一个经过认证后的Authentication对象, attemptAuthentication方法是一个抽象方法,具体实现在它的子类 UsernamePasswordAuthenticationFilter 中。
  • 认证成功后,通过sessionStrategy.onAuthentication方法来处理session并发问题。
  • continueChainBeforeSuccessfulAuthentication变量用来判断请求是否还需要继续向下走,默认情况下该参数的值为false,即认证成功后,后续的过滤器将不再执行了。
  • unsuccessfulAuthentication方法用来处理认证失败事宜,主要做了三件事:①从 SecurityContextHolder中清除数据;②清除Cookie等信息;③调用认证失败的回调方法。
  • successfulAuthentication方法主要用来处理认证成功事宜,主要做了四件事:①向 SecurityContextHolder中存入用户信息;②处理Cookie;③发布认证成功事件,这个事件类型 InteractiveAuthenticationSuccessEvent,表示通过一些自动交互的方式认证成功,例如通过 RememberMe的方式登录;④调用认证成功的回调方法。

这就是 AbstractAuthenticationProcessingFilter大致上所做的事情,还有一个抽象方法 attemptAuthentication 是在它的继承类 UsernamePasswordAuthenticationFilter中实现的,接下来我们来看—下UsernamePasswordAuthenticationFilter类:

public class UsernamePasswordAuthenticationFilter extends
		AbstractAuthenticationProcessingFilter {
	public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
	public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
	private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
	private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
	private boolean postOnly = true;
 
	public UsernamePasswordAuthenticationFilter() {
		super(new AntPathRequestMatcher("/login", "POST"));
	}
 
	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);
		setDetails(request, authRequest);
		return this.getAuthenticationManager().authenticate(authRequest);
	}
    
    @Nullable
	protected String obtainPassword(HttpServletRequest request) {
		return request.getParameter(passwordParameter);
	}
 
	@Nullable
	protected String obtainUsername(HttpServletRequest request) {
		return request.getParameter(usernameParameter);
	}
 
}
  • 首先声明了默认情况下登录表单的用户名字段和密码字段,用户名字段的key默认是username,密码字段的key默认是password。当然,这两个字段也可以自定义,自定义的方式就是我们在 SecurityConfig 中配置的 .usernameParameter(“uname”)和 .passwordParameter(“passwd”)(参考前几节)
  • 在UsernamePasswordAuthenticationFilter过滤器构建的时候,指定了当前过滤器只用来处理登录请求,默认的登录请求是/login,当然开发者也可以自行配置。
  • 接下来就是最重要的attemptAuthentication方法了,在该方法中,首先确认请求是 post类型;然后通过obtainUsername和obtainPassword方法分别从请求中提取出用户名和密码, 具体的提取过程就是调用request.getParameter方法;拿到登录请求传来的用户名/密码之后, 构造出一个 authRequest,然后调用 getAuthenticationManager().authenticate 方法进行认证,这就进入到我们前面所说的ProviderManager的流程中了,具体认证过程就不再赘述了。

以上就是整个认证流程。

总结:
搞懂了认证流程,那么接下来如果想要自定义一些认证方式,就会非常容易了,比如定义多个数据源、添加登录校验码等。下面,我们将通过两个案例,来活学活用上面所讲的认证流程。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1111009.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

竞赛 深度学习交通车辆流量分析 - 目标检测与跟踪 - python opencv

文章目录 0 前言1 课题背景2 实现效果3 DeepSORT车辆跟踪3.1 Deep SORT多目标跟踪算法3.2 算法流程 4 YOLOV5算法4.1 网络架构图4.2 输入端4.3 基准网络4.4 Neck网络4.5 Head输出层 5 最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; *…

Halcon 中查看算子和函数的执行时间

1、在Halcol主窗口的底栏中的第一个图标显示算子或函数的执行时间&#xff0c;如下图&#xff1a; 2、在Halcon的菜单栏中选择【窗口】&#xff0c;在下拉框中选择【打开输出控制台】&#xff0c;进行查看算子或函数的执行时间&#xff0c;如下图&#xff1a;

「神奇的锚点定位:探索UniApp中实现滚动定位效果,与1024程序员节同欢,解析技术之美」

&#x1f3ac; 江城开朗的豌豆&#xff1a;个人主页 &#x1f525; 个人专栏 :《 VUE 》 《 javaScript 》 &#x1f4dd; 个人网站 :《 江城开朗的豌豆&#x1fadb; 》 ⛺️ 生活的理想&#xff0c;就是为了理想的生活 ! 目录 ⭐ 文章简介 &#x1f4d8; 文章背景 &#…

小程序中如何使用自定义组件应用及搭建个人中心布局

一&#xff0c;自定义组件 从小程序基础库版本 1.6.3 开始&#xff0c;小程序支持简洁的组件化编程。所有自定义组件相关特性都需要基础库版本 1.6.3 或更高。 开发者可以将页面内的功能模块抽象成自定义组件&#xff0c;以便在不同的页面中重复使用&#xff1b;也可以将复杂的…

【Godot引擎开发】简单基础,外加一个小游戏DEMO

博主&#xff1a;_LJaXi 专栏&#xff1a; Godot | 横版游戏开发 Godot 物体规律移动内置虚函数浮点计算浮点数计算数组APIInput单例与自定义单例节点NodeSprite2DArea2DCollisionShape2DKinematicBody2DRigidBody2D Pong游戏场景安排玩家1玩家2小球记分系统文件概要 下面是介绍…

软件工程与计算总结(二十一)软件维护与演化

一.软件维护 1.软件可修改性和软件维护 产品交付给用户并投入运营之后&#xff0c;接下来的工作被看做软件维护。 因为软件不存在“磨损”的情况&#xff0c;所以与其他工程学科相比&#xff0c;软件维护只需要完成少量的使用帮助、故障解决等工作——但并不意味着维护是简单…

IOday8

#include <head.h>//要发送数据的结构体类型 struct msgbuf {long mtype; /* 消息类型*/char mtext[1024]; /* 正文数据 */}; //宏定义正文大小 #define SIZE sizeof(struct msgbuf)-sizeof(long) int main(int argc, const char *argv[]) {key_t key;if((keyft…

oracle-AWR报告生成方法

AWR报告生成方法 1. 以oracle用户登陆服务器 2. 进入到要保存awr报告的目录 3. 以sysdba身份连接数据库 sqlplus / as sysdba4. 执行生成AWR报告命令 ?/rdbms/admin/awrrpt.sql5. 选择AWR报告的文件格式 6. 选择生成多少天的AWR报告 7. 选择报告的快照起始和结束ID 8. 输入生…

Java 对象是什么样子的?

Java 对象是什么样子的&#xff1f; class Student{ int age; String name; }Student s new Student(18, “zhangsan”); 这里的 s 变量&#xff0c;就是我们常说的引用&#xff0c;这里是强引用。指向对象中的 Java对象。 很多人可能认为&#xff0c;堆中存储了 age 18, na…

Linux学习之进程一

目录 一&#xff0c;什么是进程&#xff1f; 1.何为pcb&#xff08;process control block&#xff09;&#xff1f; 2.linux的task_struct 二&#xff0c;了解task_struct的核心字段 标识符 ps指令 getpid指令 ppid&#xff08;parent process id&#xff09; getppid…

DASFAA2023 | 关系数据库和图数据库关键技术融合趋势

编者按&#xff1a; 本期论文导读为大家介绍近年来关系数据库和图数据库关键技术的融合趋势及代表性工作。相关内容来自DASFAA 2023 Tutorial “Fusion of Relational and Graph Database Techniques: An Emerging Trend”&#xff0c;由北京交通大学刘钰博士、中北大学郭青松…

【持续更新】tutorial-Linux-Markdown-etc(Linux、命令、Markdown、md、Tex、LaTex)

1. Linux命令 1.1 常用 查看文件夹下文件数量: ls -l | wc -l7zip: 解压&#xff1a;7z x compressed_file.7z -o/path/to/destination # 注意-o和目标路径是连起来的&#xff0c;没有空格压缩&#xff1a;7z a compressed_file.zip destination_path conda 查看 conda 拥有的…

@JsonCreator(mode = JsonCreator.Mode.DELEGATING) @JsonValue解释

@JsonCreator(mode = JsonCreator.Mode.DELEGATING)public MessageId(Long id) {this.id = id;}<

【中级网络工程师】下午网络配置题

目录 dis ip routing-table 路由表如何查看 静态路由配置&#xff1a; rip和ospf配置 ACL配置 定义ACL过程 关键字traffic-filter inbound 和 traffic-filter outbpund 两种安装模式 DHCP服务 基于全局分配&#xff1a; 基于接口分配&#xff1a; 如何将划分的VLAN和创…

如何确认栈中申请的变量地址

一般一个程序被加载到内存后执行而成为一个进程。进程在内存中是分区域加载的&#xff0c;分别是代码段、数据段、bss段等等。 函数中定义的变量一般存在于栈中。现在我们通过实验验证一下&#xff0c;函数中定义的变量&#xff0c;到底存在与进程哪个位置。 1.测试程序 #in…

opencv入门到精通——图片,视频,摄像头的读取与保存

简介 OpenCV是一个流行的开源计算机视觉库&#xff0c;由英特尔公司发起发展。它提供了超过2500个优化算法和许多工具包&#xff0c;可用于灰度、彩色、深度、基于特征和运动跟踪等的图像处理和计算机视觉应用。OpenCV主要使用C语言编写&#xff0c;同时也支持Python、Java、C等…

unity操作_碰撞器 c#

碰撞器Collider 在场景中选择一个物体Cube 观察检查器Inspector 自带Cube会默认挂载盒子碰撞器Box Colilider 增加组件可以增加更多中碰撞器 Edit Collider 编辑碰撞器形状 Is Trigger选项 Is Trigger &#xff1a;是否是触发器&#xff0c;如果启用此属性 则该碰撞体将用于触…

分享一下微信小程序里的预约链接怎么做

微信小程序是一种无需下载安装即可使用的应用程序&#xff0c;它依托于微信平台&#xff0c;为用户提供了更加便捷的使用体验。在小程序中&#xff0c;我们可以制作预约链接&#xff0c;以便用户直接在微信中进行预约&#xff0c;提高服务效率。下面我们将探讨如何制作微信小程…

RabbitMQ中的核心概念和交换机类型

目录 一、RabbitMQ相关概念二、Exchange类型三、RabbitMQ概念模型总结 一、RabbitMQ相关概念 Producer&#xff1a;生产者&#xff0c;就是投递消息的一方。生产者创建消息&#xff0c;然后发布到RabbitMQ中。消息一般可以包含两个部分&#xff1a;消息体和附加消息。 消息体…

利用Nginx可视化管理工具+Cpolar实现本地服务远程访问

文章目录 前言1. docker 一键安装2. 本地访问3. Linux 安装cpolar4. 配置公网访问地址5. 公网远程访问6. 固定公网地址 前言 Nginx Proxy Manager 是一个开源的反向代理工具&#xff0c;不需要了解太多 Nginx 或 Letsencrypt 的相关知识&#xff0c;即可快速将你的服务暴露到外…