spring security系列一:架構概述

一直以來都想好好寫一寫spring security 系列文章,往往提筆又不知何處下筆,又賴於spring security體系強大又過於繁雜,且spring security 與auth2.0結合的時候又很難理解,最近項目告一段落,閒來無事好好對spring security 進行總結和回顧。java

核心組件

主要介紹一些在Spring Security中常見且核心的Java類,這些類之間的關係構建起了整個spring security框架,所以首先來熟知這些類是很是有必要的。web

SecurityContextHolder

SecurityContextHolder它持有的是安全上下文(security context)的信息。當前操做的用戶是誰,該用戶是否已經被認證,他擁有哪些角色權等等,這些都被保存在SecurityContextHolder中。SecurityContextHolder默認使用ThreadLocal 策略來存儲認證信息。看到ThreadLocal 也就意味着,這是一種與線程綁定的策略。在web環境下,Spring Security在用戶登陸時自動綁定認證信息到當前線程,在用戶退出時,自動清除當前線程的認證信息。

獲取當前用戶的信息redis

Object principal =SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    if (principal instanceof UserDetails) {
        String username = ((UserDetails)principal).getUsername();
    } else {
        String username = principal.toString();
    }

getAuthentication()返回了認證信息,getPrincipal()返回了身份信息,UserDetails即是Spring對身份信息封裝的一個接口。spring

SecurityContext

安全上下文,主要持有Authentication對象,若是用戶未鑑權,那Authentication對象將會是空的。
public interface SecurityContext extends Serializable {
    // ~ Methods
    // ========================================================================================================

    /**
     * Obtains the currently authenticated principal, or an authentication request token.
     *
     * @return the <code>Authentication</code> or <code>null</code> if no authentication
     * information is available
     */
    Authentication getAuthentication();

    /**
     * Changes the currently authenticated principal, or removes the authentication
     * information.
     *
     * @param authentication the new <code>Authentication</code> token, or
     * <code>null</code> if no further authentication information should be stored
     */
    void setAuthentication(Authentication authentication);
}

Authentication

鑑權對象,該對象主要包含了用戶的詳細信息(UserDetails)和用戶鑑權時所須要的信息,如用戶提交的用戶名密碼、Remember-me Token,或者digest hash值等,按不一樣鑑權方式使用不一樣的Authentication實現。
public interface Authentication extends Principal, Serializable {
    // ~ Methods
    // ========================================================================================================

    /**
     * Set by an <code>AuthenticationManager</code> to indicate the authorities that the
     * principal has been granted. Note that classes should not rely on this value as
     * being valid unless it has been set by a trusted <code>AuthenticationManager</code>.
     * <p>
     * Implementations should ensure that modifications to the returned collection array
     * do not affect the state of the Authentication object, or use an unmodifiable
     * instance.
     * </p>
     *
     * @return the authorities granted to the principal, or an empty collection if the
     * token has not been authenticated. Never null.
     */
    Collection<? extends GrantedAuthority> getAuthorities();

    /**
     * The credentials that prove the principal is correct. This is usually a password,
     * but could be anything relevant to the <code>AuthenticationManager</code>. Callers
     * are expected to populate the credentials.
     *
     * @return the credentials that prove the identity of the <code>Principal</code>
     */
    Object getCredentials();

    /**
     * Stores additional details about the authentication request. These might be an IP
     * address, certificate serial number etc.
     *
     * @return additional details about the authentication request, or <code>null</code>
     * if not used
     */
    Object getDetails();

    /**
     * The identity of the principal being authenticated. In the case of an authentication
     * request with username and password, this would be the username. Callers are
     * expected to populate the principal for an authentication request.
     * <p>
     * The <tt>AuthenticationManager</tt> implementation will often return an
     * <tt>Authentication</tt> containing richer information as the principal for use by
     * the application. Many of the authentication providers will create a
     * {@code UserDetails} object as the principal.
     *
     * @return the <code>Principal</code> being authenticated or the authenticated
     * principal after authentication.
     */
    Object getPrincipal();

    /**
     * Used to indicate to {@code AbstractSecurityInterceptor} whether it should present
     * the authentication token to the <code>AuthenticationManager</code>. Typically an
     * <code>AuthenticationManager</code> (or, more often, one of its
     * <code>AuthenticationProvider</code>s) will return an immutable authentication token
     * after successful authentication, in which case that token can safely return
     * <code>true</code> to this method. Returning <code>true</code> will improve
     * performance, as calling the <code>AuthenticationManager</code> for every request
     * will no longer be necessary.
     * <p>
     * For security reasons, implementations of this interface should be very careful
     * about returning <code>true</code> from this method unless they are either
     * immutable, or have some way of ensuring the properties have not been changed since
     * original creation.
     *
     * @return true if the token has been authenticated and the
     * <code>AbstractSecurityInterceptor</code> does not need to present the token to the
     * <code>AuthenticationManager</code> again for re-authentication.
     */
    boolean isAuthenticated();

