Spring Security原理與應用


Spring Security是什么

Spring Security是一個能夠為基於Spring的企業應用系統提供聲明式的安全訪問控制解決方案的安全框架。它提供了一組可以在Spring應用上下文中配置的Bean(注:包括認證與權限獲取、配置、處理相關實例),充分利用了Spring IoC,DI(控制反轉Inversion of Control ,DI:Dependency Injection 依賴注入)和AOP(面向切面編程)(注:代理增強類)功能,為應用系統提供聲明式的安全訪問控制功能,減少了為企業系統安全控制編寫大量重復代碼的工作。

核心類庫與認證流程

核心驗證器

AuthenticationManager

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

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

驗證邏輯

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

ProviderManager

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

實現邏輯

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是否支持當前的認證類型,不支持的話繼續遍歷 for (AuthenticationProvider provider : getProviders()) { if (!provider.supports(toTest)) { continue; } if (debug) { logger.debug("Authentication attempt using " + provider.getClass().getName()); } try { #3.支持的話調用providerauthenticat方法認證 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,然后依次執行該 Provider 的驗證方法
    • 如果某一個 Provider 驗證成功,則跳出循環不再執行后續的驗證;
    • 如果驗證成功,會將返回的 result 既 Authentication 對象進一步封裝為 Authentication Token; 比如 UsernamePasswordAuthenticationToken、RememberMeAuthenticationToken 等;這些 Authentication Token 也都繼承自 Authentication 對象;
  2. 如果 #1 沒有任何一個 Provider 驗證成功,則試圖使用其 parent Authentication Manager 進行驗證;
  3. 是否需要擦除密碼等敏感信息;

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;

AuthenticationProvider

ProviderManager 通過 AuthenticationProvider 擴展出更多的驗證提供的方式;而 AuthenticationProvider 本身也就是一個接口,從類圖中我們可以看出它的實現類AbstractUserDetailsAuthenticationProvider AbstractUserDetailsAuthenticationProvider的子類DaoAuthenticationProvider DaoAuthenticationProvider Spring Security中一個核心的Provider,對所有的數據庫提供了基本方法和入口。

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.獲取用戶信息由子類實現即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類實現(主要判斷當前用戶是否鎖定,過期,凍結User接口) preAuthenticationChecks.check(user); #3.子類實現 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主要實現了AuthenticationProvider的接口方法authenticate 並提供了相關的驗證邏輯;

  1. 獲取用戶返回UserDetailsAbstractUserDetailsAuthenticationProvider定義了一個抽象的方法
    protected abstract UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException; 
  2. 三步驗證工作
    1. preAuthenticationChecks
    2. additionalAuthenticationChecks(抽象方法,子類實現)
    3. postAuthenticationChecks

    3.將已通過驗證的用戶信息封裝成 UsernamePasswordAuthenticationToken 對象並返回;該對象封裝了用戶的身份信息,以及相應的權限信息,相關源碼如下:

    protected Authentication createSuccessAuthentication(Object principal,
     Authentication authentication, UserDetails user) {
     UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken( principal, authentication.getCredentials(), authoritiesMapper.mapAuthorities(user.getAuthorities()));
    result.setDetails(authentication.getDetails());

     return result; }

DaoAuthenticationProvider

DaoAuthenticationProvider主要做了以下事情

  1. 對用戶身份進行加密操作;
     #1.可直接返回BCryptPasswordEncoder,也可以自己實現該接口使用自己的加密算法
    核心方法
    String encode(CharSequence rawPassword);

    boolean matches(CharSequence rawPassword, String encodedPassword); private PasswordEncoder passwordEncoder;
  2. 實現了 AbstractUserDetailsAuthenticationProvider 兩個抽象方法,
    1. 獲取用戶信息的擴展點
      protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { UserDetails loadedUser; try { loadedUser = this.getUserDetailsService().loadUserByUsername(username); } 

      主要是通過注入UserDetailsService接口對象,並調用其接口方法 loadUserByUsername(String username) 獲取得到相關的用戶信息。UserDetailsService接口非常重要。

    2. 實現 additionalAuthenticationChecks 的驗證方法(主要驗證密碼);

UserDetailsService

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

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

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

UserDetails

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(); }

JdbcDaoImpl

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

JdbcDaoImpl的子類(實現了UserDetailsManager):

JdbcUserDetailsManager

該實現類主要是提供基於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 = ?"; 

說明:

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

特別注意UserDetailsServiceUserDetails以及UserDetailsManager都是可被用戶自定義的擴展點,我們可以繼承這些接口提供自己的讀取用戶來源和管理用戶的方法,比如我們可以自己實現一個 與特定 ORM 框架,比如 Mybatis 或者 Hibernate,相關的UserDetailsServiceUserDetailsManager

UserDetailsManager的另一個實現類:

InMemoryUserDetailsManager

該實現類主要是提供基於內存對 User 進行增、刪、查、改的方法

public class InMemoryUserDetailsManager implements UserDetailsManager { 
  protected final Log logger = LogFactory.getLog(getClass());
  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);
	  }
  }`

時序圖

 

 轉載編輯自 http://niocoder.com/2018/01/02/Spring-Security%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%E4%B8%80-Spring-Security%E8%AE%A4%E8%AF%81%E8%BF%87%E7%A8%8B/

Spring Security項目案例(GitHub地址)

https://github.com/Xiaobai0419/xiaobai

 

 

 

 

















 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM