(五)Spring Security基於數據庫的權限授權


我們接着上一章(四)Spring Security基於數據庫的用戶認證,進行開發

一:重寫並實現了基於數據庫的權限數據源

/**
 * 權限資源
 * FilterInvocationSecurityMetadataSource的默認實現是
 * DefaultFilterInvocationSecurityMetadataSource
 */
@Service
public class CustomInvocationSecurityMetadataSourceService implements FilterInvocationSecurityMetadataSource {

    private static final Logger log = LoggerFactory.getLogger(CustomInvocationSecurityMetadataSourceService.class);

    private final PermissionRepository permissionRepository;
    /* key 是url+method ,value 是對應url資源的角色列表 */
    private  Map<RequestMatcher, Collection<ConfigAttribute>> requestMap = new LinkedHashMap<>();

    @Autowired
    public CustomInvocationSecurityMetadataSourceService(PermissionRepository permissionRepository) {
        this.permissionRepository = permissionRepository;
    }

    /**
     *  注意:
     *  @PostConstruct 用於在依賴關係注入完成之後需要執行的方法,以執行任何初始化。
     *  此方法必須在將類放入服務之前調用,且只執行一次。
     */
    @PostConstruct
    public void init(){
        log.info("[自定義權限資源數據源]:{}","初始化權限資源");
        List<Permission> permissions = permissionRepository.findAll();
        permissions.forEach(item->{
            Set<String> roleNames = item.getRoleNames();
            List<ConfigAttribute> configAttributes = new ArrayList<>();
            for (String roleName : roleNames) {
                configAttributes.add(new SecurityConfig(roleName));
            }
            requestMap.put(new AntPathRequestMatcher(item.getUrl()),configAttributes);
        });
        System.out.println(requestMap.toString());
    }


    /**
     * getAttributes方法返回本次訪問需要的權限,可以有多個權限。
     * 在上面的實現中如果沒有匹配的url直接返回null,
     * 也就是沒有配置權限的url默認都爲白名單,想要換成默認是黑名單隻要修改這裏即可。
     *
     * 訪問配置屬性(ConfigAttribute)用於給定安全對象(通過的驗證)
     *
     * @param object 安全的對象
     * @return 用於傳入的安全對象的屬性。 如果沒有適用的屬性,則應返回空集合。
     * @throws IllegalArgumentException 如果傳遞的對象不是SecurityDatasource實現支持的類型,則拋出異常
     */
    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        log.info("[自定義權限資源數據源]:{}","獲取本次訪問需要的權限");
        if(requestMap.isEmpty()){
            init();
        }
        final HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();
        for (Map.Entry<RequestMatcher, Collection<ConfigAttribute>> entry : requestMap.entrySet()) {
            if (entry.getKey().matches(request)) {
                log.info("[自定義權限資源數據源]:當前路徑[{}]需要的資源權限:[{}] ==> 觸發鑑權決策管理器",entry.getKey(),entry.getValue().toString());
              return entry.getValue();
            }
        }
       log.info("[自定義權限資源數據源]:{}==> {}","白名單路徑",request.getRequestURI());
        return null;
    }


    /**
     *
     *
     * getAllConfigAttributes方法如果返回了所有定義的權限資源,
     * Spring Security會在啓動時校驗每個ConfigAttribute是否配置正確,不需要校驗直接返回null。
     *
     *
     * 如果可用,則返回由實現類定義的所有ConfigAttribute。
     *
     * AbstractSecurityInterceptor使用它對針對它ConfigAttribute的每個配置屬性執行啓動時驗證。
     *
     * @return ConfigAttribute,如果沒有適用的,就返回null
     */
    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        Set<ConfigAttribute> allAttributes = new HashSet<>();
        for (Map.Entry<RequestMatcher, Collection<ConfigAttribute>> entry : requestMap.entrySet()) {
            allAttributes.addAll(entry.getValue());
        }
        log.info("[自定義權限資源數據源]:獲取所有的角色==> {}",allAttributes.toString());
        return allAttributes;
    }

    /**
     * AbstractSecurityInterceptor 調用
     * supports方法返回類對象是否支持校驗,web項目一般使用FilterInvocation來判斷,或者直接返回true。
     *
     * @param clazz 正在查詢的類
     * @return 如果實現可以處理指定的類,則爲true
     */
    @Override
    public boolean supports(Class<?> clazz) {
        return FilterInvocation.class.isAssignableFrom(clazz);
    }
}

二:重寫權限決策

根據URL資源權限和用戶角色,進行鑑權

@Service
public class CustomAccessDecisionManager implements AccessDecisionManager {

