Spring Security认证流程

前言

Spring Seuciry相关的内容看了实在是太多了,但总以为仍是理解地不够巩固,仍是须要靠知识输出作巩固。 java

相关版本:spring

java: jdk 8  
spring-boot: 2.1.6.RELEASE

过滤器链和认证过程

Spring Security FilterChain

一个认证过程,其实就是过滤器链上的一个绿色矩形Filter所要执行的过程。 数据库

基本的认证过程有三步骤:springboot

  1. Filter拦截请求,生成一个未认证的Authentication,交由AuthenticationManager进行认证;
  2. AuthenticationManager的默认实现ProviderManager会经过AuthenticationProviderAuthentication进行认证,其自己不作认证处理;
  3. 若是认证经过,则建立一个认证经过的Authentication返回;不然抛出异常,以表示认证不经过。

要理解这个过程,能够从类UsernamePasswordAuthenticationFilterProviderManagerDaoAuthenticationProviderInMemoryUserDetailsManagerUserDetailsService实现类,由UserDetailsServiceAutoConfiguration默认配置提供)进行了解。只要建立一个含有spring-boot-starter-security的springboot项目,在适当地打上断点接口看到这个流程。app

用认证部门进行讲解

Authentication Flow to Company Flow) ide

请求到前台以后,负责该请求的前台会将请求的内容封装为一个Authentication对象交给认证管理部门认证管理部门仅管理认证部门,不作具体的认证操做,具体的操做由与该前台相关的认证部门进行处理。固然,每一个认证部门须要判断Authentication是否为该部门负责,是则由该部门负责处理,不然交给下一个部门处理。认证部门认证成功以后会建立一个认证经过的Authentication返回。不然要么抛出异常表示认证不经过,要么交给下一个部门处理。spring-boot

若是须要新增认证类型,只要增长相应的前台(Filter)和与该前台(Filter)想对应的认证部门(AuthenticationProvider)就便可,固然也能够增长一个与已有前台对应的认证部门认证部门会经过前台生成的Authentication来判断该认证是否由该部门负责,于是也许提供一个二者相互认同的Authentication. 工具

认证部门须要人员资料时,则能够从人员资料部门获取。不一样的系统有不一样的人员资料部门,须要咱们提供该人员资料部门,不然将拿到空白档案。固然,人员资料部门不必定是惟一的,认证部门能够有本身的专属资料部门post

上图还能够有以下的画法:this

Authentication Flow to Company Flow

这个画法可能会和FilterChain更加符合。每个前台其实就是FilterChain中的一个,客户拿着请求逐个前台请求认证,找到正确的前台以后进行认证判断。

前台(Filter)

这里的前台Filter仅仅指实现认证的Filter,Spring Security Filter Chain中处理这些Filter还有其余的Filter,好比CsrfFilter。若是非要给角色给他们,那么就当他们是保安人员吧。

Spring Security为咱们提供了3个已经实现的Filter。UsernamePasswordAuthenticationFilterBasicAuthenticationFilterRememberMeAuthenticationFilter。若是不作任何个性化的配置,UsernamePasswordAuthenticationFilterBasicAuthenticationFilter会在默认的过滤器链中。这两种认证方式也就是默认的认证方式。

UsernamePasswordAuthenticationFilter仅仅会对/login路径生效,也就是说UsernamePasswordAuthenticationFilter负责发布认证,发布认证的接口为/login

public class UsernamePasswordAuthenticationFilter extends
        AbstractAuthenticationProcessingFilter {
    ...
    public UsernamePasswordAuthenticationFilter() {
        super(new AntPathRequestMatcher("/login", "POST"));
    }
    ...
}

UsernamePasswordAuthenticationFilter为抽象类AbstractAuthenticationProcessingFilter的一个实现,而BasicAuthenticationFilter为抽象类BasicAuthenticationFilter的一个实现。这四个类的源码提供了不错的前台(Filter)实现思路。

AbstractAuthenticationProcessingFilter

AbstractAuthenticationProcessingFilter 提供了认证先后须要作的事情,其子类只须要提供实现完成认证的抽象方法attemptAuthentication(HttpServletRequest, HttpServletResponse)便可。使用AbstractAuthenticationProcessingFilter时,须要提供一个拦截路径(使用AntPathMatcher进行匹配)来拦截对应的特定的路径。

UsernamePasswordAuthenticationFilter

UsernamePasswordAuthenticationFilter做为实际的前台,会将客户端提交的username和password封装成一个UsernamePasswordAuthenticationToken交给认证管理部门(AuthenticationManager)进行认证。如此,她的任务就完成了。

BasicAuthenticationFilter
前台(Filter)只会处理含有Authorization的Header,且小写化后的值以basic开头的请求,不然该前台(Filter)不负责处理。该Filter会从header中获取Base64编码以后的username和password,建立UsernamePasswordAuthenticationToken提供给认证管理部门(AuthenticationMananager)进行认证。

认证资料(Authentication)

前台接到请求以后,会从请求中获取所需的信息,建立自家认证部门(AuthenticationProvider)所认识的认证资料(Authentication)认证部门(AuthenticationProvider)则主要是经过认证资料(Authentication)的类型判断是否由该部门处理。

public interface Authentication extends Principal, Serializable {
    
    // 该principal具备的权限。AuthorityUtils工具类提供了一些方便的方法。
    Collection<? extends GrantedAuthority> getAuthorities();
    // 证实Principal的身份的证书,好比密码。
    Object getCredentials();
    // authentication request的附加信息,好比ip。
    Object getDetails();
    // 当事人。在username+password模式中为username,在有userDetails以后能够为userDetails。
    Object getPrincipal();
    // 是否已经经过认证。
    boolean isAuthenticated();
    // 设置经过认证。
    void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

Authentication被认证以后,会保存到一个thread-local的SecurityContext中。

// 设置
SecurityContextHolder.getContext().setAuthentication(anAuthentication);
// 获取
Authentication existingAuth = SecurityContextHolder.getContext()
                .getAuthentication();

在写前台Filter的时候,能够先检查SecurityContextHolder.getContext()中是否已经存在经过认证的Authentication了,若是存在,则能够直接跳过该Filter。已经经过验证的Authentication建议设置为一个不可修改的实例。

目前从Authentication的类图中看到的实现类,均为Authentication的抽象子类AbstractAuthenticationToken的实现类。实现类有好几个,与前面的讲到的Filter相关的有UsernamePasswordAuthenticationTokenRememberMeAuthenticationToken

AbstractAuthenticationTokenCredentialsContainerAuthentication的子类。实现了一些简单的方法,但主要的方法还须要实现。该类的getName()方法的实现能够看到经常使用的principal类为UserDetailsAuthenticationPrincipalPrincial。若是有须要将对象设置为principal,能够考虑继承这三个类中的一个。

public String getName() {
    if (this.getPrincipal() instanceof UserDetails) {
        return ((UserDetails) this.getPrincipal()).getUsername();
    }
    if (this.getPrincipal() instanceof AuthenticatedPrincipal) {
        return ((AuthenticatedPrincipal) this.getPrincipal()).getName();
    }
    if (this.getPrincipal() instanceof Principal) {
        return ((Principal) this.getPrincipal()).getName();
    }

    return (this.getPrincipal() == null) ? "" : this.getPrincipal().toString();
}

认证管理部门(AuthenticationManager)

AuthenticationManager是一个接口,认证Authentication,若是认证经过以后,返回的Authentication应该带上该principal所具备的GrantedAuthority

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

该接口的注释中说明,必须按照以下的异常顺序进行检查和抛出:

