1. 程式人生 > >Spring Security 原始碼淺析

Spring Security 原始碼淺析

1.核心元件

1.1.SecurityContextHolder

  SecurityContextHolder用於儲存安全上下文(security context)的資訊。當前操作的使用者是誰,該使用者是否已經被認證,他擁有哪些角色許可權…這些都被儲存在SecurityContextHolder中。SecurityContextHolder預設使用ThreadLocal 策略來儲存認證資訊。看到ThreadLocal 也就意味著,這是一種與執行緒繫結的策略。Spring Security在使用者登入時自動繫結認證資訊到當前執行緒,在使用者退出時,自動清除當前執行緒的認證資訊。

1.2.Authentication

public interface Authentication extends Principal, Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();
    bject getCredentials();
    Object getDetails();
    Object getPrincipal();
    boolean 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介面的實現類,也是框架中的常用介面之一。UserDetails介面將會在下面的小節重點介紹。

1.3.AuthenticationManager

  AuthenticationManager(介面)是認證相關的核心介面,也是發起認證的出發點,因為在實際需求中,我們可能會允許使用者使用使用者名稱+密碼登入,同時允許使用者使用郵箱+密碼,手機號碼+密碼登入,甚至,可能允許使用者使用指紋登入(還有這樣的操作?沒想到吧),所以說AuthenticationManager一般不直接認證,AuthenticationManager介面的常用實現類ProviderManager 內部會維護一個List列表,存放多種認證方式,實際上這是委託者模式的應用(Delegate)。

public class ProviderManager implements AuthenticationManager, MessageSourceAware,
        InitializingBean {
    // 維護一個AuthenticationProvider列表
    private List<AuthenticationProvider> providers = Collections.emptyList();

    public Authentication authenticate(Authentication authentication)
          throws AuthenticationException {
       Class<? extends Authentication> toTest = authentication.getClass();
       AuthenticationException lastException = null;
       Authentication result = null;
       // 依次認證
       for (AuthenticationProvider provider : getProviders()) {
          if (!provider.supports(toTest)) {
             continue;
          }
          try {
             result = provider.authenticate(authentication);
             if (result != null) {
                copyDetails(authentication, result);
                break;
             }
          }
          ...
          catch (AuthenticationException e) {
             lastException = e;
          }
       }
       // 如果有Authentication資訊,則直接返回
       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;
    }
}

1.4.AuthenticationProvider

  AuthenticationProvider 就是實際的認證介面。比如DaoAuthenticationProvider,使用者提交的使用者名稱和密碼,被封裝成了UsernamePasswordAuthenticationToken,而根據使用者名稱載入使用者的任務則是交給了UserDetailsService ,DaoAuthenticationProvider中,對應的方法便是retrieveUser,雖然有兩個引數,但是retrieveUser只有第一個引數起主要作用,返回一個UserDetails。還需要完成UsernamePasswordAuthenticationToken和UserDetails密碼的比對,這便是交給additionalAuthenticationChecks方法完成的,如果這個void方法沒有拋異常,則認為比對成功。比對密碼的過程,用到了PasswordEncoder和SaltSource。

1.5.UserDetails

public interface UserDetails extends Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();
    String getPassword();
    String getUsername();
    boolean isAccountNonExpired();
    boolean isAccountNonLocked();
    boolean isCredentialsNonExpired();
    boolean isEnabled();
}

  它和Authentication介面很類似,比如它們都擁有username,authorities,區分他們也是本文的重點內容之一。Authentication的getCredentials()與UserDetails中的getPassword()需要被區分對待,前者是使用者提交的密碼憑證,後者是使用者正確的密碼,認證器其實就是對這兩者的比對。Authentication中的getAuthorities()實際是由UserDetails的getAuthorities()傳遞而形成的。還記得Authentication介面中的getUserDetails()方法嗎?其中的UserDetails使用者詳細資訊便是經過了AuthenticationProvider之後被填充的。

1.6.UserDetailsService

  UserDetailsService只負責從特定的地方(通常是資料庫)載入使用者資訊,僅此而已

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

1.7.AccessDecisionManager

  AccessDecisionManager會呼叫AccessDecisionVoter進行投票,並根據結果進行決策。

  • AffirmativeBased 一票通過,只要有一個投票器通過就允許訪問
  • ConsensusBased 有一半以上投票器通過才允許訪問資源
  • UnanimousBased 所有投票器都通過才允許訪問

1.8.AccessDecisionVoter

public interface AccessDecisionVoter<S> {
    boolean supports(ConfigAttribute attribute);
    boolean supports(Class<?> clazz);
    int vote(Authentication authentication, S object,
            Collection<ConfigAttribute> attributes);
}

2.請求過程

主要的過濾器有:

  • LogoutFilter
  • BasicAuthenticationFilter Basic認證
  • UsernamePasswordAuthenticationFilter 使用者認證,會呼叫AuthenticationManager進行認證
  • SmsCodeAuthenticationFilter 簡訊認證
  • OAuth2AuthenticationProcessingFilter OAuth2認證
  • AnonymousAuthenticationFilter
  • SessionManagementFilter
  • FilterSecurityInterceptor 會呼叫AccessDecisionManager進行授權,授權通過會呼叫SecurityContextHolder.setContext(token.getSecurityContext())

3.boot方式的初始化過程

3.1時序圖

3.2類圖

這裡寫圖片描述
AbstractConfiguredSecurityBuilder的doBuild方法:

protected final O doBuild() throws Exception {
        synchronized (configurers) {
            buildState = BuildState.INITIALIZING;

            beforeInit();
            init();

            buildState = BuildState.CONFIGURING;

            beforeConfigure();
            configure();

            buildState = BuildState.BUILDING;

            O result = performBuild();

            buildState = BuildState.BUILT;

            return result;
        }
    }