    private static final Logger log = LoggerFactory.getLogger(CustomAccessDecisionManager.class);

    /**
     * 權限鑑定
     *
     * @param authentication   from SecurityContextHolder.getContext() => userDetails.getAuthorities()
     * @param object           就是FilterInvocation對象,可以得到request等web資源。
     * @param configAttributes from MetaDataSource.getAttributes(),已經被框架做了非空判斷
     * @throws AccessDeniedException   如果由於身份驗證不具有所需的權限或ACL特權而拒絕訪問
     * @throws InsufficientAuthenticationException 如果由於身份驗證沒有提供足夠的信任級別而拒絕訪問
     */
    @Override
    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {

        log.info("****************************************權限鑑定********************************************");
        /*FilterInvocation filterInvocation = (FilterInvocation) object; // object 是一個URL
        log.info("[當前路徑[{}]需要的資源權限]:{}",filterInvocation.getRequestUrl(),configAttributes);*/
        log.info("[登錄用戶[{}]權限]:{}",authentication.getName(),authentication.getAuthorities());

        if(configAttributes == null){
            return;
        }

        for (ConfigAttribute configAttribute : configAttributes) {
            /* 資源的權限 */
            String attribute = configAttribute.getAttribute();
            /* 用戶的權限 */
            for (GrantedAuthority authority : authentication.getAuthorities()) { // 當前用戶的權限
                if(authority.getAuthority().trim().equals("ROLE_ANONYMOUS"))return;
                log.info("[資源角色==用戶角色] ? {} == {}", attribute.trim(), authority.getAuthority().trim());
                if (attribute.trim().equals(authority.getAuthority().trim())) {
                    log.info("[鑑權決策管理器]:登錄用戶[{}]權限匹配",authentication.getName());
                    return;
                }
            }
        }
        log.info("[鑑權決策管理器]:登錄用戶[{}]權限不足",authentication.getName());
        throw new AccessDeniedException("權限不足");
    }

    /**
     * AbstractSecurityInterceptor 調用,遍歷ConfigAttribute集合,篩選出不支持的attribute
     *
     * @param attribute a configuration attribute that has been configured against the
     *                  <code>AbstractSecurityInterceptor</code>
     * @return true if this <code>AccessDecisionManager</code> can support the passed
     * configuration attribute
     */
    @Override
    public boolean supports(ConfigAttribute attribute) {
        return true;
    }

    /**
     * AbstractSecurityInterceptor 調用,驗證AccessDecisionManager是否支持這個安全對象的類型。
     * supports(Class)方法被安全攔截器實現調用,
     * 包含安全攔截器將顯示的AccessDecisionManager支持安全對象的類型。
     *
     * @param clazz the class that is being queried
     * @return <code>true</code> if the implementation can process the indicated class
     */
    @Override
    public boolean supports(Class<?> clazz) {
        return true;
    }
}

三:實現AbstractSecurityInterceptor

默認實現是FilterSecurityInterceptor,進行訪問資源時,會通過這個攔截器攔截
訪問資源(即授權管理),訪問url時,會通過AbstractSecurityInterceptor攔截器攔截, 其中會調用FilterInvocationSecurityMetadataSource的方法來獲取被攔截url所需的全部權限,在調用授權管理器AccessDecisionManager,這個授權管理器會通過spring的全局緩存SecurityContextHolder獲取用戶的權限信息, 還會獲取被攔截的url和被攔截url所需的全部權限,然後根據所配的策略(有:一票決定,一票否定,少數服從多數等), 如果權限足夠,則返回,權限不夠則報錯並調用權限不足頁面

@Component
public class CustomFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {

    private static final Logger log = LoggerFactory.getLogger(CustomFilterSecurityInterceptor.class);

    private static final String FILTER_APPLIED = "__spring_security_CustomFilterSecurityInterceptor_filterApplied";


    private final CustomInvocationSecurityMetadataSourceService customInvocationSecurityMetadataSourceService;
    private final CustomAccessDecisionManager customAccessDecisionManager;

    @Autowired
    public CustomFilterSecurityInterceptor(CustomInvocationSecurityMetadataSourceService customInvocationSecurityMetadataSourceService, CustomAccessDecisionManager customAccessDecisionManager) {
        this.customInvocationSecurityMetadataSourceService = customInvocationSecurityMetadataSourceService;
        this.customAccessDecisionManager = customAccessDecisionManager;
    }

