Spring Secutity和Apache Shiro是Java領域的兩大主流開源安全框架,也是許可權系統設計的主要技術選型。本文主要介紹Spring Secutity的實現原理,並基於Spring Secutity設計基於RBAC的許可權系統。 ...
Spring Secutity和Apache Shiro是Java領域的兩大主流開源安全框架,也是許可權系統設計的主要技術選型。本文主要介紹Spring Secutity的實現原理,並基於Spring Secutity設計基於RBAC的許可權系統。
一、技術選型
為何把Spring Secutity作為許可權系統的技術選型,主要考慮了以下幾個方面:
- 數據鑒權的能力:Spring Secutity支持數據鑒權,即細粒度許可權控制。
- Spring生態基礎:Spring Secutity可以和Spring生態無縫集成。
- 多樣認證能力:Spring Secutity支持多樣認證方式,如預認證方式可以與第三方認證系統集成。
Spring Security | Apache Shiro | |
---|---|---|
認證 | 支持多種認證方式(如密碼、匿名、預認證) | 簡單登錄認證 |
鑒權 | 功能鑒權、數據鑒權 | 功能鑒權 |
多源適配 | Mem、JDBC、DAO、LDAP、 OpenID、OAuth等 |
LDAP、JDBC、Kerberos、 ActiveDirectory等 |
加密 | 支持多種加密方式 | 簡單加密方式 |
運行環境 | 依賴Spring | 可獨立運行 |
開放性 | 開源、Spring生態基礎 | 開源 |
複雜度 | 複雜、較重 | 簡單、靈活 |
二、核心架構
許可權系統一般包含兩大核心模塊:認證(Authentication)和鑒權(Authorization)。
- 認證:認證模塊負責驗證用戶身份的合法性,生成認證令牌,並保存到服務端會話中(如TLS)。
- 鑒權:鑒權模塊負責從服務端會話內獲取用戶身份信息,與訪問的資源進行許可權比對。
官方給出的Spring Security的核心架構圖如下:
核心架構解讀:
- AuthenticationManager:負責認證管理,解析用戶登錄信息(封裝在Authentication),讀取用戶、角色、許可權信息進行認證,認證結果被回填到Authentication,保存在SecurityContext。
- AccessDecisionManager:負責鑒權投票表決,彙總投票器的結果,實現一票通過(預設)、多票通過、一票否決策略。
- SecurityInterceptor:負責許可權攔截,包括Web URL攔截和方法調用攔截。通過ConfigAttributes獲取資源的描述信息,藉助於AccessDecisionManager進行鑒權攔截。
- SecurityContext:安全上下文,保存認證結果。提供了全局上下文、線程繼承上下文、線程獨立上下文(預設)三種策略。
- Authentication:認證信息,保存用戶的身份標示、許可權列表、證書、認證通過標記等信息。
- SecuredResource:被安全管控的資源,如Web URL、用戶、角色、自定義領域對象等。
- ConfigAttributes:資源屬性配置,描述安全管控資源的信息,為SecurityInterceptor提供攔截邏輯的輸入。
三、設計原理
通過對源碼的分析,我把Spring Security的核心領域模型設計整理如下:
全局抽象模型解讀:
- 配置:AuthenticationConfiguration負責認證系統的全局配置,GlobalMethodSecurityConfiguration負責方法調用攔截的全局配置。
- 構建:AuthenticationConfiguration通過AuthenticationManagerBuilder構建認證管理器AuthenticationManager,GlobalMethodSecurityConfiguration會自動初始化AbstractSecurityInterceptor進行方法調用攔截。
- Web攔截:HttpSecurity對Web進行安全配置,內置了大量GenericFilterBean過濾器對URL進行攔截。負責認證的過濾器會通過AuthenticationManager進行認證,並將認證結果保存到SecurityContext。
- 方法攔截:Spring通過AOP技術(cglib/aspectj)對標記為@PreAuthorize、@PreFilter、@PostAuthorize、@PostFilter等註解的方法進行攔截,通過AbstractSecurityInterceptor調用AuthenticationManager進行身份認證(如果必要的話)。
- 認證:認證管理器AuthenticationManager內置了多種認證器AuthenticationProvider,只要其中一個認證通過,認證便成功。不同的AuthenticationProvider獲取各自需要的信息(HTTP請求、資料庫查詢、遠程服務等)進行認證,認證結果全部封裝在Authentication。需要載入用戶、角色、許可權信息的認證器(如密碼認證、預認證等)需要對接UserDetailsManager介面實現用戶CRUD功能。
- 鑒權:許可權攔截器AbstractSecurityInterceptor通過讀取不同的SecurityMetadataSource載入需要被鑒權資源的描述信息ConfigAttribute,然後把認證信息Authentication、資源描述ConfigAttribute、資源對象本身傳遞給AccessDecisionManager進行表決。AccessDecisionManager內置了多個投票器AccessDecisionVoter,投票器會將鑒權信息中的ConfigAttribute轉換為SpringEL的格式,通過表達式處理器SecurityExpressionHandler執行基於表達式的鑒權邏輯,鑒權邏輯會通過反射的方式轉發到SecurityExpressionRoot的各個操作上去。
- 定製:通過WebSecurityConfigureAdapter可以定製HTTP安全配置HttpSecurity和認證管理器生成器AuthenticationManagerBuilder;通過AbstractPreAuthenticatedProcessingFilter可以定製預認證過濾器;通過UserDetailsManager和UserDetails介面可以對接自定義數據源;通過GrantedAuthority定製許可權信息;通過PermissionEvaluator可以定製自定義領域模型的訪問控制邏輯。
四、應用集成
理清Spring Security的定製點後,就可以在系統內部集成Spring Security了。
這裡使用預認證的方式,以適配第三方認證系統。AbstractPreAuthenticatedProcessingFilter提供了預認證的擴展點,基於該抽象類實現一個自定義認證過濾器。
public class MyPreAuthFilter extends AbstractPreAuthenticatedProcessingFilter {
@Override
protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
// 從第三方系統獲取用戶ID
return userId;
}
@Override
protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
return "";
}
}
Spring Security會根據預認證過濾器getPreAuthenticatedPrincipal返回的用戶ID信息,載入用戶角色等初始信息。這裡需要實現UserDetailsManager介面,提供用戶信息管理器。
@Service
public class MyUserManager implements UserDetailsManager {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 從資料庫載入用戶信息
return user;
}
// 其他管理介面
}
UserDetails內包含了GrantedAuthority介面類型的許可權信息抽象,一般可以基於它自定義角色和許可權。Spring Security使用一種介面形式表達角色和許可權,角色和許可權的差別是角色的ID是以"ROLE_"為首碼。
public class MyRole implements GrantedAuthority {
private final String role;
@Override
public String getAuthority() {
return "ROLE_" + role;
}
}
public class MyAuthority implements GrantedAuthority {
private final String authority;
@Override
public String getAuthority() {
return authority;
}
}
接下來註冊自定義認證過濾器和用戶管理器,這裡需要實現WebSecurityConfigurerAdapter進行Web安全配置。
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, mode = AdviceMode.PROXY)
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
UserDetailsManager userDetailsManager;
@Bean
protected AuthenticationProvider createPreAuthProvider() {
// 註冊用戶管理器
PreAuthenticatedAuthenticationProvider provider = new PreAuthenticatedAuthenticationProvider();
provider.setPreAuthenticatedUserDetailsService(new UserDetailsByNameServiceWrapper<>(userDetailsManager));
return provider;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 註冊預認證過濾器
http.addFilter(new MyPreAuthFilter(authenticationManager()));
}
}
這樣,最簡單的Spring Security框架集成內系統內部已經完成了。在系統的任意服務介面上可以使用如下方式進行鑒權。
public interface MyService {
@PreAuthorize("hasAuthority('QUERY')")
Object getById(String id);
@PreAuthorize("hasRole('ADMIN')")
void deleteById(String id);
}
PreAuthorize註解表示調用前鑒權,Spring使用預設使用動態代理技術生成鑒權邏輯。註解內配置了SpringEL表達式來定製鑒權方式。上述代碼中,hasAuthority會檢查用戶是否有QUERY許可權,hasRole會檢查用戶是否有ADMIN角色。
使用動態代理的方式進行AOP,只允許在介面層面進行許可權攔截,如果想在任意的方法上進行許可權攔截,那麼就需要藉助於AspectJ的方式進行AOP。首先將註解EnableGlobalMethodSecurity的mode設置為AdviceMode.ASPECTJ,然後添加JVM啟動參數,這樣就可以在任意方法上使用Spring Security的註解了。
-javaagent:/path/to/org/aspectj/aspectjweaver/1.9.4/aspectjweaver-1.9.4.jar
以上還是只是以用戶的身份信息(角色/許可權)進行許可權,靈活度有限,也發揮不了Spring Security的數據鑒權的能力。要使用數據鑒權,需要實現一個Spring Bean。
@Component
public class MyPermissionEvaluator implements PermissionEvaluator {
@Override
public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {
// 自定義數據鑒權
return false;
}
@Override
public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
// 自定義數據鑒權
return false;
}
}
PermissionEvaluator會被自動註冊到Spring Security框架,並允許在註解內使用如下方式進行鑒權。
@PreAuthorize("hasPermission(#id, 'QUERY')")
Object func1(String id) {
}
@PreAuthorize("hasPermission(#id, 'TABLE', 'QUERY')")
Object func2(String id) {
}
其中,func1的註解表示校驗用戶是否對id有QUERY許可權,代碼邏輯路由到MyPermissionEvaluator的第一個介面。func2的註解表示校驗用戶是否對TABLE類型的id有QUERY許可權,代碼邏輯路由到MyPermissionEvaluator的第二個介面。PermissionEvaluator提供了許可權系統中數據鑒權的擴展點,稍後會描述如何利用該擴展點定製基於RBAC的許可權系統。
五、許可權系統
構建基於RBAC(Role Based Access Control)的許可權系統,需要明確用戶、角色、許可權、資源這幾個核心的概念類的含義和它們之間的關係。
- 資源:許可權系統內需要安全控制的客體,一般是系統內的數據或功能。
- 許可權:描述了資源上的操作抽象,一般是一種動作。
- 授權:是許可權和資源的組合,表示對資源的某一個操作。
- 角色:描述了一組授權的集合,表示一類特殊概念的功能集。
- 用戶:許可權系統的主體,一般是當前系統的訪問用戶,用戶可以擁有多種角色。
以下是我們設計的基於RABC的許可權核心領域模型:
一般情況下,系統內需要許可權管控的資源是無法用戶自定義的,因為資源會耦合大量的業務邏輯,所以我們提供了自 資源工廠,通過配置化的方式構建業務模塊所需的資源。而用戶、角色、許可權,以及授權記錄都是可以通過相應的管理器進行查詢更新。
另外,資源抽象允許表達資源的繼承和組合關係,繼而表達更複雜的資源模型,資源統一鑒權的流程為:
- 執行鑒權時,首先看資源是原子資源還是組合資源。
- 對於原子資源,先查詢是否有授權記錄,再查看角色預授權是否包含當前授權,存在一種便成功。
- 沒有授權記錄和角色預授權的原子資源,嘗試用父資源(如果有的話)代替鑒權,否則鑒權失敗。
- 對於組合資源,先進行資源展開,獲取子資源列表。
- 遍歷子資源列表,並依次對子資源進行鑒權,子資源鑒權結果彙總後,即組合資源鑒權結果。
綜上,基於統一資源抽象和資源配置化構建,可以實現資源的統一構建,繼而實現統一鑒權。
六、總結回顧
本文從Spring Security的架構和原理出發,描述了開源安全框架對於認證和鑒權模塊的設計思路和細節。並提供了系統內集成Spring Security的方法,結合RBAC通用許可權系統模型,討論了統一資源構建和統一鑒權的設計和實現。如果你也需要設計一個新的許可權系統,希望本文對你有所幫助。