(六)Spring Security 認證流程

認證流程

在這裏插入圖片描述
圖片來自於:黑馬程序員SpringSecurity認證課程java

認證過程:程序員

  1. 用戶提交用戶名、密碼被SecurityFilterChain中的UsernamePasswordAuthenticationFilter 過濾器獲取到,封裝爲請求Authentication,一般狀況下是UsernamePasswordAuthenticationToken這個實現類。
  2. 而後過濾器將Authentication提交至認證管理器(AuthenticationManager)進行認證
  3. 認證成功後, AuthenticationManager身份管理器返回一個被填充滿了信息的(包括上面提到的權限信息,身份信息,細節信息,但密碼一般會被移除) Authentication 實例。
  4. SecurityContextHolder 安全上下文容器將第3步填充了信息的 Authentication ,經過SecurityContextHolder.getContext().setAuthentication(…)方法,設置到其中。能夠看出AuthenticationManager接口(認證管理器)是認證相關的核心接口,也是發起認證的出發點,它
    的實現類爲ProviderManager。而Spring Security支持多種認證方式,所以ProviderManager維護着一個
    List 列表,存放多種認證方式,最終實際的認證工做是由
    AuthenticationProvider完成的。我們知道web表單的對應的AuthenticationProvider實現類爲
    DaoAuthenticationProvider,它的內部又維護着一個UserDetailsService負責UserDetails的獲取。最終
    AuthenticationProvider將UserDetails填充至Authentication。
    認證核心組件的大致關係以下:
    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Eg2sjTq4-1599134067449)(F7BB8422DBB94B699003912F92800ECA)]
    圖片來自於:黑馬程序員SpringSecurity認證課程

知識點認識

Authentication

咱們所面對的系統中的用戶,在Spring Security中被稱爲主體(principal)。主體包含了全部可以通過驗證而得到系統訪問權限的用戶、設備或其餘系統。主體的概念實際上來自 Java Security,Spring Security經過一層包裝將其定義爲一個Authentication。web

public interface Authentication extends Principal, Serializable { 
    // 獲取主體受權列表
    Collection<? extends GrantedAuthority> getAuthorities();

    // 獲取主體憑證,通常爲密碼
    Object getCredentials();
    
    // 獲取主體攜帶的詳細信息
    Object getDetails();
    
    // 獲取主體,一般爲username
    Object getPrincipal();

    // 獲取當前主體是否定證成功
    boolean isAuthenticated();

    // 設置當前主體是否定證成功狀態
    void setAuthenticated(boolean var1) throws IllegalArgumentException;
}

AuthenticateProvider

Spring Security 認證的過程其實就是一個構建Authentication的過程。Authentication 在Spring Security的各個AuthenticationProvider中流動,AuthenticationProvider被Spring Security定義爲一個驗證過程:spring

public interface AuthenticationProvider { 
    // 驗證完成,成功,返回一個驗證完成的Authentication
    Authentication authenticate(Authentication var1) throws AuthenticationException;

    // 是否支持驗證當前的Authentication類型
    boolean supports(Class<?> var1);
}

大部分場景下身份驗證都是基於用戶名和密碼進行的,因此Spring Security提供了一個UsernamePasswordAuthenticationToken用於代指這一類證。,每個登陸用戶即主體都被包裝爲一個UsernamePasswordAuthenticationToken,從而在Spring Security的各個AuthenticationProvider中流動。數據庫

ProviderManager

一次完整的認證能夠包含多個AuthenticationProvider,通常由ProviderManager管理。安全