    /**
     *  初始化時將定義的DecisionManager,注入到父類AbstractSecurityInterceptor中
     *  注意:
     *  @PostConstruct 用於在依賴關係注入完成之後需要執行的方法,以執行任何初始化。
     *  此方法必須在將類放入服務之前調用,且只執行一次。
     */
    @PostConstruct
    public void init(){
        log.info("設置==========================================鑑權決策管理器");
        super.setAccessDecisionManager(customAccessDecisionManager);
    }

    /**
     * 向父類提供要處理的安全對象類型,因爲父親被調用的方法參數類型大多是Object,框架需要保證傳遞進去的安全對象類型相同
     *
     * @return 子類爲其提供服務的安全對象的類型
     */
    @Override
    public Class<?> getSecureObjectClass() {
        return FilterInvocation.class;
    }

    /**
     * 獲取到自定義MetadataSource的方法
     *
     * 啓動時會有3次調用
     * 第一次調用:{@link AbstractSecurityInterceptor#afterPropertiesSet()} 135行
     * 第二次調用:{@link AbstractSecurityInterceptor#afterPropertiesSet()} 137行
     * 第三次調用:{@link AbstractSecurityInterceptor#afterPropertiesSet()} 156行
     *
     * 登錄時調用
     * 調用:{@link AbstractSecurityInterceptor#beforeInvocation(Object)} 196行
     *
     * @return  權限資源映射的數據源
     */
    @Override
    public SecurityMetadataSource obtainSecurityMetadataSource() {
        return this.customInvocationSecurityMetadataSourceService;
    }

    /**
     *
     * 由Web容器調用,以向filter指示正在將其放入服務中。
     * servlet容器在實例化filter後,只調用一個init方法。
     * 在要求filter執行任何過濾之前,init方法必須成功完成。
     * 如果init方法滿足以下條件之一,則web容器無法將篩選器放入服務:拋出 servletException
     * 在web容器定義的時間內不返回
     * 默認實現時NO-OP
     * @param filterConfig 與正在初始化的filter實例關聯的配置信息
     * @throws ServletException 如果實例化失敗
     */
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        log.info("filer==========================================init");
    }

    /**
     * 每當request/response對由於客戶端請求鏈末端的資源而通過鏈時,容器調用過濾器的doFilter方法。
     * 傳入此方法的filter chain 允許Filter傳遞請求並響應鏈中的下一個實體。
     *
     * 此方法的典型實現將遵循以下模式:
     * 1.檢查請求
     * 2.也可以使用自定義實現包裝請求對象,輸入filter的內容或頭
     * 3.(可選)使用自定義實現包裝響應對象,以Filter 內容或頭進行輸出過濾
     * 4.使用FilterChain對象的chain.doFilter()調用鏈中的下一個實體
     * 5.在調用FilterChain中的下一個實體後,直接在響應上設置頭。
     * @param request  要處理的請求
     * @param response 與請求關聯的響應
     * @param chain   提供對鏈中下一個Filter的訪問,以便此Filter將請求和響應傳遞給以進行進一步處理
     * @throws IOException      如果在此篩選器處理請求期間發生I/O錯誤
     * @throws ServletException 如果由於其他原因處理失敗
     */
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        log.info("[自定義過濾器]:{}","CustomFilterSecurityInterceptor.doFilter()");
        FilterInvocation filterInvocation = new FilterInvocation(request, response, chain);
        invoke(filterInvocation);
    }

    /**
     * 由Web容器調用,以向filter指示它正在退出服務。
     * 只有當filter的doFilter方法中的所有線程都退出或超出時間間後,才調用此方法。
     * 在web容器調用此方法之後,它將不再在此filter的實例上調用doFilter方法。
     * 此方法使filter有機會clean正在保留的任何資源(例如內存、文件句柄、線程)
     * 並確保任何持久狀態與filter在內存中的當前狀態同步。
     *
     * 默認實現時NO-OP
     */
    @Override
    public void destroy() {
        log.info("filer==========================================destroy");
    }


    private void invoke(FilterInvocation fi) throws IOException, ServletException {
        if ((fi.getRequest() != null) && (fi.getRequest().getAttribute(FILTER_APPLIED) != null)) {
            // 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 ) {
                fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
            }

            /* 調用父類的beforeInvocation ==> accessDecisionManager.decide(..) */
            InterceptorStatusToken token = super.beforeInvocation(fi);

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

注意:在spring容器託管的AbstractSecurityInterceptor的bean,都會自動加入到servlet的filter chain,不用在websecurityconfig配置
示例
在這裏插入圖片描述
當權限不足時,會自動跳轉到403
在這裏插入圖片描述

四:項目地址

(五)Spring Security基於數據庫的權限授權

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