Spring Security 架構簡介

1、技術概述

1.1 Spring vs Spring Boot vs Spring Security

1.1.1 Spring Framework

Spring Framework 爲開發 Java 應用程序提供了全面的基礎架構支持。它包含了一些不錯的功能,如 "依賴注入",以及一些現成的模塊:html

  • Spring JDBC
  • Spring MVC
  • Spring Security
  • Spring AOP
  • Spring ORM

這些模塊能夠大大減小應用程序的開發時間。例如,在 Java Web 開發的早期,咱們須要編寫大量樣板代碼以將記錄插入數據源。可是,經過使用 Spring JDBC 模塊的 JDBCTemplate,咱們能夠僅經過少許配置將其簡化爲幾行代碼。java

閱讀更多關於 Angular、TypeScript、Node.js/Java 、Spring 等技術文章,歡迎訪問個人我的博客 —— 全棧修仙之路
1.1.2 Spring Boot

Spring Boot 是基於 Spring Framework,它爲你的 Spring 應用程序提供了自動裝配特性,它的設計目標是讓你儘量快的上手應用程序的開發。如下是 Spring Boot 所擁有的一些特性:spring

  • 能夠建立獨立的 Spring 應用程序,而且基於 Maven 或 Gradle 插件,能夠建立可執行的 JARs 和 WARs;
  • 內嵌 Tomcat 或 Jetty 等 Servlet 容器;
  • 提供自動配置的 "starter" 項目對象模型(POMS)以簡化 Maven 配置;
  • 儘量自動配置 Spring 容器;
  • 提供一些常見的功能、如監控、WEB容器,健康,安全等功能;
  • 絕對沒有代碼生成,也不須要 XML 配置。
1.1.3 Spring Security

Spring Security 是一個可以爲基於 Spring 的企業應用系統提供聲明式的安全訪問控制解決方案的安全框架。它提供了一組能夠在 Spring 應用上下文中配置的 Bean,充分利用了 Spring IoC(Inversion of Control 控制反轉),DI(Dependency Injection 依賴注入)和 AOP(面向切面編程)功能,爲應用系統提供聲明式的安全訪問控制功能,減小了爲企業系統安全控制編寫大量重複代碼的工做。數據庫

Spring Security 擁有如下特性:編程

  • 對身份驗證和受權的全面且可擴展的支持
  • 防護會話固定、點擊劫持,跨站請求僞造等攻擊
  • 支持 Servlet API 集成
  • 支持與 Spring Web MVC 集成

Spring、Spring Boot 和 Spring Security 三者的關係以下圖所示:安全

spring-boot-security.jpg

1.2 Spring Security 集成

目前 Spring Security 5 支持與如下技術進行集成:架構

  • HTTP basic access authentication
  • LDAP system
  • OpenID identity providers
  • JAAS API
  • CAS Server
  • ESB Platform
  • ......
  • Your own authentication system

在進入 Spring Security 正題以前,咱們先來了解一下它的總體架構:框架

spring-security-arch.jpg

2、核心組件

2.1 SecurityContextHolder,SecurityContext 和 Authentication

最基本的對象是 SecurityContextHolder,它是咱們存儲當前應用程序安全上下文的詳細信息,其中包括當前使用應用程序的主體的詳細信息。如當前操做的用戶是誰,該用戶是否已經被認證,他擁有哪些角色權限等。ide

默認狀況下,SecurityContextHolder 使用 ThreadLocal 來存儲這些詳細信息,這意味着 Security Context 始終可用於同一執行線程中的方法,即便 Security Context 未做爲這些方法的參數顯式傳遞。spring-boot

獲取當前用戶的信息

由於身份信息與當前執行線程已綁定,因此可使用如下代碼塊在應用程序中獲取當前已驗證用戶的用戶名:

Object principal = SecurityContextHolder.getContext()
  .getAuthentication().getPrincipal();

if (principal instanceof UserDetails) {
  String username = ((UserDetails)principal).getUsername();
} else {
  String username = principal.toString();
}

調用 getContext() 返回的對象是 SecurityContext 接口的一個實例,對應 SecurityContext 接口定義以下:

// org/springframework/security/core/context/SecurityContext.java
public interface SecurityContext extends Serializable {
    Authentication getAuthentication();
    void setAuthentication(Authentication authentication);
}
Authentication

在 SecurityContext 接口中定義了 getAuthentication 和 setAuthentication 兩個抽象方法,當調用 getAuthentication 方法後會返回一個 Authentication 類型的對象,這裏的 Authentication 也是一個接口,它的定義以下:

// org/springframework/security/core/Authentication.java
public interface Authentication extends Principal, Serializable {
  // 權限信息列表,默認是GrantedAuthority接口的一些實現類,一般是表明權限信息的一系列字符串。
    Collection<? extends GrantedAuthority> getAuthorities();
  // 密碼信息,用戶輸入的密碼字符串,在認證事後一般會被移除,用於保障安全。
    Object getCredentials();
    Object getDetails();
  // 最重要的身份信息,大部分狀況下返回的是UserDetails接口的實現類,也是框架中的經常使用接口之一。
    Object getPrincipal();
    boolean isAuthenticated();
    void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

以上的 Authentication 接口是 spring-security-core jar 包中的接口,直接繼承自 Principal 類,而 Principal 是位於 java.security 包中,由此可知 Authentication 是 spring security 中核心的接口。經過這個 Authentication 接口的實現類,咱們能夠獲得用戶擁有的權限信息列表,密碼,用戶細節信息,用戶身份信息,認證信息等。

2.2 小結

下面咱們來簡單總結一下 SecurityContextHolder,SecurityContext 和 Authentication 這個三個對象之間的關係,SecurityContextHolder 用來保存 SecurityContext (安全上下文對象),經過調用 SecurityContext 對象中的方法,如 getAuthentication 方法,咱們能夠方便地獲取 Authentication 對象,利用該對象咱們能夠進一步獲取已認證用戶的詳細信息。

SecurityContextHolder,SecurityContext 和 Authentication 的詳細定義以下所示:

security-context-holder.jpg

3、身份驗證

3.1 Spring Security 中的身份驗證是什麼?

讓咱們考慮一個每一個人都熟悉的標準身份驗證方案:

  • 系統會提示用戶使用用戶名和密碼登陸。
  • 系統驗證用戶名和密碼是否正確。
  • 若驗證經過則獲取該用戶的上下文信息(如權限列表)。
  • 爲用戶創建安全上下文。
  • 用戶繼續進行,可能執行某些操做,該操做可能受訪問控制機制的保護,該訪問控制機制根據當前安全上下文信息檢查操做所需的權限。

前三項構成了身份驗證進程,所以咱們將在 Spring Security 中查看這些內容。

  • 獲取用戶名和密碼並將其組合到 UsernamePasswordAuthenticationToken 的實例中(咱們以前看到的Authentication 接口的實例)。
  • 令牌將傳遞給 AuthenticationManager 的實例以進行驗證。
  • AuthenticationManager 在成功驗證時返回徹底填充的 Authentication 實例。
  • SecurityContext 對象是經過調用 SecurityContextHolder.getContext().setAuthentication(…) 建立的,傳入返回的身份驗證 Authentication 對象。

3.2 Spring Security 身份驗證流程示例

瞭解完上述的身份驗證流程,咱們來看一個簡單的示例:

AuthenticationManager 接口:

public interface AuthenticationManager {
  // 對傳入的authentication對象進行認證
    Authentication authenticate(Authentication authentication)
            throws AuthenticationException;
}

SampleAuthenticationManager 類:

class SampleAuthenticationManager implements AuthenticationManager {
  static final List<GrantedAuthority> AUTHORITIES = new ArrayList<GrantedAuthority>();

  static {
    AUTHORITIES.add(new SimpleGrantedAuthority("ROLE_USER"));
  }

  public Authentication authenticate(Authentication auth) throws AuthenticationException {
    // 判斷用戶名和密碼是否相等,僅當相等時才認證經過
    if (auth.getName().equals(auth.getCredentials())) {
      return new UsernamePasswordAuthenticationToken(auth.getName(),
          auth.getCredentials(), AUTHORITIES);
    }
    throw new BadCredentialsException("Bad Credentials");
   }
}

AuthenticationExample 類:

public class AuthenticationExample {
  private static AuthenticationManager am = new SampleAuthenticationManager();