  1. DisabledException:帐号不可用
  2. LockedException:帐号被锁
  3. BadCredentialsException:证书不正确

Spring Security提供一个默认的实现ProviderManager认证管理部门(ProviderManager)仅执行管理职能,具体的认证职能由认证部门(AuthenticationProvider)执行。

public class ProviderManager implements AuthenticationManager, MessageSourceAware,
        InitializingBean {
    ...

    public ProviderManager(List<AuthenticationProvider> providers) {
        this(providers, null);
    }

    public ProviderManager(List<AuthenticationProvider> providers,
            AuthenticationManager parent) {
        Assert.notNull(providers, "providers list cannot be null");
        this.providers = providers;
        this.parent = parent;
        checkState();
    }

    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();

        for (AuthenticationProvider provider : getProviders()) {
            // #1, 检查是否由该认证部门进行认证`AuthenticationProvider`
            if (!provider.supports(toTest)) {
                continue;
            }

            if (debug) {
                logger.debug("Authentication attempt using "
                        + provider.getClass().getName());
            }

            try {
                // #2, 认证部门进行认证
                result = provider.authenticate(authentication);

                if (result != null) {
                    copyDetails(authentication, result);
                    // #3,认证经过则再也不进行下一个认证部门的认证,不然抛出的异常被捕获,执行下一个认证部门(AuthenticationProvider)
                    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;
            }
        }

        if (result == null && parent != null) {
            // Allow the parent to try.
            try {
                result = parentResult = parent.authenticate(authentication);
            }
            catch (ProviderNotFoundException e) {
                // ignore as we will throw below if no other exception occurred prior to
                // calling parent and the parent
                // may throw ProviderNotFound even though a provider in the child already
                // handled the request
            }
            catch (AuthenticationException e) {
                lastException = parentException = e;
            }
        }
        // #4, 若是认证经过,执行认证经过以后的操做
        if (result != null) {
            if (eraseCredentialsAfterAuthentication
                    && (result instanceof CredentialsContainer)) {
                // Authentication is complete. Remove credentials and other secret data
                // from authentication
                ((CredentialsContainer) result).eraseCredentials();
            }

            // If the parent AuthenticationManager was attempted and successful than it will publish an AuthenticationSuccessEvent
            // This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it
            if (parentResult == null) {
                eventPublisher.publishAuthenticationSuccess(result);
            }
            return result;
        }

        // Parent was null, or didn't authenticate (or throw an exception).
        // #5,若是认证不经过,必然有抛出异常,不然表示没有配置相应的认证部门(AuthenticationProvider)
        if (lastException == null) {
            lastException = new ProviderNotFoundException(messages.getMessage(
                    "ProviderManager.providerNotFound",
                    new Object[] { toTest.getName() },
                    "No AuthenticationProvider found for {0}"));
        }

        // If the parent AuthenticationManager was attempted and failed than it will publish an AbstractAuthenticationFailureEvent
        // This check prevents a duplicate AbstractAuthenticationFailureEvent if the parent AuthenticationManager already published it
        if (parentException == null) {
            prepareException(lastException, authentication);
        }

        throw lastException;
    }
    ...
}
  1. 遍历全部的认证部门(AuthenticationProvider),找到支持的认证部门进行认证
  2. 认证部门进行认证
  3. 认证经过则再也不进行下一个认证部门的认证,不然抛出的异常被捕获,执行下一个认证部门(AuthenticationProvider)
  4. 若是认证经过,执行认证经过以后的操做
  5. 若是认证不经过,必然有抛出异常,不然表示没有配置相应的认证部门(AuthenticationProvider)

当使用到Spring Security OAuth2的时候,会看到另外一个实现OAuth2AuthenticationManager

认证部门(AuthenticationProvider)

认证部门(AuthenticationProvider)负责实际的认证工做,与认证管理部门(ProvderManager)协同工做。也许其余的认证管理部门(AuthenticationManager)并不须要认证部门(AuthenticationProvider)的协做。

public interface AuthenticationProvider {
    // 进行认证
    Authentication authenticate(Authentication authentication)
            throws AuthenticationException;
    // 是否由该AuthenticationProvider进行认证
    boolean supports(Class<?> authentication);
}

该接口有不少的实现类,其中包含了RememberMeAuthenticationProvider(直接AuthenticationProvider)和DaoAuthenticationProvider(经过AbastractUserDetailsAuthenticationProvider简介继承)。这里重点讲讲AbastractUserDetailsAuthenticationProviderDaoAuthenticationProvider

AbastractUserDetailsAuthenticationProvider

顾名思义,AbastractUserDetailsAuthenticationProvider是对UserDetails支持的Provider,其余的Provider,如RememberMeAuthenticationProvider就不须要用到UserDetails。该抽象类有两个抽象方法须要实现类完成:

// 获取 UserDetails
protected abstract UserDetails retrieveUser(String username,
        UsernamePasswordAuthenticationToken authentication)
        throws AuthenticationException;

protected abstract void additionalAuthenticationChecks(UserDetails userDetails,
        UsernamePasswordAuthenticationToken authentication)
        throws AuthenticationException;

retrieveUser()方法为校验提供UserDetails。先看下UserDetails:

public interface UserDetails extends Serializable {
    
