[13] FilterSecurityInterceptor

FilterSecurityInterceptor

簡介

Spring Security對於權限的控制有2種方式,1.通過ExpressionInterceptUrlRegistry進行配置,2.通過註解和切面的方式。FilterSecurityInterceptor是針對於第一種方式權限配置的控制機制,在項目或服務啓動時,Spring Security會把ExpressionInterceptUrlRegistry中配置的權限控制規則轉換成SecurityMetadataSource,客戶端或者瀏覽器發起請求時,會經過FilterSecurityInterceptor過濾器,過濾器根據SecurityMetadataSource進行權限校驗。第二種方式是通過且AOP切面實現的,與FilterSecurityInterceptor無關,因爲同爲權限控制,所以這裏附帶分析一下。

代碼分析

配置鑑權

步驟1:配置鑑權

筆者項目主要用與配置swagger以及feign api白名單,考慮到白名單變更的頻率也比較高,進行硬編碼也不太合適。於是寫了個SpringBootAutoConfiguration,將白名單配置到配置文件中修改起來也比較方便,筆者這裏只是配置白名單URL進行放行,用了permitAll(),還有其他權限校驗方法,比如hasRole(),hasAnyRole()等等,可以根據自己項目情況進行硬編碼設置,配置如下:

carp:
  security:
    oauth2:
      resource:
        enable: true
        whitelabel-clients: carp
        whitelabel-urls:
          #內部接口加密但不攔截
          - /feign/**
          #swagger接口
          - /doc/api/**
          - /swagger-ui.html
          - /swagger-ui.html**
          - /webjars/**
          - /swagger-resources/**
          - /v2/api-docs/**
public abstract class AbstractResourceServerConfig extends ResourceServerConfigurerAdapter {
   	//注入配置文件中配置的屬性
    @Resource
    protected CarpAuthResourceProperties carpAuthResourceProperties;
    
    @Override
    public void configure(HttpSecurity http) throws Exception {
       	ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http
            .authorizeRequests();
        //是否開啓資源權限校驗
        if (!carpAuthResourceProperties.getEnable()) {
            registry.anyRequest().permitAll();
            return;
        }
        if (carpAuthResourceProperties != null && carpAuthResourceProperties.getWhitelabelUrls().size() > 0) {
            //白名單URL放行
            carpAuthResourceProperties.getWhitelabelUrls().forEach(s -> registry.antMatchers(s).permitAll());
        }
        registry.anyRequest().authenticated();
    }
}

步驟2:鑑權攔截處理

FilterSecurityInterceptor的鑑權邏輯集中在父類AbstractSecurityInterceptor#beforeInvocation()方法中,對於FilterSecurityInterceptor而言,是針對請求url的權限攔截和鑑權,所以Collection attributes取到也是針對請求路徑的EL表達式鑑權配置,因此accessDecisionManager.decide()這裏進行的投票也是基於請求路徑進行規則匹配來進行投票,beforeInvocation()的大致邏輯,代碼如下:

//權限規則全局變量
private FilterInvocationSecurityMetadataSource securityMetadataSource;
//通過Set注入配置的權限過濾規則
public void setSecurityMetadataSource(FilterInvocationSecurityMetadataSource newSource) {
    this.securityMetadataSource = newSource;
}

public void doFilter(ServletRequest request, ServletResponse response,
        FilterChain chain) throws IOException, ServletException {
    //chain是Servlet原生容器的過濾鏈,並非Spring Security中的過濾鏈
    FilterInvocation fi = new FilterInvocation(request, response, chain);
    //執行過濾的核心規則
    invoke(fi);
}

public void invoke(FilterInvocation fi) throws IOException, ServletException {
    if ((fi.getRequest() != null)
            && (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
            && observeOncePerRequest) {
        //同一次請求中,過濾器已經執行過,直接進入原生過濾鏈
        //FilterSecurityInterceptor是Spring Security過濾鏈的最後一個,所以要進入Servlet原生的過濾前
        fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
    }
    else {
        if (fi.getRequest() != null && observeOncePerRequest) {
            //打一個過濾器已執行標識
            fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
        }
		//原生servlet過濾鏈執行後進行權限規則校驗
        InterceptorStatusToken token = super.beforeInvocation(fi);

        try {
            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
        }
        finally {
            //還原SpringSecurity上下文
            super.finallyInvocation(token);
        }
		//後置處理,不做深入研究
        super.afterInvocation(token, null);
    }
}

protected InterceptorStatusToken beforeInvocation(Object object) {
    Assert.notNull(object, "Object was null");
    final boolean debug = logger.isDebugEnabled();

    //對於FilterSecurityInterceptor SecureObjectClass 是FilterInvocation.class,判斷是否是合法的Invocation
    if (!getSecureObjectClass().isAssignableFrom(object.getClass())) {
        throw new IllegalArgumentException(
                "Security invocation attempted for object "
                        + object.getClass().getName()
                        + " but AbstractSecurityInterceptor only configured to support secure objects of type: "
                        + getSecureObjectClass());
    }

    Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
            .getAttributes(object);
    //沒有配置任何權限過濾規則時,是否允許訪問接口
    if (attributes == null || attributes.isEmpty()) {
        if (rejectPublicInvocations) {
            throw new IllegalArgumentException(
                    "Secure object invocation "
                            + object
                            + " was denied as public invocations are not allowed via this interceptor. "
                            + "This indicates a configuration error because the "
                            + "rejectPublicInvocations property is set to 'true'");
        }

        publishEvent(new PublicInvocationEvent(object));

        return null; // no further work post-invocation
    }

    if (SecurityContextHolder.getContext().getAuthentication() == null) {
        //身份認證信息爲空,發送對應事件,並拋出AuthenticationCredentialsNotFoundException異常
        credentialsNotFound(messages.getMessage(
                "AbstractSecurityInterceptor.authenticationNotFound",
                "An Authentication object was not found in the SecurityContext"),
                object, attributes);
    }

    //如果必要,則進行身份認證
    Authentication authenticated = authenticateIfRequired();

    // Attempt authorization
    try {
        //訪問投票決定是否可以訪問
        this.accessDecisionManager.decide(authenticated, object, attributes);
    }
    catch (AccessDeniedException accessDeniedException) {
        publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
                accessDeniedException));

        throw accessDeniedException;
    }

    //發送授權事件
    if (publishAuthorizationSuccess) {
        publishEvent(new AuthorizedEvent(object, attributes, authenticated));
    }

    // Attempt to run as a different user
    // 當attributes有屬性值以RUN_AS_XXX開頭時,buildRunAs會解析爲ROLE_XXX,重新根據權限生成新的身份認證信息
    Authentication runAs = this.runAsManager.buildRunAs(authenticated, object,
            attributes);

    if (runAs == null) {
        //包裝成一個複合對象
        return new InterceptorStatusToken(SecurityContextHolder.getContext(), false,
                attributes, object);
    }
    else {
        //一下4行代碼,1:取出舊的上下文 2:創建一個空的上下文 3:將新生成的RunAs身份認證信息放到信息的上下文中 4:將舊的上下文放到複合對象
        SecurityContext origCtx = SecurityContextHolder.getContext();
        SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());
        SecurityContextHolder.getContext().setAuthentication(runAs);

        //包裝成一個複合對象,由於上下被替換,當前放入的是舊上下文,故contextHolderRefreshRequired這裏爲true
        return new InterceptorStatusToken(origCtx, true, attributes, object);
    }
}

this.obtainSecurityMetadataSource().getAttributes(object)這行代碼直觀可能會疑惑,這裏就是把Request請求的取出,找出與請求路徑能夠匹配的ConfigAttribute,通過Debug,觀察變量值可能更直觀,代碼和截圖如下:

public Collection<ConfigAttribute> getAttributes(Object object) {
    final HttpServletRequest request = ((FilterInvocation) object).getRequest();
    for (Map.Entry<RequestMatcher, Collection<ConfigAttribute>> entry : requestMap
            .entrySet()) {
        if (entry.getKey().matches(request)) {
            return entry.getValue();
        }
    }
    return null;
}

image.png
Authentication authenticated = authenticateIfRequired()是取出上下文中的身份認證信息,判斷是否進行必要驗證,代碼解析註釋到代碼當中了,如下:

private Authentication authenticateIfRequired() {
    Authentication authentication = SecurityContextHolder.getContext()
            .getAuthentication();
    //一般 1:未登錄的用戶,匿名過濾器會生成一個匿名Token 2:登錄過的用戶會有一個Bearer Token
    //在筆者的配置中,尚未執行到authenticationManager.authenticate(authentication);就return了
    if (authentication.isAuthenticated() && !alwaysReauthenticate) {
        return authentication;
    }

    authentication = authenticationManager.authenticate(authentication);

    SecurityContextHolder.getContext().setAuthentication(authentication);

    return authentication;
}

緊接着就是非常重要的accessDecisionManager.decide()方法,這裏的accessDecisionManager是一個AffirmativeBased實例,投票有3種結果ACCESS_GRANTED(授權)、ACCESS_ABSTAIN(棄權)、ACCESS_DENIED(拒絕),投票規則爲:所有的投票器進行一輪投票,一旦有一票通過則授權通過,棄權票作廢,全部拒絕則授權失敗。對於FilterSecurityInterceptor,AffirmativeBased持有的是一個WebExpressionVoter關鍵代碼如下:

public void decide(Authentication authentication, Object object,
        Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
    int deny = 0;

    for (AccessDecisionVoter voter : getDecisionVoters()) {
        int result = voter.vote(authentication, object, configAttributes);

        switch (result) {
        //一旦有一個授權則授權成功
        case AccessDecisionVoter.ACCESS_GRANTED:
            return;

        case AccessDecisionVoter.ACCESS_DENIED:
            deny++;

            break;

        default:
            break;
        }
    }

    if (deny > 0) {
        throw new AccessDeniedException(messages.getMessage(
                "AbstractAccessDecisionManager.accessDenied", "Access is denied"));
    }

    //To get this far, every AccessDecisionVoter abstained
    //deny=0情況可能有:1. 沒有投票器 2. 所有投票器棄權
    //這裏就是判斷是否允許全部棄權的
    checkAllowIfAllAbstainDecisions();
}

image.png
那麼decisionVoters是什麼時候注入的?我們在AbstractAccessDecisionManager構造方法方法上打個短斷點,執行堆棧和創建decisionManager的代碼如下:
image.png

@Override
@SuppressWarnings("rawtypes")
List<AccessDecisionVoter<?>> getDecisionVoters(H http) {
    List<AccessDecisionVoter<?>> decisionVoters = new ArrayList<>();
    WebExpressionVoter expressionVoter = new WebExpressionVoter();
    expressionVoter.setExpressionHandler(getExpressionHandler(http));
    decisionVoters.add(expressionVoter);
    return decisionVoters;
}

至於RunAsManager,代碼中也做簡要的註釋,筆者對這部分代碼還沒有較深入的瞭解和實踐,不作過多說明。
FilterSecurityInterceptor是Spring Security過濾器中最後一個,執行beforeInvocation()後就要執行servlet中的過濾器了,截圖比較直觀,如下:
image.png
finallyInvocation()代碼比較簡單,當在beforeInvocation()中RunAsManager試圖模擬另外一個用戶身份創建新的身份認證信息成功時,contextHolderRefreshRequired=true,在執行finallyInvocation()就能進入if分支,將原來的認證信息給還原,代碼如下:

protected void finallyInvocation(InterceptorStatusToken token) {
    //上下文被RunAsManager替換過,則進行恢復
    if (token != null && token.isContextHolderRefreshRequired()) {
        if (logger.isDebugEnabled()) {
            logger.debug("Reverting to original Authentication: "
                    + token.getSecurityContext().getAuthentication());
        }

        SecurityContextHolder.setContext(token.getSecurityContext());
    }
}

afterInvocation()查了一些資料,是通過afterInvocationManager改變返回值的,但是有一點疑問,super.afterInvocation(token, null),這裏默認傳入了一個null,所以在invoke()中沒有任何地方引用,所以有點讓人費解,筆者大致看了一下afterInvocationManager的代碼,沒有深入研究,猜測afterInvocation()傳入的null,應該是一個默認值,對於FilterSecurityInterceptor返回值void,可能無需更改,但對於MethodSecurityInterceptor是Aop實現的,每個方法都有可能有返回值,估計是針對MethodSecurityInterceptor實現的。這裏不做深入研究了。

2. 註解式鑑權

步驟1:配置

註解式鑑權是Aop切面的機制,FilterSecurityInterceptor對其不能進行控制,因此二者關係並不大。其主要的實現類爲AspectJMethodSecurityInterceptor->MethodSecurityInterceptor->AbstractSecurityInterceptor,按箭頭方向依次繼承,使用註解式鑑權,開啓配置以及實例如下:

@EnableGlobalMethodSecurity(prePostEnabled = true)
@Configuration
public class WebSecurityConfig {
}
@PreAuthorize("hasRole('ADMIN')")
@ApiOperation("異步獲取上下文用戶信息")
@GetMapping("/security/async")
public R<Object> asyncGetContextUser() throws Exception {
}

@PreAuthorize("hasAnyRole('ROOT','ADMIN')")
@ApiOperation("獲取上下文用戶信息")
@GetMapping("/security/sync")
public R<Object> getContextUser() {
}

步驟2:註解使用方式

securedEnabled

@EnableGlobalMethodSecurity(securedEnabled=true) 開啓@Secured 註解過濾權限

jsr250Enabled

@EnableGlobalMethodSecurity(jsr250Enabled=true)開啓@RolesAllowed 註解過濾權限

prePostEnabled

@PreAuthorize 在方法調用之前,基於表達式的計算結果來限制對方法的訪問
@PostAuthorize 允許方法調用,但是如果表達式計算結果爲false,將拋出一個安全性異常
@PostFilter 允許方法調用,但必須按照表達式來過濾方法的結果
@PreFilter 允許方法調用,但必須在進入方法之前過濾輸入值

接下來我們分析MethodSecurityInterceptor的相關代碼,代碼如下:

public Object invoke(MethodInvocation mi) throws Throwable {
    InterceptorStatusToken token = super.beforeInvocation(mi);

    Object result;
    try {
        result = mi.proceed();
    }
    finally {
        super.finallyInvocation(token);
    }
    //可以在此處對Aop的返回結果進行修改
    return super.afterInvocation(token, result);
}

步驟3:源碼分析

MethodSecurityInterceptor實現方式不同,beforeInvocation()中,obtainSecurityMetadataSource().getAttributes(object)這行代代碼與FilterSecurityInterceptor進行的操作也很不一樣,MethodSecurityInterceptor是通過反射,取到方法上的註解,然後包裝成ConfigAttribute,截圖和代碼如下:
image.png

public final Collection<ConfigAttribute> getAttributes(Object object) {
    if (object instanceof MethodInvocation) {
        MethodInvocation mi = (MethodInvocation) object;
        Object target = mi.getThis();
        Class<?> targetClass = null;

        if (target != null) {
            targetClass = target instanceof Class<?> ? (Class<?>) target
                    : AopProxyUtils.ultimateTargetClass(target);
        }
        Collection<ConfigAttribute> attrs = getAttributes(mi.getMethod(), targetClass);
        if (attrs != null && !attrs.isEmpty()) {
            return attrs;
        }
        if (target != null && !(target instanceof Class<?>)) {
            attrs = getAttributes(mi.getMethod(), target.getClass());
        }
        return attrs;
    }

同樣,this.accessDecisionManager.decide(authenticated, object, attributes);這行代碼也很不一樣,accessDecisionManager包含了三個投票器,如下:

  • PreInvocationAuthorizationAdviceVoter

該投票器通過解析傳入attibutes,根據SpEL表達式判斷用戶的權限是否滿足條件,進行投票。

  • RoleVoter

該投票器先判斷ConfigAttribute的屬性值是否以ROLE_前綴開頭,如果滿足條件,則進行權限比對,比對成功則投票通過。但是PreInvocationAttribute的getAttribute()始終返回null,因此PreInvocationAttribute不可能認證通過。

  • AuthenticatedVoter

該投票器先判斷ConfigAttribute的屬性值是IS_AUTHENTICATED_FULLY、IS_AUTHENTICATED_REMEMBERED、IS_AUTHENTICATED_ANONYMOUSLY中哪一種認證方式,匹配到其中一種,則採用對應的方式進行認證,匹配不到則投棄權票。
image.png

這3種投票器是什麼時候注入的?通過debug,我們發現是在GlobalMethodSecurityConfiguration#accessDecisionManager()硬編碼寫入的,調用堆棧和代碼如下:
image.png

protected AccessDecisionManager accessDecisionManager() {
    List<AccessDecisionVoter<?>> decisionVoters = new ArrayList<>();
    if (prePostEnabled()) {
        ExpressionBasedPreInvocationAdvice expressionAdvice =
                new ExpressionBasedPreInvocationAdvice();
        expressionAdvice.setExpressionHandler(getExpressionHandler());
        decisionVoters
                .add(new PreInvocationAuthorizationAdviceVoter(expressionAdvice));
    }
    if (jsr250Enabled()) {
        decisionVoters.add(new Jsr250Voter());
    }
    RoleVoter roleVoter = new RoleVoter();
    GrantedAuthorityDefaults grantedAuthorityDefaults =
            getSingleBeanOrNull(GrantedAuthorityDefaults.class);
    if (grantedAuthorityDefaults != null) {
        roleVoter.setRolePrefix(grantedAuthorityDefaults.getRolePrefix());
    }
    decisionVoters.add(roleVoter);
    decisionVoters.add(new AuthenticatedVoter());
    return new AffirmativeBased(decisionVoters);
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章