  public static void main(String[] args) throws Exception {
    BufferedReader in = new BufferedReader(new InputStreamReader(System.in));

    while(true) {
      System.out.println("Please enter your username:");
      String name = in.readLine();
      System.out.println("Please enter your password:");
      String password = in.readLine();
      try {
        // 使用用戶輸入的name和password建立request對象,這裏的UsernamePasswordAuthenticationToken
        // 是前面提到的Authentication接口的實現類
        Authentication request = new UsernamePasswordAuthenticationToken(name, password);
        // 使用SampleAuthenticationManager實例,對request進行認證操做
        Authentication result = am.authenticate(request);
        // 若認證成功,則保存返回的認證信息,包括已認證用戶的受權信息
        SecurityContextHolder.getContext().setAuthentication(result);
        break;
      } catch(AuthenticationException e) {
        System.out.println("Authentication failed: " + e.getMessage());
      }
    }
    System.out.println("Successfully authenticated. Security context contains: " +
      SecurityContextHolder.getContext().getAuthentication());
  }
}

在以上代碼中,咱們實現的 AuthenticationManager 將驗證用戶名和密碼相同的任何用戶。它爲每一個用戶分配一個角色。上面代碼的驗證過程是這樣的:

Please enter your username:
semlinker
Please enter your password:
12345
Authentication failed: Bad Credentials
Please enter your username:
semlinker
Please enter your password:
semlinker
Successfully authenticated. Security context contains: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@441d0230: Principal: semlinker; Credentials: [PROTECTED]; Authenticated: true; Details: null; Granted Authorities: ROLE_USER

4、 核心服務

4.1 AuthenticationManager,ProviderManager 和 AuthenticationProvider

AuthenticationManager(接口)是認證相關的核心接口,也是發起認證的出發點,由於在實際需求中,咱們可能會容許用戶使用用戶名 + 密碼登陸,同時容許用戶使用郵箱 + 密碼,手機號碼 + 密碼登陸,甚至,可能容許用戶使用指紋登陸,因此要求認證系統要支持多種認證方式。

Spring Security 中 AuthenticationManager 接口的默認實現是 ProviderManager,但它自己並不直接處理身份驗證請求,它會委託給已配置的 AuthenticationProvider 列表,每一個列表依次被查詢以查看它是否能夠執行身份驗證。每一個 Provider 驗證程序將拋出異常或返回一個徹底填充的 Authentication 對象。

也就是說,Spring Security 中核心的認證入口始終只有一個:AuthenticationManager,不一樣的認證方式:用戶名 + 密碼(UsernamePasswordAuthenticationToken),郵箱 + 密碼,手機號碼 + 密碼登陸則對應了三個 AuthenticationProvider。

下面咱們來看一下 ProviderManager 的核心源碼:

// spring-security-core-5.2.0.RELEASE-sources.jar
// org/springframework/security/authentication/ProviderManager.java
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;
        AuthenticationException parentException = null;
        Authentication result = null;
        Authentication parentResult = null;
        boolean debug = logger.isDebugEnabled();

    // 遍歷providers列表,判斷是否支持當前authentication對象的認證方式
        for (AuthenticationProvider provider : getProviders()) {
            if (!provider.supports(toTest)) {
                continue;
            }

            try {
        // 執行provider的認證方式並獲取返回結果
                result = provider.authenticate(authentication);

                if (result != null) {
                    copyDetails(authentication, result);
                    break;
                }
            }
            catch (AccountStatusException | InternalAuthenticationServiceException e) {
                prepareException(e, authentication);
                throw e;
            } catch (AuthenticationException e) {
                lastException = e;
            }
        }

    // 若當前ProviderManager沒法完成認證操做,且其包含父級認證器,則容許轉交給父級認證器嘗試進行認證
        if (result == null && parent != null) {
            try {
                result = parentResult = parent.authenticate(authentication);
            }
            catch (ProviderNotFoundException e) {
            }
            catch (AuthenticationException e) {
                lastException = parentException = e;
            }
        }

    
        if (result != null) {
            if (eraseCredentialsAfterAuthentication
                    && (result instanceof CredentialsContainer)) {
        // 完成認證,從authentication對象中移除私密數據
                ((CredentialsContainer) result).eraseCredentials();
            }
      
      // 若父級AuthenticationManager認證成功,則派發AuthenticationSuccessEvent事件
            if (parentResult == null) {
                eventPublisher.publishAuthenticationSuccess(result);
            }
            return result;
        }
    
    // 未認證成功,拋出ProviderNotFoundException異常
        if (lastException == null) {
            lastException = new ProviderNotFoundException(messages.getMessage(
                    "ProviderManager.providerNotFound",
                    new Object[] { toTest.getName() },
                    "No AuthenticationProvider found for {0}"));
        }
    
        if (parentException == null) {
            prepareException(lastException, authentication);
        }

        throw lastException;
    }
}

在 ProviderManager 進行認證的過程當中,會遍歷 providers 列表,判斷是否支持當前 authentication 對象的認證方式,若支持該認證方式時,就會調用所匹配 provider(AuthenticationProvider)對象的 authenticate 方法進行認證操做。若認證失敗則返回 null,下一個 AuthenticationProvider 會繼續嘗試認證,若是全部認證器都沒法認證成功,則 ProviderManager 會拋出一個 ProviderNotFoundException 異常。

