1. 程式人生 > >SpringSecurity原理剖析與許可權系統設計

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通用許可權系統模型,討論了統一資源構建和統一鑑權的設計和實現。如果你也需要設計一個新的許可權系統,希望本文對你有所幫助。