SpringBoot整合SpringSecurity(八)動態權限

序言

SpringSecurity 進行的權限驗證,有時候可能並不太滿足我們的需求。有時候呢可能需要你自己去擴展達到一個對自己業務滿意的驗證,這時候怎麼辦呢?第一呢, 先不要百度,你要懂權限驗證的一個流程,不懂的話可以去看我之前的博客,第二呢,就是放開手按照自己假象的思路去實踐!

思路

我說一下我的需求,我想判斷那個角色擁有某些url的權限,這時候該怎麼進行擴展呢?

我的思路是對 自定義 AccessDecisionVoter 然後我們知道在 FilterSecurityInterceptor 裏是從 SecurityMetadataSource 中拿到的權限配置項,知道了這些後,就可以做出擴展。

  1. 定義一個自己的SecurityMetadataSource 使其從數據庫中查詢權限自己構建 ConfigAttribute
  2. 定義一個具有自己需求的AccessDecisionVoter
  3. 規定一個自己的AccessDecisionManager

實踐

SecurityMetadataSource

首先呢我們先定義一個自己的 SecurityMetadataSource ,然後每次調用的時候都去數據庫裏去查詢。

public class DynamicFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {


    private FilterInvocationSecurityMetadataSource superMetadataSource;


    public DynamicFilterInvocationSecurityMetadataSource(FilterInvocationSecurityMetadataSource expressionBasedFilterInvocationSecurityMetadataSource) {
        this.superMetadataSource = expressionBasedFilterInvocationSecurityMetadataSource;
        //TODO 在這裏去查詢你的數據庫
    }

    private final AntPathMatcher antPathMatcher = new AntPathMatcher();

    /**
     * 假設這就是從數據庫中查詢到的數據
     * 意思就是 ROLE_JAVA 的角色 才能訪問 /tt
     */
    private final Map<String, String> urlRoleMap = new HashMap<String, String>() {{
        put("/tt", "ROLE_JAVA");
    }};

    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        FilterInvocation fi = (FilterInvocation) object;
        String url = fi.getRequestUrl();
        for (Map.Entry<String, String> entry : urlRoleMap.entrySet()) {
            if (antPathMatcher.match(entry.getKey(), url)) {
                return SecurityConfig.createList(entry.getValue());
            }
        }
        //如果沒有匹配到就拿 咱們自定義的配置
        return superMetadataSource.getAttributes(object);
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }


    @Override
    public boolean supports(Class<?> clazz) {
        return FilterInvocation.class.isAssignableFrom(clazz);
    }
}

AccessDecisionVoter

緊接着我們定義一個自己的 AccessDecisionVoter 這一步的作用是對我們自己構造的 SecurityConfig.createList(entry.getValue()); 進行一個角色驗證。

public class MyDynamicVoter implements AccessDecisionVoter<Object> {
    /**
     * supports 方法說明這個投票器是否可以傳遞到下一個投票器
     * 可以支持傳遞,則返回true
     *
     * @param attribute
     * @return
     */
    @Override
    public boolean supports(ConfigAttribute attribute) {
        return true;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return true;
    }

    @Override
    public int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) {
        //如果沒有進行認證則永遠返回 -1
        if (authentication == null) {
            return ACCESS_DENIED;
        }
        int result = ACCESS_ABSTAIN;
        Collection<? extends GrantedAuthority> authorities = extractAuthorities(authentication);
        for (ConfigAttribute attribute : attributes) {
            if (attribute.getAttribute() == null) {
                continue;
            }
            if (this.supports(attribute)) {
                result = ACCESS_DENIED;
                for (GrantedAuthority authority : authorities) {
                    if (attribute.getAttribute().equals(authority.getAuthority())) {
                        return ACCESS_GRANTED;
                    }
                }
            }
        }
        return result;
    }

    private Collection<? extends GrantedAuthority> extractAuthorities(
            Authentication authentication) {
        return authentication.getAuthorities();
    }

}

AccessDecisionManager

最後呢我們可以選擇重寫或者使用 Spring Security 提供的驗證器 看你自己。這裏呢我還是使用 Spring Security提供的 AffirmativeBased

 @Bean
 public AccessDecisionManager accessDecisionManager() {
    List<AccessDecisionVoter<?>> decisionVoters = new ArrayList<>();
    decisionVoters.add(new AuthenticatedVoter());
    decisionVoters.add(new WebExpressionVoter());
    decisionVoters.add(new MyDynamicVoter());
    return new AffirmativeBased(decisionVoters);
 }

這裏順便把WebExpressionVoter 投票器加入,讓其驗證除我們自定義的權限配置。

加入配置項

最後呢把我們定義好的條條框框都加入到我們的配置鏈中

   protected void configure(HttpSecurity http) throws Exception {
        //http.httpBasic()  //httpBasic 登錄
        http.formLogin()
                .failureHandler(failureAuthenticationHandler) // 自定義登錄失敗處理
                .successHandler(successAuthenticationHandler) // 自定義登錄成功處理
                .and()
                .logout()
                .logoutUrl("/logout")
                .and()
                .formLogin()
                .loginPage("/login")
                .loginProcessingUrl("/authentication/form") // 自定義登錄路徑
                .and()
                .authorizeRequests()// 對請求授權
                // 自定義FilterInvocationSecurityMetadataSource
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O fsi) {
                        fsi.setSecurityMetadataSource(mySecurityMetadataSource(fsi.getSecurityMetadataSource()));
                        fsi.setAccessDecisionManager(accessDecisionManager());
                        return fsi;
                    }
                })
                .antMatchers("/login", "/authentication/require",
                        "/authentication/form").permitAll()
                .anyRequest()
                .authenticated().and().exceptionHandling().accessDeniedHandler(accessDeniedAuthenticationHandler)
                .and()
                .csrf().disable();// 禁用跨站攻擊
    }


@Bean
public DynamicFilterInvocationSecurityMetadataSource mySecurityMetadataSource(FilterInvocationSecurityMetadataSource filterInvocationSecurityMetadataSource) {
   return new DynamicFilterInvocationSecurityMetadataSource(filterInvocationSecurityMetadataSource);
}


@Bean
public AccessDecisionManager accessDecisionManager() {
   List<AccessDecisionVoter<?>> decisionVoters = new ArrayList<>();
   decisionVoters.add(new AuthenticatedVoter());
   decisionVoters.add(new WebExpressionVoter());
   decisionVoters.add(new MyDynamicVoter());
   return new AffirmativeBased(decisionVoters);
}

然後定義一個controller

@RestController
public class PermissionController {


    @RequestMapping("/tt")
    public String tt() {
        System.out.println("1");
        return "tt";
    }
}

讓登錄用戶擁有ROLE_JAVA 角色的時候,即可訪問/tt

當然這只是我的一個需求,具體的怎麼擴展決定在你自己,只要懂了大概的流程,既可以對Spring Security的權限校驗做出更好的擴展。

順便說下我們這個自定義權限並不會影響到權限註解,權限註解是進行了切面處理所以還會在走一遍權限認證器和投票的。

本博文是基於springboot2.x 和security 5 如果有什麼不對的請在下方留言。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章