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

1. 前言

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

2.FilterSecurityInterceptor

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

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

初始化了一個 FilterInvocation 而後被 invoke 方法處理:web

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 進行投票決策來肯定是否放行。 咱們來看一下這兩個接口。spring

安全攔截器和「安全對象」模型參考:數據庫

3. 元數據加載器

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

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 風格,這裏 增刪改查 將很是方便你來對資源的管控。參考的實現:bash

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

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

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

相關文章
相關標籤/搜索