Spring Security 實戰干貨:動態權限控制(下)實現



1. 前言

Spring Security 實戰干貨:內置 Filter 全解析 中提到的第 32Filter 不知道你是否有印象。它決定了訪問特定路徑應該具備的權限,訪問的用戶的角色,權限是什么?訪問的路徑需要什么樣的角色和權限? 它就是 FilterSecurityInterceptor ,正是我們需要的那個輪子。

2.FilterSecurityInterceptor

過濾器排行榜第 32 位!肩負對 http 接口權限認證的重要職責。我們來看它的過濾邏輯:

 	public void doFilter(ServletRequest request, ServletResponse response,
 			FilterChain chain) throws IOException, ServletException {
 		FilterInvocation fi = new FilterInvocation(request, response, chain);
 		invoke(fi);
 	}

初始化了一個 FilterInvocation 然后被 invoke 方法處理:

	public void invoke(FilterInvocation fi) throws IOException, ServletException {
		if ((fi.getRequest() != null)
				&& (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
				&& observeOncePerRequest) {
			// filter already applied to this request and user wants us to observe
			// once-per-request handling, so don't re-do security checking
			fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
		}
		else {
			// first time this request being called, so perform security checking
			if (fi.getRequest() != null && observeOncePerRequest) {
				fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
			}

			InterceptorStatusToken token = super.beforeInvocation(fi);

			try {
				fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
			}
			finally {
				super.finallyInvocation(token);
			}

			super.afterInvocation(token, null);
		}
	}

每一次請求被 Filter 過濾都會被打上標記 FILTER_APPLIED,沒有被打上標記的 走了父類的 beforeInvocation 方法然后再進入過濾器鏈,看上去是走了一個前置的處理。那么前置處理了什么呢?
首先會通過 this.obtainSecurityMetadataSource().getAttributes(Object object) 拿受保護對象(就是當前請求的 URI)所有的映射角色(ConfigAttribute 直接理解為角色的進一步抽象) 。然后使用訪問決策管理器 AccessDecisionManager 進行投票決策來確定是否放行。 我們來看一下這兩個接口。

安全攔截器和“安全對象”模型參考:

3. 元數據加載器

元數據加載器 FilterInvocationSecurityMetadataSourceFilterSecurityInterceptor 的屬性,UML 圖如下:

FilterInvocationSecurityMetadataSource 是一個標記接口,其抽象方法繼承自 SecurityMetadataSource``AopInfrastructureBean 。它的作用是來獲取我們上一篇文章所描述的資源角色元數據

  • Collection getAttributes(Object object) 根據提供的受保護對象的信息,其實就是 URI,獲取該 URI 配置的所有角色
  • Collection getAllConfigAttributes() 這個就是獲取全部角色
  • boolean supports(Class<?> clazz) 對特定的安全對象是否提供 ConfigAttribute 支持

3.1 自定義實現思路

所有的思路僅供參考,實際以你的業務為准!

Collection<ConfigAttribute> getAttributes(Object object) 方法的實現:肯定是獲取請求中的 URI 來和 所有的 資源配置中的 Ant Pattern 進行匹配以獲取對應的資源配置, 這里需要將資源查詢接口查詢的資源配置封裝為 AntPathRequestMatcher以方便進行 Ant Match
這里需要特別提一下如果你使用 Restful 風格,這里 增刪改查 將非常方便你來對資源的管控。參考的實現:

 @Bean
 public RequestMatcherCreator requestMatcherCreator() {
   return metaResources -> metaResources.stream()
           .map(metaResource -> new AntPathRequestMatcher(metaResource.getPattern(), metaResource.getMethod()))
           .collect(Collectors.toSet());
 }

HttpRequest 匹配到對應的資源配置后就能根據資源配置去取對應的角色集合。這些角色將交給訪問決策管理器 AccessDecisionManager 進行投票表決以決定是否放行。

4. 決策管理器

決策管理器 AccessDecisionManager用來投票決定是否放行請求。

  public interface AccessDecisionManager {
    // 決策 主要通過其持有的 AccessDecisionVoter 來進行投票決策
   	void decide(Authentication authentication, Object object,
   			Collection<ConfigAttribute> configAttributes) throws AccessDeniedException,
   			InsufficientAuthenticationException;
   // 以確定AccessDecisionManager是否可以處理傳遞的ConfigAttribute
   	boolean supports(ConfigAttribute attribute);
   //以確保配置的AccessDecisionManager支持安全攔截器將呈現的安全 object 類型。
   	boolean supports(Class<?> clazz);
   }

AccessDecisionManager 有三個默認實現:

  • AffirmativeBased 基於肯定的決策器。 用戶持有一個同意訪問的角色就能通過。
  • ConsensusBased 基於共識的決策器。 用戶持有同意的角色數量多於禁止的角色數。
  • UnanimousBased 基於一致的決策器。 用戶持有的所有角色都同意訪問才能放行。

投票決策模型參考:

4.1 自定義決策管理器

動態控制權限就需要我們實現自己的訪問決策器。我們上面說了默認有三個實現,這里我選擇基於肯定的決策器 AffirmativeBased,只要用戶持有一個持有一個角色包含想要訪問的資源就能訪問該資源。接下來就是投票器 AccessDecisionVoter 的定義了,其實我們可以選擇內置的

5. 決策投票器

決策投票器 AccessDecisionVoter 將安全配置屬性 ConfigAttribute 以特定的邏輯進行解析並基於特定的策略來進行投票,投贊成票時總票數 1 ,反對票總票數 -1 ,棄權時總票數 0 , 然后由 AccessDecisionManager 根據具體的計票策略來決定是否放行。

5.1 角色投票器

Spring Security 提供的最常用的投票器是角色投票器 RoleVoter,它將安全配置屬性 ConfigAttribute 視為簡單的角色名稱,並在用戶被分配了該角色時授予訪問權限。
如果任何 ConfigAttribute 以前綴 ROLE_ 開頭,它將投票。如果有一個 GrantedAuthority 返回一個字符串(通過 getAuthority() 方法)正好等於一個或多個從前綴 ROLE_ 開始的 ConfigAttributes,它將投票授予訪問權限。如果沒有任何以 ROLE_開頭的 ConfigAttributes匹配,則 RoleVoter 將投票拒絕訪問。如果沒有 ConfigAttribute 以 ROLE_為前綴,將棄權。
這正是我們想要的投票器。

5.2 角色分層投票器

通常要求應用程序中的特定角色應自動“包含”其他角色。例如,在具有 ROLE_ADMINROLE_USER 角色概念的應用中,您可能希望管理員能夠執行普通用戶可以執行的所有操作。你不得不進行各種復雜的邏輯嵌套來滿足這一需求。現在幸好有了 RoleHierarchyVoter 可以幫你減少這種負擔。
它由上面的 RoleVoter 派生,通過配置了一個 RoleHierarchy就可以實現 ROLE_ADMIN ⇒ ROLE_STAFF ⇒ ROLE_USER ⇒ ROLE_GUEST 這種層次包含結構,左邊的一定能訪問右邊可以訪問的資源。具體的配置規則為:角色從左到右、從高到低以 > 相連(注意兩個空格),以換行符 \n 為分割線。舉個例子

   ROLE_ADMIN > ROLE_STAFF
   ROLE_STAFF > ROLE_USER
   ROLE_USER > ROLE_GUEST

請注意動態配置中你需要自行實現角色分層的邏輯。DEMO 中並未對該風格進行實現。

6. 配置

配置需要兩個方面。

6.1 自定義組件的配置

我們需要將元數據加載器 和 訪問決策器注入 Spring IoC

 /** * 動態權限組件配置 * * @author Felordcn */
 @Configuration
 public class DynamicAccessControlConfiguration {
     /** * RequestMatcher 生成器 * @return RequestMatcher */
     @Bean
     public RequestMatcherCreator requestMatcherCreator() {
         return metaResources -> metaResources.stream()
                 .map(metaResource -> new AntPathRequestMatcher(metaResource.getPattern(), metaResource.getMethod()))
                 .collect(Collectors.toSet());
     }

     /** * 元數據加載器 * * @return dynamicFilterInvocationSecurityMetadataSource */
     @Bean
     public FilterInvocationSecurityMetadataSource dynamicFilterInvocationSecurityMetadataSource() {
         return new DynamicFilterInvocationSecurityMetadataSource();
     }

     /** * 角色投票器 * @return roleVoter */
     @Bean
     public RoleVoter roleVoter() {
         return new RoleVoter();
     }

     /** * 基於肯定的訪問決策器 * * @param decisionVoters AccessDecisionVoter類型的 Bean 會自動注入到 decisionVoters * @return affirmativeBased */
     @Bean
     public AccessDecisionManager affirmativeBased(List<AccessDecisionVoter<?>> decisionVoters) {
         return new AffirmativeBased(decisionVoters);
     }

 }

Spring SecurityJava Configuration 不會公開它配置的每個 object 的每個 property。這簡化了大多數用戶的配置。
雖然有充分的理由不直接公開每個 property,但用戶可能仍需要像本文一樣的取實現個性化需求。為了解決這個問題,Spring Security 引入了 ObjectPostProcessor 的概念,它可用於修改或替換 Java Configuration 創建的許多 Object 實例。 FilterSecurityInterceptor 的替換配置正是通過這種方式來進行:

@Configuration
@ConditionalOnClass(WebSecurityConfigurerAdapter.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
public class CustomSpringBootWebSecurityConfiguration {
    private static final String LOGIN_PROCESSING_URL = "/process";

    /** * Json login post processor json login post processor. * * @return the json login post processor */
    @Bean
    public JsonLoginPostProcessor jsonLoginPostProcessor() {
        return new JsonLoginPostProcessor();
    }

    /** * Pre login filter pre login filter. * * @param loginPostProcessors the login post processors * @return the pre login filter */
    @Bean
    public PreLoginFilter preLoginFilter(Collection<LoginPostProcessor> loginPostProcessors) {
        return new PreLoginFilter(LOGIN_PROCESSING_URL, loginPostProcessors);
    }

    /** * Jwt 認證過濾器. * * @param jwtTokenGenerator jwt 工具類 負責 生成 驗證 解析 * @param jwtTokenStorage jwt 緩存存儲接口 * @return the jwt authentication filter */
    @Bean
    public JwtAuthenticationFilter jwtAuthenticationFilter(JwtTokenGenerator jwtTokenGenerator, JwtTokenStorage jwtTokenStorage) {
        return new JwtAuthenticationFilter(jwtTokenGenerator, jwtTokenStorage);
    }

    /** * The type Default configurer adapter. */
    @Configuration
    @Order(SecurityProperties.BASIC_AUTH_ORDER)
    static class DefaultConfigurerAdapter extends WebSecurityConfigurerAdapter {

        @Autowired
        private JwtAuthenticationFilter jwtAuthenticationFilter;
        @Autowired
        private PreLoginFilter preLoginFilter;
        @Autowired
        private AuthenticationSuccessHandler authenticationSuccessHandler;
        @Autowired
        private AuthenticationFailureHandler authenticationFailureHandler;
        @Autowired
        private FilterInvocationSecurityMetadataSource filterInvocationSecurityMetadataSource;
        @Autowired
        private AccessDecisionManager accessDecisionManager;

        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            super.configure(auth);
        }

        @Override
        public void configure(WebSecurity web) {
            super.configure(web);
        }

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.csrf().disable()
                    .cors()
                    .and()
                    // session 生成策略用無狀態策略
                    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                    .and()
                    .exceptionHandling().accessDeniedHandler(new SimpleAccessDeniedHandler()).authenticationEntryPoint(new SimpleAuthenticationEntryPoint())
                    .and()
                    // 動態權限配置
                    .authorizeRequests().anyRequest().authenticated().withObjectPostProcessor(filterSecurityInterceptorObjectPostProcessor())
                    .and()
                    .addFilterBefore(preLoginFilter, UsernamePasswordAuthenticationFilter.class)
                    // jwt 必須配置於 UsernamePasswordAuthenticationFilter 之前
                    .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
                    // 登錄 成功后返回jwt token 失敗后返回 錯誤信息
                    .formLogin().loginProcessingUrl(LOGIN_PROCESSING_URL).successHandler(authenticationSuccessHandler).failureHandler(authenticationFailureHandler)
                    .and().logout().addLogoutHandler(new CustomLogoutHandler()).logoutSuccessHandler(new CustomLogoutSuccessHandler());

        }

        /** * 自定義 FilterSecurityInterceptor ObjectPostProcessor 以替換默認配置達到動態權限的目的 * * @return ObjectPostProcessor */
        private ObjectPostProcessor<FilterSecurityInterceptor> filterSecurityInterceptorObjectPostProcessor() {
            return new ObjectPostProcessor<FilterSecurityInterceptor>() {
                @Override
                public <O extends FilterSecurityInterceptor> O postProcess(O object) {
                    object.setAccessDecisionManager(accessDecisionManager);
                    object.setSecurityMetadataSource(filterInvocationSecurityMetadataSource);
                    return object;
                }
            };
        }

    }
}

然后你編寫一個 Controller 方法就將其在數據庫注冊為一個資源進行動態的訪問控制了。無須注解或者更詳細的 Java Config 配置

7. 總結

從最開始到現在一共 10 個 DEMO 。我們循序漸進地從如何學習 Spring Security 到目前實現了基於 RBAC、動態的權限資源訪問控制。如果你能堅持到現在那么已經能滿足了一些基本開發定制的需要。當然 Spring Security 還有很多局部的一些概念,我也會在以后抽時間進行講解。

8. roadmap

我先喘口氣休幾天。后續的一些 Spring Security 教程將圍繞目前更加流行的 OAuth2.0SSOOpenID 展開。敬請關注 felord.cn

老規矩, 關注 Felordcn 回復 day10 獲取 DEMO

關注公眾號:Felordcn獲取更多資訊

個人博客:https://felord.cn


免責聲明!

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



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