Spring Framework 爲開發 Java 應用程序提供了全面的基礎架構支持。它包含了一些不錯的功能,如 "依賴注入",以及一些現成的模塊:html
這些模塊能夠大大減小應用程序的開發時間。例如,在 Java Web 開發的早期,咱們須要編寫大量樣板代碼以將記錄插入數據源。可是,經過使用 Spring JDBC 模塊的 JDBCTemplate,咱們能夠僅經過少許配置將其簡化爲幾行代碼。java
閱讀更多關於 Angular、TypeScript、Node.js/Java 、Spring 等技術文章,歡迎訪問個人我的博客 —— 全棧修仙之路
Spring Boot 是基於 Spring Framework,它爲你的 Spring 應用程序提供了自動裝配特性,它的設計目標是讓你儘量快的上手應用程序的開發。如下是 Spring Boot 所擁有的一些特性:spring
Spring Security 是一個可以爲基於 Spring 的企業應用系統提供聲明式的安全訪問控制解決方案的安全框架。它提供了一組能夠在 Spring 應用上下文中配置的 Bean,充分利用了 Spring IoC(Inversion of Control 控制反轉),DI(Dependency Injection 依賴注入)和 AOP(面向切面編程)功能,爲應用系統提供聲明式的安全訪問控制功能,減小了爲企業系統安全控制編寫大量重複代碼的工做。數據庫
Spring Security 擁有如下特性:編程
Spring、Spring Boot 和 Spring Security 三者的關係以下圖所示:安全
目前 Spring Security 5 支持與如下技術進行集成:架構
在進入 Spring Security 正題以前,咱們先來了解一下它的總體架構:框架
最基本的對象是 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); }
在 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 接口的實現類,咱們能夠獲得用戶擁有的權限信息列表,密碼,用戶細節信息,用戶身份信息,認證信息等。
下面咱們來簡單總結一下 SecurityContextHolder,SecurityContext 和 Authentication 這個三個對象之間的關係,SecurityContextHolder 用來保存 SecurityContext (安全上下文對象),經過調用 SecurityContext 對象中的方法,如 getAuthentication 方法,咱們能夠方便地獲取 Authentication 對象,利用該對象咱們能夠進一步獲取已認證用戶的詳細信息。
SecurityContextHolder,SecurityContext 和 Authentication 的詳細定義以下所示:
讓咱們考慮一個每一個人都熟悉的標準身份驗證方案:
前三項構成了身份驗證進程,所以咱們將在 Spring Security 中查看這些內容。
UsernamePasswordAuthenticationToken
的實例中(咱們以前看到的Authentication
接口的實例)。AuthenticationManager
的實例以進行驗證。AuthenticationManager
在成功驗證時返回徹底填充的 Authentication
實例。SecurityContextHolder.getContext().setAuthentication(…)
建立的,傳入返回的身份驗證 Authentication 對象。瞭解完上述的身份驗證流程,咱們來看一個簡單的示例:
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
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
異常。
在 Spring Security 中較經常使用的 AuthenticationProvider 是 DaoAuthenticationProvider,這也是 Spring Security 最先支持的 AuthenticationProvider 之一。顧名思義,Dao 正是數據訪問層的縮寫,也暗示了這個身份認證器的實現思路。DaoAuthenticationProvider 類的內部結構以下:
在實際項目中,最多見的認證方式是使用用戶名和密碼。用戶在登陸表單中提交了用戶名和密碼,而對於已註冊的用戶,在數據庫中已保存了正確的用戶名和密碼,認證即是負責比對同一個用戶名,提交的密碼和數據庫中所保存的密碼是否相同即是了。
在 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 方法加載用戶。
在 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 接口的定義以下:
雖然 Authentication 與 UserDetails 很相似,但它們之間是有區別的。Authentication 的 getCredentials() 與 UserDetails 中的 getPassword() 須要被區分對待,前者是用戶提交的密碼憑證,後者是用戶正確的密碼,認證器其實就是對這二者進行比對。
此外 Authentication 中的 getAuthorities() 實際是由 UserDetails 的 getAuthorities() 傳遞而造成的。還記得 Authentication 接口中的 getUserDetails() 方法嗎?其中的 UserDetails 用戶詳細信息就是通過了 provider (AuthenticationProvider) 認證以後被填充的。
大多數身份驗證提供程序都利用了 UserDetails
和 UserDetailsService
接口。UserDetailsService 接口的定義以下:
public interface UserDetailsService { UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; }
在 UserDetailsService 接口中,只有一個 loadUserByUsername 方法,用於經過 username 來加載匹配的用戶。當找不到 username 對應用戶時,會拋出 UsernameNotFoundException 異常。UserDetailsService 和 AuthenticationProvider 二者的職責經常被人們搞混,記住一點便可,UserDetailsService 只負責從特定的地方(一般是數據庫)加載用戶信息,僅此而已。
UserDetailsService 常見的實現類有 JdbcDaoImpl,InMemoryUserDetailsManager,前者從數據庫加載用戶,後者從內存中加載用戶,固然你也能夠本身實現 UserDetailsService。
前面咱們已經介紹了 Spring Security 的核心組件(SecurityContextHolder,SecurityContext 和 Authentication)和核心服務(AuthenticationManager,ProviderManager 和 AuthenticationProvider),最後咱們再來回顧一下 Spring Security 總體架構: