1. 程式人生 > >Java安全框架(一)Spring Security

Java安全框架(一)Spring Security

文章主要分三部分 1、Spring Security的架構及核心元件:(1)認證;(2)許可權攔截;(3)資料庫管理;(4)許可權快取;(5)自定義決策; 2、環境搭建與使用,使用當前熱門的Spring Boot來搭建環境,結合專案中實際的例子來做幾個Case; 3、Spring Security的優缺點總結,結合第二部分中幾個Case的實現來總結Spring Security的優點和缺點。 ## 1、Spring Security介紹 ​ 整體介紹,Spring Security為基於J2EE開發的企業應用軟體提供了全面的安全服務,特別是使用Spring開發的企業軟體專案,如果你熟悉Spring,尤其是Spring的依賴注入原理,這將幫助你更快掌握Spring Security,目前使用Spring Security有很多原因,通常因為在J2EE的Servlet規範和EJB規範中找不到典型應用場景的解決方案,提到這些規範,特別要指出的是它們不能在WAR或EAR級別進行移植,這樣如果你需要更換伺服器環境,就要在新的目標環境中進行大量的工作,對你的應用進行重新配置安全,使用Spring Security就解決了這些問題,也為你提供了很多很有用的可定製的安全特性。 ​ Spring Security包含三個主要的元件:`SecurityContext`、`AuthenticationManager`、`AccessDecisionManager`.
圖1-1 Spring Security主要元件
### 1.1 認證 ​ Spring Security提供了很多過濾器,它們攔截Servlet請求,並將這些請求轉交給認證處理過濾器和訪問決策過濾器進行處理,並強制安全性認證使用者身份和使用者許可權以達到保護WEB資源的目的,Spring Security安全機制包括兩個主要的操作,**認證**和**驗證**,驗證也可以稱為許可權控制,這是Spring Security兩個主要的方向,認證是為使用者建立一個他所宣告的主體的過程,這個主體一般是指使用者裝置或可以在系統中執行行動的其他系統,驗證指使用者能否在應用中執行某個操作,在到達授權判斷之前身份的主體已經由身份認證過程建立了。下面列出幾種常用認證模式,這裡不對它們作詳細介紹,需要詳細瞭解的老鐵們可以自行查查對應的資料。 1. `Basic`:`HTTP1.0`提出,一種基於challenge/response的認證模式,針對特定的realm需要提供使用者名稱和密碼認證後才可訪問,其中密碼使用明文傳輸。缺點:①無狀態導致每次通訊都要帶上認證資訊,即使是已經認證過的資源;②傳輸安全性不足,認證資訊用`Base64`編碼,基本就是明文傳輸,很容易對報文擷取並盜用認證資訊。 2. `Digest`:`HTTP1.1`提出,它主要是為了解決Basic模式安全問題,用於替代原來的Basic認證模式,Digest認證也是採用challenge/response認證模式,基本的認證流程比較類似。Digest模式避免了密碼在網路上明文傳輸,提高了安全性,但它仍然存在缺點,例如認證報文被攻擊者攔截到攻擊者可以獲取到資源。 3. `X.509`:證書認證,`X.509`是一種非常通用的證書格式,證書包含版本號、序列號(唯一)、簽名、頒發者、有效期、主體、主體公鑰。 4. `LDAP`:輕量級目錄訪問協議(Lightweight Directory Access Protocol)。 5. `Form`:基於表單的認證模式。 ### 1.2 許可權攔截
圖1-2 使用者請求
圖1-3 過濾器
​ Spring Security提供了很多過濾器,其中`SecurityContextPersistenceFilter`、`UsernamePasswordAuthenticationFilter`、`FilterSecurityInterceptor`分別對應`SecurityContext`、`AuthenticationManager`、`AccessDecisionManager`的處理。
圖1-4 Spring Security過濾鏈流程圖
下面分別介紹各個過濾器的功能。 | 過濾器 | 描述 | | ----------------------------------------- | ------------------------------------------------------------ | | `WebAsyncManagerIntegrationFilter` | 設定`SecurityContext`到非同步執行緒中,用於獲取使用者上下文資訊 | | `SecurityContextPersistenceFilter` | 整個請求過程中`SecurityContext`的建立和清理
1.未登入,`SecurityContext`為null,建立一個新的`ThreadLocal`的`SecurityContext`填充`SecurityContextHolder`.
2.已登入,從`SecurityContextRepository`獲取的`SecurityContext`物件.
兩個請求完成後都清空`SecurityContextHolder`,並更新`SecurityContextRepository` | | `HeaderWriterFilter` | 新增頭資訊到響應物件 | | `CsrfFilter` | 防止csrf攻擊(跨站請求偽造)的過濾器 | | `LogoutFilter` | 登出處理 | | `UsernamePasswordAuthenticationFilter` | 獲取表單使用者名稱和密碼,處理基於表單的登入請求 | | `DefaultLoginPageGeneratingFilter` | 配置登入頁面 | | `BasicAuthenticationFilter` | 檢測和處理http basic認證,將結果放進`SecurityContextHolder` | | `RequestCacheAwareFilter` | 處理請求request的快取 | | `SecurityContextHolderAwareRequestFilter` | 包裝請求request,便於訪問`SecurityContextHolder` | | `AnonymousAuthenticationFilter` | 匿名身份過濾器,不存在使用者資訊時呼叫該過濾器 | | `SessionManagementFilter` | 檢測有使用者登入認證時做相應的session管理 | | `ExceptionTranslationFilter` | 處理`AccessDeniedException`訪問異常和`AuthenticationException`認證異常 | | `FilterSecurityInterceptor` | 檢測使用者是否具有訪問資源路徑的許可權 | ### 1.3 資料庫管理
圖1-5 Spring Security核心處理流程
​ 上圖展示的Spring Security核心處理流程。當一個使用者登入時,會先進行身份認證,如果身份認證未通過會要求使用者重新認證,當用戶身份證通過後就會呼叫角色管理器判斷他是否可以訪問,這裡,如果要實現資料庫管理使用者及許可權,就需要自定義使用者登入功能,Spring Security已經提供好了一個介面`UserDetailsService`。 ```java package org.springframework.security.core.userdetails; public interface UserDetailsService { /** * Locates the user based on the username. In the actual implementation, the search * may possibly be case sensitive, or case insensitive depending on how the * implementation instance is configured. In this case, the UserDetails
* object that comes back may have a username that is of a different case than what * was actually requested.. * * @param username the username identifying the user whose data is required. * * @return a fully populated user record (never null) * * @throws UsernameNotFoundException if the user could not be found or the user has no * GrantedAuthority */ UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; } ``` ​ `UserDetailService`該介面只有一個方法,通過方法名可以看出方法是通過使用者名稱來獲取使用者資訊的,但返回結果是`UserDetails`物件,`UserDetails`也是一個介面,介面中任何一個方法返回false使用者的憑證就會被視為無效。 ```java package org.springframework.security.core.userdetails; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import java.io.Serializable; import java.util.Collection; /** * Provides core user information. * *

* Implementations are not used directly by Spring Security for security purposes. They * simply store user information which is later encapsulated into {@link Authentication} * objects. This allows non-security related user information (such as email addresses, * telephone numbers etc) to be stored in a convenient location. *

* Concrete implementations must take particular care to ensure the non-null contract * detailed for each method is enforced. See * {@link org.springframework.security.core.userdetails.User} for a reference * implementation (which you might like to extend or use in your code). * * @see UserDetailsService * @see UserCache * * @author Ben Alex */ public interface UserDetails extends Serializable { // ~ Methods // ======================================================================================================== /** * Returns the authorities granted to the user. Cannot return null. * * @return the authorities, sorted by natural key (never null) */ Collection getAuthorities(); //許可權集合 /** * Returns the password used to authenticate the user. * * @return the password */ String getPassword(); //密碼 /** * Returns the username used to authenticate the user. Cannot return null. * * @return the username (never null) */ String getUsername(); //使用者名稱 /** * Indicates whether the user's account has expired. An expired account cannot be * authenticated. * * @return true if the user's account is valid (ie non-expired), * false if no longer valid (ie expired) */ boolean isAccountNonExpired(); //賬戶是否過期 /** * Indicates whether the user is locked or unlocked. A locked user cannot be * authenticated. * * @return true if the user is not locked, false otherwise */ boolean isAccountNonLocked(); //賬戶是否被鎖定 /** * Indicates whether the user's credentials (password) has expired. Expired * credentials prevent authentication. * * @return true if the user's credentials are valid (ie non-expired), * false if no longer valid (ie expired) */ boolean isCredentialsNonExpired(); //證書是否過期 /** * Indicates whether the user is enabled or disabled. A disabled user cannot be * authenticated. * * @return true if the user is enabled, false otherwise */ boolean isEnabled(); //賬戶是否有效 } ``` ​ 這裡需要注意的是`Authentication`與`UserDetails`物件的區分,`Authentication`物件才是Spring Security使用的進行安全訪問控制使用者資訊的安全物件,實際上`Authentication`物件有未認證和已認證兩種狀態,在作為引數傳入認證管理器的時候,它是一個為認證的物件,它從客戶端獲取使用者的身份認證資訊,如使用者名稱、密碼,可以是從一個登入頁面,也可以是從cookie中獲取,並由系統自動生成一個`Authentication`物件,而這裡的`UserDetails`代表的是一個使用者安全資訊的源,這個源可以是從資料庫、LDAP伺服器、CA中心返回,Spring Security要做的就是將未認證的`Authentication`物件與`UserDetails`物件進行匹配,成功後將`UserDetails`物件中的許可權資訊拷貝到`Authentication`中,組成一個完整的`Authentication`物件,與其他元件進行共享。 ```java package org.springframework.security.core; import java.io.Serializable; import java.security.Principal; import java.util.Collection; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.context.SecurityContextHolder; public interface Authentication extends Principal, Serializable { /**許可權集合*/ Collection getAuthorities(); /**獲取憑證*/ Object getCredentials(); /**獲取認證一些額外資訊*/ Object getDetails(); /**過去認證的實體*/ Object getPrincipal(); /**是否認證通過*/ boolean isAuthenticated(); /** * See {@link #isAuthenticated()} for a full description. *

* Implementations should always allow this method to be called with a * false parameter, as this is used by various classes to specify the * authentication token should not be trusted. If an implementation wishes to reject * an invocation with a true parameter (which would indicate the * authentication token is trusted - a potential security risk) the implementation * should throw an {@link IllegalArgumentException}. * * @param isAuthenticated true if the token should be trusted (which may * result in an exception) or false if the token should not be trusted * * @throws IllegalArgumentException if an attempt to make the authentication token * trusted (by passing true as the argument) is rejected due to the * implementation being immutable or implementing its own alternative approach to * {@link #isAuthenticated()} */ void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException; } ``` ​ 瞭解了Spring Security的上面三個物件,當我們需要資料庫管理使用者時,我們需要手動實現`UserDetailsService`物件中的`loadUserByUsername`方法,這就需要我們同時準備以下幾張資料表,分別是使用者表(user)、角色表(role)、許可權表(permission)、使用者和角色關係表(user_role)、許可權和角色關係表(permission_role),`UserDetails`中的使用者狀態通過使用者表裡的屬性去填充,`UserDetails`中的許可權集合則是通過角色表、許可權表、使用者和角色關係表、許可權和角色關係表構成的RBAC模型來提供,這樣就可以把使用者認證、使用者許可權集合放在資料庫中進行管理了。 ### 1.4 許可權快取 ​ Spring Security的許可權快取和資料庫管理有關,都是在使用者認證上做文章,所以都與`UserDetails`有關,與資料庫管理不同的是,Spring Security提供了一個可以快取`UserDetailsService`的實現類,這個類的名字是`CachingUserDetailsService` ```java package org.springframework.security.authentication; import org.springframework.security.core.userdetails.UserCache; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.cache.NullUserCache; import org.springframework.util.Assert; /** * * @author Luke Taylor * @since 2.0 */ public class CachingUserDetailsService implements UserDetailsService { private UserCache userCache = new NullUserCache(); private final UserDetailsService delegate; public CachingUserDetailsService(UserDetailsService delegate) { this.delegate = delegate; } public UserCache getUserCache() { return userCache; } public void setUserCache(UserCache userCache) { this.userCache = userCache; } public UserDetails loadUserByUsername(String username) { UserDetails user = userCache.getUserFromCache(username); //快取中不存在UserDetails時,通過UserDetailsService載入 if (user == null) { user = delegate.loadUserByUsername(username); } Assert.notNull(user, () -> "UserDetailsService " + delegate + " returned null for username " + username + ". " + "This is an interface contract violation"); //將UserDetials存入快取,並將UserDetails返回 userCache.putUserInCache(user); return user; } } ``` ​ `CachingUserDetailsService`類的構造接收一個用於真正載入`UserDetails`的`UserDetailsService`實現類,當需要載入`UserDetails`時,會首先從快取中獲取,如果快取中沒有`UserDetails`存在,則使用持有的`UserDetailsService`實現類進行載入,然後將載入後的結果存在快取中,`UserDetails`與快取的互動是通過`UserCache`介面來實現的,`CachingUserDetailsService`預設擁有一個`UserCache`的`NullUserCache()`實現。Spring Security提供的快取都是基於記憶體的快取,並且快取的`UserDetails`物件,在實際應用中一般會用到更多的快取,比如Redis,同時也會對許可權相關的資訊等更多的資料進行快取。 ### 2.5 自定義決策 ​ Spring Security在使用者身份認證通過後,會呼叫一個角色管理器判斷是否可以繼續訪問,[Spring Security核心處理流程(圖1-5)](#1.3 資料庫管理)中的`AccessDecisionManager`就是Spring Security的角色管理器,它對應的抽象類為`AbstractAccessDecisionManager`,要自定義決策管理器的話一般是繼承這個抽象類,而不是去實現介面。 ```java package org.springframework.security.access.vote; import java.util.List; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.security.access.AccessDecisionManager; import org.springframework.security.access.AccessDecisionVoter; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.core.SpringSecurityMessageSource; import org.springframework.beans.factory.InitializingBean; import org.springframework.context.MessageSource; import org.springframework.context.MessageSourceAware; import org.springframework.context.support.MessageSourceAccessor; import org.springframework.util.Assert; /** * Abstract implementation of {@link AccessDecisionManager}. * *

* Handles configuration of a bean context defined list of {@link AccessDecisionVoter}s * and the access control behaviour if all voters abstain from voting (defaults to deny * access). */ public abstract class AbstractAccessDecisionManager implements AccessDecisionManager, InitializingBean, MessageSourceAware { protected final Log logger = LogFactory.getLog(getClass()); private List

* If one or more voters cannot support the presented class, false is * returned. * * @param clazz the type of secured object being presented * @return true if this type is supported */ public boolean supports(Class clazz) { for (AccessDecisionVoter voter : this.decisionVoters) { if (!voter.supports(clazz)) { return false; } } return true; } } ``` ​ 裡面的核心方法是`supports`方法,方法中用到一個`decisionVoters`的集合,集合中的型別是`AccessDecisionVoter`,這是Spring Security引入的一個投票器,有無許可權訪問的最終決定權就是由投票器來決定的。 ```java package org.springframework.security.access; import java.util.Collection; import org.springframework.security.core.Authentication; public interface AccessDecisionVoter { int ACCESS_GRANTED = 1; int ACCESS_ABSTAIN = 0; int ACCESS_DENIED = -1; boolean supports(ConfigAttribute attribute); boolean supports(Class clazz); int vote(Authentication authentication, S object, Collection