SpringSecurity原理剖析與權限系統設計


Spring Secutity和Apache Shiro是Java領域的兩大主流開源安全框架,也是權限系統設計的主要技術選型。本文主要介紹Spring Secutity的實現原理,並基於Spring Secutity設計基於RBAC的權限系統。

一、技術選型

為何把Spring Secutity作為權限系統的技術選型,主要考慮了以下幾個方面:

  1. 數據鑒權的能力:Spring Secutity支持數據鑒權,即細粒度權限控制。
  2. Spring生態基礎:Spring Secutity可以和Spring生態無縫集成。
  3. 多樣認證能力: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通用權限系統模型,討論了統一資源構建和統一鑒權的設計和實現。如果你也需要設計一個新的權限系統,希望本文對你有所幫助。


免責聲明!

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



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