Spring Security源碼分析一:Spring Security認證過程

Spring Security是一個能夠為基于Spring的企業(yè)應用系統(tǒng)提供聲明式的安全訪問控制解決方案的安全框架掂骏。它提供了一組可以在Spring應用上下文中配置的Bean昼弟,充分利用了Spring IoC睦尽,DI(控制反轉Inversion of Control ,DI:Dependency Injection 依賴注入)和AOP(面向切面編程)功能袜蚕,為應用系統(tǒng)提供聲明式的安全訪問控制功能猾普,減少了為企業(yè)系統(tǒng)安全控制編寫大量重復代碼的工作虾啦。

類圖

為了方便理解Spring Security認證流程翠霍,特意畫了如下的類圖锭吨,包含相關的核心認證類

概述

核心驗證器

AuthenticationManager

該對象提供了認證方法的入口,接收一個Authentiaton對象作為參數;

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

ProviderManager

它是 AuthenticationManager 的一個實現(xiàn)類寒匙,提供了基本的認證邏輯和方法零如;它包含了一個 List<AuthenticationProvider> 對象,通過 AuthenticationProvider 接口來擴展出不同的認證提供者(當Spring Security默認提供的實現(xiàn)類不能滿足需求的時候可以擴展AuthenticationProvider 覆蓋supports(Class<?> authentication)方法)锄弱;

驗證邏輯

AuthenticationManager 接收 Authentication 對象作為參數考蕾,并通過 authenticate(Authentication) 方法對其進行驗證;AuthenticationProvider實現(xiàn)類用來支撐對 Authentication 對象的驗證動作会宪;UsernamePasswordAuthenticationToken實現(xiàn)了 Authentication主要是將用戶輸入的用戶名和密碼進行封裝肖卧,并供給 AuthenticationManager 進行驗證;驗證完成以后將返回一個認證成功的 Authentication 對象掸鹅;

Authentication

Authentication對象中的主要方法