4.2 DaoAuthenticationProvider

在 Spring Security 中較經常使用的 AuthenticationProvider 是 DaoAuthenticationProvider,這也是 Spring Security 最先支持的 AuthenticationProvider 之一。顧名思義,Dao 正是數據訪問層的縮寫,也暗示了這個身份認證器的實現思路。DaoAuthenticationProvider 類的內部結構以下:

dao-authentication-provider.jpg

在實際項目中,最多見的認證方式是使用用戶名和密碼。用戶在登陸表單中提交了用戶名和密碼,而對於已註冊的用戶,在數據庫中已保存了正確的用戶名和密碼,認證即是負責比對同一個用戶名,提交的密碼和數據庫中所保存的密碼是否相同即是了。

在 Spring Security 中,對於使用用戶名和密碼進行認證的場景,用戶在登陸表單中提交的用戶名和密碼,被封裝成了 UsernamePasswordAuthenticationToken,而根據用戶名加載用戶的任務則是交給了 UserDetailsService,在 DaoAuthenticationProvider 中,對應的方法就是 retrieveUser,雖然有兩個參數,可是 retrieveUser 只有第一個參數起主要做用,返回一個 UserDetails。retrieveUser 方法的具體實現以下:

// spring-security-core-5.2.0.RELEASE-sources.jar
// org/springframework/security/authentication/dao/DaoAuthenticationProvider.java
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;
        }
        catch (UsernameNotFoundException ex) {
            mitigateAgainstTimingAttack(authentication);
            throw ex;
        }
        catch (InternalAuthenticationServiceException ex) {
            throw ex;
        }
        catch (Exception ex) {
            throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
        }
}

在 DaoAuthenticationProvider 類的 retrieveUser 方法中,會以傳入的 username 做爲參數,調用 UserDetailsService 對象的 loadUserByUsername 方法加載用戶。

4.3 UserDetails 與 UserDetailsService

4.3.1 UserDetails 接口

在 DaoAuthenticationProvider 類中 retrieveUser 方法簽名是這樣的:

protected final UserDetails retrieveUser(String username,
    UsernamePasswordAuthenticationToken authentication) 
  throws AuthenticationException {
}

該方法返回 UserDetails 對象,這裏的 UserDetails 也是一個接口,它的定義以下:

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

顧名思義,UserDetails 表示詳細的用戶信息,這個接口涵蓋了一些必要的用戶信息字段,具體的實現類對它進行了擴展。前面咱們也介紹了一個 Authentication 接口,它與 UserDetails 接口的定義以下:

user-details-vs-authentication.png

雖然 Authentication 與 UserDetails 很相似,但它們之間是有區別的。Authentication 的 getCredentials() 與 UserDetails 中的 getPassword() 須要被區分對待,前者是用戶提交的密碼憑證,後者是用戶正確的密碼,認證器其實就是對這二者進行比對。

此外 Authentication 中的 getAuthorities() 實際是由 UserDetails 的 getAuthorities() 傳遞而造成的。還記得 Authentication 接口中的 getUserDetails() 方法嗎?其中的 UserDetails 用戶詳細信息就是通過了 provider (AuthenticationProvider) 認證以後被填充的。

4.3.2 UserDetailsService 接口

大多數身份驗證提供程序都利用了 UserDetailsUserDetailsService 接口。UserDetailsService 接口的定義以下:

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

在 UserDetailsService 接口中,只有一個 loadUserByUsername 方法,用於經過 username 來加載匹配的用戶。當找不到 username 對應用戶時,會拋出 UsernameNotFoundException 異常。UserDetailsService 和 AuthenticationProvider 二者的職責經常被人們搞混,記住一點便可,UserDetailsService 只負責從特定的地方(一般是數據庫)加載用戶信息,僅此而已。

UserDetailsService 常見的實現類有 JdbcDaoImpl,InMemoryUserDetailsManager,前者從數據庫加載用戶,後者從內存中加載用戶,固然你也能夠本身實現 UserDetailsService。

4.4 Spring Security Architecture

前面咱們已經介紹了 Spring Security 的核心組件(SecurityContextHolder,SecurityContext 和 Authentication)和核心服務(AuthenticationManager,ProviderManager 和 AuthenticationProvider),最後咱們再來回顧一下 Spring Security 總體架構:

spring-security-arch.jpg

5、參考資源

full-stack-logo

相關文章
相關標籤/搜索