    Collection<? extends GrantedAuthority> getAuthorities();

    String getPassword();
    
    String getUsername();
    // 帐号是否过时
    boolean isAccountNonExpired();
    // 帐号是否被锁
    boolean isAccountNonLocked();
    // 证书(password)是否过时
    boolean isCredentialsNonExpired();
    // 帐号是否可用
    boolean isEnabled();
}

AbastractUserDetailsAuthenticationProvider#authentication(Authentication)分为三步验证:

  1. preAuthenticationChecks.check(user);
  2. additionalAuthenticationChecks(user,
    (UsernamePasswordAuthenticationToken) authentication);
  3. postAuthenticationChecks.check(user);

preAuthenticationChecks的默认实现为DefaultPreAuthenticationChecks,负责完成校验:

  1. UserDetails#isAccountNonLocked()
  2. UserDetails#isEnabled()
  3. UserDetails#isAccountNonExpired()

postAuthenticationChecks的默认实现为DefaultPostAuthenticationChecks,负责完成校验:

  1. UserDetails#user.isCredentialsNonExpired()

additionalAuthenticationChecks须要由实现类完成。

校验成功以后,AbstractUserDetailsAuthenticationProvider会建立并返回一个经过认证的Authentication

protected Authentication createSuccessAuthentication(Object principal,
        Authentication authentication, UserDetails user) {
    // Ensure we return the original credentials the user supplied,
    // so subsequent attempts are successful even with encoded passwords.
    // Also ensure we return the original getDetails(), so that future
    // authentication events after cache expiry contain the details
    UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
            principal, authentication.getCredentials(),
            authoritiesMapper.mapAuthorities(user.getAuthorities()));
    result.setDetails(authentication.getDetails());

    return result;
}

DaoAuthenticationProvider

以下为DaoAuthenticationProviderAbstractUserDetailsAuthenticationProvider抽象方法的实现。

// 检查密码是否正确
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"));
    }
}
// 经过资料室(UserDetailsService)获取UserDetails对象
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;
    }
    ...
}

在以上的代码中,须要提供UserDetailsServicePasswordEncoder实例。只要实例化这两个类,并放入到Spring容器中便可。

资料部门(UserDetailsService)

UserDetailsService接口提供认证过程所需的UserDetails的类,如DaoAuthenticationProvider须要一个UserDetailsService实例。

public interface UserDetailsService {
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

Spring Security提供了两个UserDetailsService的实现:InMemoryUserDetailsManagerJdbcUserDetailsManagerInMemoryUserDetailsManager为默认配置,从UserDetailsServiceAutoConfiguration的配置中能够看出。固然也不容易理解,基于数据库的实现须要增长数据库的配置,不适合作默认实现。这两个类均为UserDetailsManager的实现类,UserDetailsManager定义了UserDetails的CRUD操做。InMemoryUserDetailsManager使用Map<String, MutableUserDetails>作存储。

public interface UserDetailsManager extends UserDetailsService {
    void createUser(UserDetails user);

    void updateUser(UserDetails user);

    void deleteUser(String username);

    void changePassword(String oldPassword, String newPassword);

    boolean userExists(String username);
}

若是咱们须要增长一个UserDetailsService,能够考虑实现UserDetailsService或者UserDetailsManager

增长一个认证流程

到这里,咱们已经知道Spring Security的流程了。从上面的内容能够知道,如要增长一个新的认证方式,只要增长一个[前台(Filter) + 认证部门(AuthenticationProvider) + 资料室(UserDetailsService)]组合便可。事实上,资料室(UserDetailsService)不是必须的,可根据认证部门(AuthenticationProvider)须要实现。

前台 + 认证部门 + 资料室

我会在另外一篇文章中以手机号码+验证码登陆为例进行讲解。

相关文章
相关标签/搜索