public interface Authentication extends Principal, Serializable {
    //#1.權限結合塞帐,可使用AuthorityUtils.commaSeparatedStringToAuthorityList("admin,ROLE_ADMIN")返回字符串權限集合
    Collection<? extends GrantedAuthority> getAuthorities();
    //#2.用戶名密碼認證時可以理解為密碼
    Object getCredentials();
    //#3.認證時包含的一些信息。
    Object getDetails();
    //#4.用戶名密碼認證時可理解時用戶名
    Object getPrincipal();
    #5.是否被認證巍沙,認證為true    
    boolean isAuthenticated();
    #6.設置是否能被認證
    void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;

ProviderManager

ProviderManagerAuthenticationManager的實現(xiàn)類壁榕,提供了基本認證實現(xiàn)邏輯和流程;

public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {
        //#1.獲取當前的Authentication的認證類型
        Class<? extends Authentication> toTest = authentication.getClass();
        AuthenticationException lastException = null;
        Authentication result = null;
        boolean debug = logger.isDebugEnabled();
        //#2.遍歷所有的providers使用supports方法判斷該provider是否支持當前的認證類型赎瞎,不支持的話繼續(xù)遍歷
        for (AuthenticationProvider provider : getProviders()) {
            if (!provider.supports(toTest)) {
                continue;
            }

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

            try {
                #3.支持的話調用provider的authenticat方法認證
                result = provider.authenticate(authentication);

                if (result != null) {
                    #4.認證通過的話重新生成Authentication對應的Token
                    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 {
                #5.如果#1 沒有驗證通過,則使用父類型AuthenticationManager進行驗證
                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;
            }
        }
        #6. 是否擦出敏感信息
        if (result != null) {
            if (eraseCredentialsAfterAuthentication
                    && (result instanceof CredentialsContainer)) {
                // Authentication is complete. Remove credentials and other secret data
                // from authentication
                ((CredentialsContainer) result).eraseCredentials();
            }

            eventPublisher.publishAuthenticationSuccess(result);
            return result;
        }

        // Parent was null, or didn't authenticate (or throw an exception).

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

        prepareException(lastException, authentication);

        throw lastException;
    }
  1. 遍歷所有的 Providers颊咬,然后依次執(zhí)行該 Provider 的驗證方法
    • 如果某一個 Provider 驗證成功务甥,則跳出循環(huán)不再執(zhí)行后續(xù)的驗證;
    • 如果驗證成功喳篇,會將返回的 result 既 Authentication 對象進一步封裝為 Authentication Token敞临;
      比如 UsernamePasswordAuthenticationToken、RememberMeAuthenticationToken 等麸澜;這些 Authentication Token 也都繼承自 Authentication 對象挺尿;
  2. 如果 #1 沒有任何一個 Provider 驗證成功,則試圖使用其 parent Authentication Manager 進行驗證;
  3. 是否需要擦除密碼等敏感信息编矾;

AuthenticationProvider

ProviderManager 通過 AuthenticationProvider 擴展出更多的驗證提供的方式熟史;而 AuthenticationProvider 本身也就是一個接口,從類圖中我們可以看出它的實現(xiàn)類AbstractUserDetailsAuthenticationProviderAbstractUserDetailsAuthenticationProvider的子類DaoAuthenticationProvider窄俏。DaoAuthenticationProviderSpring Security中一個核心的Provider,對所有的數據庫提供了基本方法和入口蹂匹。

DaoAuthenticationProvider

DaoAuthenticationProvider主要做了以下事情

  1. 對用戶身份盡心加密操作;
    #1.可直接返回BCryptPasswordEncoder凹蜈,也可以自己實現(xiàn)該接口使用自己的加密算法核心方法String encode(CharSequence rawPassword);和boolean matches(CharSequence rawPassword, String encodedPassword);
    

private PasswordEncoder passwordEncoder;

2. 實現(xiàn)了 `AbstractUserDetailsAuthenticationProvider` 兩個抽象方法限寞,
    1. 獲取用戶信息的擴展點
    ```java
protected final UserDetails retrieveUser(String username,
            UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
        UserDetails loadedUser;

        try {
            loadedUser = this.getUserDetailsService().loadUserByUsername(username);
        }
主要是通過注入`UserDetailsService`接口對象,并調用其接口方法 `loadUserByUsername(String username)` 獲取得到相關的用戶信息仰坦。`UserDetailsService`接口非常重要履植。
2. 實現(xiàn) additionalAuthenticationChecks 的驗證方法(主要驗證密碼);

AbstractUserDetailsAuthenticationProvider

AbstractUserDetailsAuthenticationProviderDaoAuthenticationProvider提供了基本的認證方法悄晃;

public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {
        Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
                messages.getMessage(
                        "AbstractUserDetailsAuthenticationProvider.onlySupports",
                        "Only UsernamePasswordAuthenticationToken is supported"));

        // Determine username
        String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
                : authentication.getName();

        boolean cacheWasUsed = true;
        UserDetails user = this.userCache.getUserFromCache(username);

        if (user == null) {
            cacheWasUsed = false;

            try {
                #1.獲取用戶信息由子類實現(xiàn)即DaoAuthenticationProvider
                user = retrieveUser(username,
                        (UsernamePasswordAuthenticationToken) authentication);
            }
            catch (UsernameNotFoundException notFound) {
                logger.debug("User '" + username + "' not found");

                if (hideUserNotFoundExceptions) {
                    throw new BadCredentialsException(messages.getMessage(
                            "AbstractUserDetailsAuthenticationProvider.badCredentials",
                            "Bad credentials"));
                }
                else {
                    throw notFound;
                }
            }

            Assert.notNull(user,
                    "retrieveUser returned null - a violation of the interface contract");
        }

        try {
            #2.前檢查由DefaultPreAuthenticationChecks類實現(xiàn)(主要判斷當前用戶是否鎖定玫霎,過期,凍結User接口)
            preAuthenticationChecks.check(user);
            #3.子類實現(xiàn)
            additionalAuthenticationChecks(user,
                    (UsernamePasswordAuthenticationToken) authentication);
        }
        catch (AuthenticationException exception) {
            if (cacheWasUsed) {
                // There was a problem, so try again after checking
                // we're using latest data (i.e. not from the cache)
                cacheWasUsed = false;
                user = retrieveUser(username,
                        (UsernamePasswordAuthenticationToken) authentication);
                preAuthenticationChecks.check(user);
                additionalAuthenticationChecks(user,
                        (UsernamePasswordAuthenticationToken) authentication);
            }
            else {
                throw exception;
            }
        }
        #4.檢測用戶密碼是否過期對應#2 的User接口
        postAuthenticationChecks.check(user);

        if (!cacheWasUsed) {
            this.userCache.putUserInCache(user);
        }

        Object principalToReturn = user;

        if (forcePrincipalAsString) {
            principalToReturn = user.getUsername();
        }

        return createSuccessAuthentication(principalToReturn, authentication, user);
    }

AbstractUserDetailsAuthenticationProvider主要實現(xiàn)了AuthenticationProvider的接口方法authenticate 并提供了相關的驗證邏輯传泊;

  1. 獲取用戶返回UserDetails
    AbstractUserDetailsAuthenticationProvider定義了一個抽象的方法

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

2. 三步驗證工作
    1. preAuthenticationChecks
    2. additionalAuthenticationChecks(抽象方法鼠渺,子類實現(xiàn))
    3. postAuthenticationChecks
3. 將已通過驗證的用戶信息封裝成 UsernamePasswordAuthenticationToken 對象并返回;該對象封裝了用戶的身份信息眷细,以及相應的權限信息拦盹,相關源碼如下,
    ```java
protected Authentication createSuccessAuthentication(Object principal,
        UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
                principal, authentication.getCredentials(),
                authoritiesMapper.mapAuthorities(user.getAuthorities()));
        result.setDetails(authentication.getDetails());

        return result;
    }

UserDetailsService

UserDetailsService是一個接口溪椎,提供了一個方法

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

通過用戶名 username 調用方法 loadUserByUsername 返回了一個UserDetails接口對象(對應AbstractUserDetailsAuthenticationProvider的三步驗證方法)普舆;

public interface UserDetails extends Serializable {
    #1.權限集合
    Collection<? extends GrantedAuthority> getAuthorities();
    #2.密碼   
    String getPassword();
    #3.用戶民
    String getUsername();
    #4.用戶是否過期
    boolean isAccountNonExpired();
    #5.是否鎖定 
    boolean isAccountNonLocked();
    #6.用戶密碼是否過期 
    boolean isCredentialsNonExpired();
    #7.賬號是否可用(可理解為是否刪除)
    boolean isEnabled();
}

Spring 為UserDetailsService默認提供了一個實現(xiàn)類 org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl

JdbcUserDetailsManager

該實現(xiàn)類主要是提供基于JDBC對 User 進行增、刪校读、查沼侣、改的方法

public class JdbcUserDetailsManager extends JdbcDaoImpl implements UserDetailsManager,
        GroupManager {
    // ~ Static fields/initializers
    // =====================================================================================

    // UserDetailsManager SQL
    #1.定義了一些列對數據庫操作的語句
    public static final String DEF_CREATE_USER_SQL = "insert into users (username, password, enabled) values (?,?,?)";
    public static final String DEF_DELETE_USER_SQL = "delete from users where username = ?";
    public static final String DEF_UPDATE_USER_SQL = "update users set password = ?, enabled = ? where username = ?";
    public static final String DEF_INSERT_AUTHORITY_SQL = "insert into authorities (username, authority) values (?,?)";
    public static final String DEF_DELETE_USER_AUTHORITIES_SQL = "delete from authorities where username = ?";
    public static final String DEF_USER_EXISTS_SQL = "select username from users where username = ?";
    public static final String DEF_CHANGE_PASSWORD_SQL = "update users set password = ? where username = ?";



InMemoryUserDetailsManager

該實現(xiàn)類主要是提供基于內存對 User 進行增、刪歉秫、查蛾洛、改的方法
`public class InMemoryUserDetailsManager implements UserDetailsManager {
protected final Log logger = LogFactory.getLog(getClass());
#1.用MAP 存儲
private final Map<String, MutableUserDetails> users = new HashMap<String, MutableUserDetails>();

private AuthenticationManager authenticationManager;

public InMemoryUserDetailsManager() {
}

public InMemoryUserDetailsManager(Collection<UserDetails> users) {
    for (UserDetails user : users) {
        createUser(user);
    }
}`

總結

UserDetailsService接口作為橋梁,是DaoAuthenticationProvier與特定用戶信息來源進行解耦的地方雁芙,UserDetailsServiceUserDetailsUserDetailsManager所構成轧膘;UserDetailsUserDetailsManager各司其責,一個是對基本用戶信息進行封裝兔甘,一個是對基本用戶信息進行管理谎碍;

特別注意UserDetailsService洞焙、UserDetails以及UserDetailsManager都是可被用戶自定義的擴展點蟆淀,我們可以繼承這些接口提供自己的讀取用戶來源和管理用戶的方法拯啦,比如我們可以自己實現(xiàn)一個 與特定 ORM 框架,比如 Mybatis 或者 Hibernate熔任,相關的UserDetailsServiceUserDetailsManager褒链;

時序圖

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市笋敞,隨后出現(xiàn)的幾起案子碱蒙,更是在濱河造成了極大的恐慌,老刑警劉巖夯巷,帶你破解...
    沈念sama閱讀 222,627評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件赛惩,死亡現(xiàn)場離奇詭異,居然都是意外死亡趁餐,警方通過查閱死者的電腦和手機喷兼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,180評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來后雷,“玉大人季惯,你說我怎么就攤上這事⊥瓮唬” “怎么了勉抓?”我有些...
    開封第一講書人閱讀 169,346評論 0 362
  • 文/不壞的土叔 我叫張陵,是天一觀的道長候学。 經常有香客問我藕筋,道長,這世上最難降的妖魔是什么梳码? 我笑而不...
    開封第一講書人閱讀 60,097評論 1 300
  • 正文 為了忘掉前任隐圾,我火速辦了婚禮,結果婚禮上掰茶,老公的妹妹穿的比我還像新娘暇藏。我一直安慰自己,他們只是感情好濒蒋,可當我...
    茶點故事閱讀 69,100評論 6 398
  • 文/花漫 我一把揭開白布盐碱。 她就那樣靜靜地躺著,像睡著了一般沪伙。 火紅的嫁衣襯著肌膚如雪瓮顽。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,696評論 1 312
  • 那天焰坪,我揣著相機與錄音,去河邊找鬼聘惦。 笑死某饰,一個胖子當著我的面吹牛儒恋,可吹牛的內容都是我干的。 我是一名探鬼主播黔漂,決...
    沈念sama閱讀 41,165評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼诫尽,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了炬守?” 一聲冷哼從身側響起牧嫉,我...
    開封第一講書人閱讀 40,108評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎减途,沒想到半個月后酣藻,有當地人在樹林里發(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 46,646評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡鳍置,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,709評論 3 342
  • 正文 我和宋清朗相戀三年辽剧,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片税产。...
    茶點故事閱讀 40,861評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡怕轿,死狀恐怖,靈堂內的尸體忽然破棺而出辟拷,到底是詐尸還是另有隱情撞羽,我是刑警寧澤,帶...
    沈念sama閱讀 36,527評論 5 351
  • 正文 年R本政府宣布衫冻,位于F島的核電站诀紊,受9級特大地震影響,放射性物質發(fā)生泄漏羽杰。R本人自食惡果不足惜渡紫,卻給世界環(huán)境...
    茶點故事閱讀 42,196評論 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望考赛。 院中可真熱鬧惕澎,春花似錦、人聲如沸颜骤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,698評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽忍抽。三九已至八孝,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間鸠项,已是汗流浹背干跛。 一陣腳步聲響...
    開封第一講書人閱讀 33,804評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留祟绊,地道東北人楼入。 一個月前我還...
    沈念sama閱讀 49,287評論 3 379
  • 正文 我出身青樓哥捕,卻偏偏與公主長得像,于是被迫代替她去往敵國和親嘉熊。 傳聞我的和親對象是個殘疾皇子遥赚,可洞房花燭夜當晚...
    茶點故事閱讀 45,860評論 2 361

推薦閱讀更多精彩內容