    /**
     * See {@link #isAuthenticated()} for a full description.
     * <p>
     * Implementations should <b>always</b> allow this method to be called with a
     * <code>false</code> parameter, as this is used by various classes to specify the
     * authentication token should not be trusted. If an implementation wishes to reject
     * an invocation with a <code>true</code> parameter (which would indicate the
     * authentication token is trusted - a potential security risk) the implementation
     * should throw an {@link IllegalArgumentException}.
     *
     * @param isAuthenticated <code>true</code> if the token should be trusted (which may
     * result in an exception) or <code>false</code> if the token should not be trusted
     *
     * @throws IllegalArgumentException if an attempt to make the authentication token
     * trusted (by passing <code>true</code> as the argument) is rejected due to the
     * implementation being immutable or implementing its own alternative approach to
     * {@link #isAuthenticated()}
     */
    void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

一、 Authentication是spring security包中的接口,直接繼承自Principal類,而Principal是位於java.security包中的。能夠見得,Authentication在spring security中是最高級別的身份/認證的抽象。
二、 由這個頂級接口,咱們能夠獲得用戶擁有的權限信息列表,密碼,用戶細節信息,用戶身份信息,認證信息。
三、getAuthorities(),權限信息列表,默認是GrantedAuthority接口的一些實現類,一般是表明權限信息的一系列字符串。
四、getCredentials(),密碼信息,用戶輸入的密碼字符串,在認證事後一般會被移除,用於保障安全。
五、getDetails(),細節信息,web應用中的實現接口一般爲 WebAuthenticationDetails,它記錄了訪問者的ip地址和sessionId的值。
六、getPrincipal(),敲黑板!!!最重要的身份信息,大部分狀況下返回的是UserDetails接口的實現類,也是框架中的經常使用接口之一。數據庫

GrantedAuthority

該接口表示了當前用戶所擁有的權限(或者角色)信息。這些信息由受權負責對象AccessDecisionManager來使用,並決定最終用戶是否能夠訪問某資源(URL或方法調用或域對象)。鑑權時並不會使用到該對象。

UserDetails

這個接口規範了用戶詳細信息所擁有的字段,譬如用戶名、密碼、帳號是否過時、是否鎖定等。在Spring Security中,獲取當前登陸的用戶的信息,通常狀況是須要在這個接口上面進行擴展,用來對接本身系統的用戶
public interface UserDetails extends Serializable {
    // ~ Methods
    // ========================================================================================================

    /**
     * Returns the authorities granted to the user. Cannot return <code>null</code>.
     *
     * @return the authorities, sorted by natural key (never <code>null</code>)
     */
    Collection<? extends GrantedAuthority> getAuthorities();

    /**
     * Returns the password used to authenticate the user.
     *
     * @return the password
     */
    String getPassword();

    /**
     * Returns the username used to authenticate the user. Cannot return <code>null</code>
     * .
     *
     * @return the username (never <code>null</code>)
     */
    String getUsername();

    /**
     * Indicates whether the user's account has expired. An expired account cannot be
     * authenticated.
     *
     * @return <code>true</code> if the user's account is valid (ie non-expired),
     * <code>false</code> if no longer valid (ie expired)
     */
    boolean isAccountNonExpired();

    /**
     * Indicates whether the user is locked or unlocked. A locked user cannot be
     * authenticated.
     *
     * @return <code>true</code> if the user is not locked, <code>false</code> otherwise
     */
    boolean isAccountNonLocked();

    /**
     * Indicates whether the user's credentials (password) has expired. Expired
     * credentials prevent authentication.
     *
     * @return <code>true</code> if the user's credentials are valid (ie non-expired),
     * <code>false</code> if no longer valid (ie expired)
     */
    boolean isCredentialsNonExpired();

    /**
     * Indicates whether the user is enabled or disabled. A disabled user cannot be
     * authenticated.
     *
     * @return <code>true</code> if the user is enabled, <code>false</code> otherwise
     */
    boolean isEnabled();
}

UserDetailsService

這個接口只提供一個接口loadUserByUsername(String username),這個接口很是重要,通常狀況咱們都是經過擴展這個接口來顯示獲取咱們的用戶信息,用戶登錄時傳遞的用戶名和密碼也是經過這裏這查找出來的用戶名和密碼進行校驗,可是真正的校驗不在這裏,而是由AuthenticationManager以及AuthenticationProvider負責的,須要強調的是,若是用戶不存在,不該返回NULL,而要拋出異常UsernameNotFoundException
public interface UserDetailsService {
    // ~ Methods
    // ========================================================================================================