public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean { 
    private static final Log logger = LogFactory.getLog(ProviderManager.class);
    private AuthenticationEventPublisher eventPublisher;
    
    // AuthenticationProvider 列表
    private List<AuthenticationProvider> providers;
    protected MessageSourceAccessor messages;
    private AuthenticationManager parent;
    private boolean eraseCredentialsAfterAuthentication;

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

    public ProviderManager(List<AuthenticationProvider> providers, AuthenticationManager parent) { 
        this.eventPublisher = new ProviderManager.NullEventPublisher();
        this.providers = Collections.emptyList();
        this.messages = SpringSecurityMessageSource.getAccessor();
        this.eraseCredentialsAfterAuthentication = true;
        Assert.notNull(providers, "providers list cannot be null");
        this.providers = providers;
        this.parent = parent;
        this.checkState();
    }

    public void afterPropertiesSet() { 
        this.checkState();
    }

    private void checkState() { 
        if (this.parent == null && this.providers.isEmpty()) { 
            throw new IllegalArgumentException("A parent AuthenticationManager or a list of AuthenticationProviders is required");
        }
    }

    // 迭代AuthenticationProvider 列表,進行認證,返回最終結果
    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();
            // 判斷AuthenticationProvider 是否支持當前驗證
            if (provider.supports(toTest)) { 
                if (debug) { 
                    logger.debug("Authentication attempt using " + provider.getClass().getName());
                }

                try { 
                    // 執行AuthenticationProvider的認證。
                    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;
        }
    }

    private void prepareException(AuthenticationException ex, Authentication auth) { 
        this.eventPublisher.publishAuthenticationFailure(ex, auth);
    }

    private void copyDetails(Authentication source, Authentication dest) { 
        if (dest instanceof AbstractAuthenticationToken && dest.getDetails() == null) { 
            AbstractAuthenticationToken token = (AbstractAuthenticationToken)dest;
            token.setDetails(source.getDetails());
        }

    }

    public List<AuthenticationProvider> getProviders() { 
        return this.providers;
    }

    public void setMessageSource(MessageSource messageSource) { 
        this.messages = new MessageSourceAccessor(messageSource);
    }

    public void setAuthenticationEventPublisher(AuthenticationEventPublisher eventPublisher) { 
        Assert.notNull(eventPublisher, "AuthenticationEventPublisher cannot be null");
        this.eventPublisher = eventPublisher;
    }

    public void setEraseCredentialsAfterAuthentication(boolean eraseSecretData) { 
        this.eraseCredentialsAfterAuthentication = eraseSecretData;
    }

    public boolean isEraseCredentialsAfterAuthentication() { 
        return this.eraseCredentialsAfterAuthentication;
    }

    private static final class NullEventPublisher implements AuthenticationEventPublisher { 
        private NullEventPublisher() { 
        }

        public void publishAuthenticationFailure(AuthenticationException exception, Authentication authentication) { 
        }

        public void publishAuthenticationSuccess(Authentication authentication) { 
        }
    }
}

自定義AuthenticationProvider

Spring Security提供了多種常見的認證技術,包括但不限於如下幾種:app

  • HTTP層面的認證技術,包括HTTP基本認證和HTTP摘要認證兩種。
  • 基於LDAP的認證技術(Lightweight Directory Access Protocol,輕量目錄訪問協議)。
  • 聚焦於證實用戶身份的OpenID認證技術。
  • 聚焦於受權的OAuth認證技術。
  • 系統內維護的用戶名和密碼認證技術。
    其中,使用最爲普遍的是由系統維護的用戶名和密碼認證技術,一般會涉及數據庫訪問。爲了更好地按需定製,Spring Security 並無直接糅合整個認證過程,而是提供了一個抽象的AuthenticationProvider,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();
        }

        // 返回一個認證經過的Authentication
        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;
    }

    // 此認證支持UsernamePasswordAuthenticationToken及其衍生類認證
    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"));
            }
        }
    }
}

在 AbstractUserDetailsAuthenticationProvider中實現了基本的認證流程,經過繼承AbstractUserDetailsAuthenticationProvider,並實現retrieveUser和additionalAuthenticationChecks兩個抽象方法便可自定義核心認證過程,靈活性很是高。示例,Spring Security 用於處理UsernamePasswordAuthenticationToken的DaoAuthenticationProvider:ide

public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider { 
    private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";
    // 密碼加密
    private PasswordEncoder passwordEncoder;
    private volatile String userNotFoundEncodedPassword;
    
    // UserDetailsService 用來獲取用戶信息
    private UserDetailsService userDetailsService;
    private UserDetailsPasswordService userDetailsPasswordService;

    public DaoAuthenticationProvider() { 
        this.setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
    }

    // 添加附加認證
    @Override
    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");
    }
    
    // 獲取用戶
    @Override
    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;
    }
}

UserDetailsService

道DaoAuthenticationProvider處理了web表單的認證邏輯,認證成功後既獲得一個Authentication(UsernamePasswordAuthenticationToken實現),裏面包含了身份信息(Principal)。
這個身份信息就是一個Object,大多數狀況下它能夠被強轉爲UserDetails對象。DaoAuthenticationProvider中包含了一個UserDetailsService實例,它負責根據用戶名提取用戶信息UserDetails(包含密碼)。post

然後DaoAuthenticationProvider會去對比UserDetailsService提取的用戶密碼與用戶提交的密碼是否匹配做爲認證成功的關鍵依據。ui

所以能夠經過將自定義的 UserDetailsService 公開爲spring bean來定義自定義身份驗證。

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

PasswordEncoder

DaoAuthenticationProvider認證處理器經過UserDetailsService獲取到UserDetails後,它是如何與請求Authentication中的密碼作對比呢?

在這裏Spring Security爲了適應多種多樣的加密類型,又作了抽象,DaoAuthenticationProvider經過PasswordEncoder接口的matches方法進行密碼的對比,而具體的密碼對比細節取決於實現:

public interface PasswordEncoder { 
    String encode(CharSequence var1);

    boolean matches(CharSequence var1, String var2);

    default boolean upgradeEncoding(String encodedPassword) { 
        return false;
    }
}

而Spring Security提供不少內置的PasswordEncoder,可以開箱即用,使用某種PasswordEncoder只須要進行以下聲明便可,以下:

@Bean 
public PasswordEncoder passwordEncoder() { 
    return  NoOpPasswordEncoder.getInstance();
}
相關文章
相關標籤/搜索