    /**
     * Locates the user based on the username. In the actual implementation, the search
     * may possibly be case sensitive, or case insensitive depending on how the
     * implementation instance is configured. In this case, the <code>UserDetails</code>
     * object that comes back may have a username that is of a different case than what
     * was actually requested..
     *
     * @param username the username identifying the user whose data is required.
     *
     * @return a fully populated user record (never <code>null</code>)
     *
     * @throws UsernameNotFoundException if the user could not be found or the user has no
     * GrantedAuthority
     */
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

Spring Security是如何完成身份認證的?

一、用戶名和密碼被過濾器獲取到,封裝成Authentication,一般狀況下是UsernamePasswordAuthenticationToken這個實現類。緩存

二、AuthenticationManager 身份管理器負責驗證這個Authentication安全

三、認證成功後,AuthenticationManager身份管理器返回一個被填充滿了信息的(包括上面提到的權限信息,身份信息,細節信息,但密碼一般會被移除)Authentication實例。session

四、SecurityContextHolder安全上下文容器將第3步填充了信息的Authentication,經過SecurityContextHolder.getContext().setAuthentication()方法,設置到其中。app

AuthenticationManager

初次接觸Spring Security的朋友相信會被AuthenticationManager,ProviderManager ,AuthenticationProvider …這麼多類似的Spring認證類搞得暈頭轉向,但只要稍微梳理一下就能夠理解清楚它們的聯繫和設計者的用意。AuthenticationManager(接口)是認證相關的核心接口,也是發起認證的出發點,由於在實際需求中,咱們可能會容許用戶使用用戶名+密碼登陸,同時容許用戶使用郵箱+密碼,手機號碼+密碼登陸,甚至,可能容許用戶使用指紋登陸(還有這樣的操做?沒想到吧),因此說AuthenticationManager通常不直接認證,AuthenticationManager接口的經常使用實現類ProviderManager 內部會維護一個List<AuthenticationProvider>列表,存放多種認證方式,實際上這是委託者模式的應用(Delegate)。也就是說,核心的認證入口始終只有一個:AuthenticationManager,不一樣的認證方式:用戶名+密碼(UsernamePasswordAuthenticationToken),郵箱+密碼,手機號碼+密碼登陸則對應了三個AuthenticationProvider。這樣一來就好理解多了?。

其中AuthenticationProvider下驗證登錄的關鍵源代碼以下:框架

public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {
        Class<? extends Authentication> toTest = authentication.getClass();
        AuthenticationException lastException = null;
        Authentication result = null;
        boolean debug = logger.isDebugEnabled();

        //依次進行驗證
        for (AuthenticationProvider provider : getProviders()) {
            if (!provider.supports(toTest)) {
                continue;
            }

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

            try {
                result = provider.authenticate(authentication);

                // 若是有Authentication信息,則直接返回
                if (result != null) {
                    copyDetails(authentication, result);
                    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 = 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 = e;
            }
        }


        if (result != null) {
            if (eraseCredentialsAfterAuthentication
                    && (result instanceof CredentialsContainer)) {
                //移除密碼
                ((CredentialsContainer) result).eraseCredentials();
            }

            //發佈登陸成功事件
            eventPublisher.publishAuthenticationSuccess(result);
            return result;
        }

         //執行到此,說明沒有認證成功,包裝異常信息

        if (lastException == null) {
            lastException = new ProviderNotFoundException(messages.getMessage(
                    "ProviderManager.providerNotFound",
                    new Object[] { toTest.getName() },
                    "No AuthenticationProvider found for {0}"));
        }

        prepareException(lastException, authentication);

        throw lastException;
    }

強調一下UserDetails和UserDetailsService

UserDetails

上面不斷提到了UserDetails這個接口,它表明了最詳細的用戶信息,這個接口涵蓋了一些必要的用戶信息字段,咱們通常都須要對它進行必要的擴展。

它和Authentication接口很相似,好比它們都擁有username,authorities,區分他們也是本文的重點內容之一。Authentication的getCredentials()與UserDetails中的getPassword()須要被區分對待,前者是用戶提交的密碼憑證,後者是用戶正確的密碼,認證器其實就是對這二者的比對。Authentication中的getAuthorities()實際是由UserDetails的getAuthorities()傳遞而造成的。還記得Authentication接口中的getUserDetails()方法嗎?其中的UserDetails用戶詳細信息即是通過了AuthenticationProvider以後被填充的。

UserDetailsService

UserDetailsService和AuthenticationProvider二者的職責經常被人們搞混,UserDetailsService只負責從特定的地方加載用戶信息,能夠是數據庫、redis緩存、接口等

核心類圖

最後附上核心類圖一張

clipboard.png

相關文章
相關標籤/搜索