Spring Security小教程 Vol 8. AccessDecisionVoter組件介紹

第八期 AccessDecisionVoter組件介紹

這一期主要我們將介紹訪問控制三劍客負(fù)責(zé)對(duì)授權(quán)規(guī)則做角色的組件——AccessDecisionVoter接口。以及對(duì)Spring Security默認(rèn)提供的幾個(gè)基礎(chǔ)AccessDecisionVoter實(shí)現(xiàn)類做一個(gè)詳細(xì)的說明,最后我們將會(huì)客制化一個(gè)基于時(shí)間的AccessDecisionVoter實(shí)現(xiàn)用于實(shí)戰(zhàn)說明。

  • AccessDecisionVoter接口說明
  • Spring Security的AccessDecisionVoter
  • 客制化實(shí)例:基于時(shí)間的AccessDecisionVoter

一、AccessDecisionVoter接口說明

AccessDecisionVoter接口說明

AccessDecisionVoter主要的職責(zé)就是對(duì)它所對(duì)應(yīng)的訪問規(guī)則作出判斷,當(dāng)前的訪問規(guī)則是否可以得到授權(quán)。
AccessDecisionVoter接口的主要方法其實(shí)與之前的AuthenticationProvider非常的相似。

    boolean supports(ConfigAttribute attribute);

    int vote(Authentication authentication, S object,
            Collection<ConfigAttribute> attributes);
  • supports方法用于判斷對(duì)于當(dāng)前ConfigAttribute訪問規(guī)則是否支持;
  • 如果支持的情況下,vote方法對(duì)其進(jìn)行判斷投票返回對(duì)應(yīng)的授權(quán)結(jié)果。
    最終的授權(quán)結(jié)果一共有三種,分別是同意、棄權(quán)和反對(duì)。說實(shí)話這個(gè)規(guī)則和聯(lián)合國(guó)安理會(huì)投票差不多性質(zhì)。當(dāng)前一個(gè)訪問可能存在多個(gè)規(guī)則的情況下,每一個(gè)AccessDecisionVoter投出自己的那一票,最終的投票結(jié)果是還是要看當(dāng)前的投票規(guī)則,比如是超過1/3還是要過半數(shù)。而投票規(guī)則的判斷則是被放置了在了AccessDecisionManager進(jìn)行完成。
    int ACCESS_GRANTED = 1;
    int ACCESS_ABSTAIN = 0;
    int ACCESS_DENIED = -1;

二、 Spring Security的AccessDecisionVoter

通過上面對(duì)于AccessDecisionVoter的基本介紹,我們得知了一個(gè)設(shè)計(jì)上的大原則:AccessDecisionVoter的實(shí)現(xiàn)是為了滿足對(duì)應(yīng)規(guī)則ConfigAttribute。大體上來說AccessDecisionVoter是與ConfigAttribute一一對(duì)應(yīng)的。
讓我們回一下在上一期我們介紹的主要的幾種ConfigAttribute實(shí)現(xiàn):

  • 基于Web表達(dá)式的WebExpressionConfigAttribute
  • 基于@Secured注解的SecurityConfig
  • 基于@Pre-@Post注解的PostInvocationExpressionAttribute
    我們可以在下圖中輕松的找到他門對(duì)應(yīng)的AccessDecisionVoter。
    主要的AccessDecisionVoter

    這邊我們重點(diǎn)說一下在客制化場(chǎng)景下被利用的SecurityConfig配置和他默認(rèn)的兩個(gè)AccessDecisionVoter:
  • RoleVoter
  • AuthenticatedVoter
    首先,我們來回憶下SecurityConfig的使用形式,即利用@Secured注解編寫一個(gè)表達(dá)式:
@Secured("ROLE_USER")
@Secured("IS_AUTHENTICATED_ANONYMOUSLY")

我們了解到了AccessDecisionVoterConfigAttribute的關(guān)聯(lián)關(guān)系是通過supports方法進(jìn)行判斷,我們分別對(duì)RoleVoterAuthenticatedVoter的supports方法進(jìn)行瀏覽:

RoleVoter
RoleVoter是Spring Security中默認(rèn)基于角色規(guī)則的核心組件。在UserDetailsService中創(chuàng)建用戶我們都會(huì)需要設(shè)置對(duì)用用戶的角色信息。在默認(rèn)配置下用戶的角色信息都是以"ROLE_"+角色名的形式存儲(chǔ)的。
對(duì)應(yīng)的在RoleVoter的supports方法中會(huì)對(duì)表達(dá)式是否以'ROLE_'開始作為對(duì)應(yīng)啟用規(guī)則的判斷。如果規(guī)則表達(dá)式是以ROLE_開始的,RoleVoter則會(huì)去遍歷對(duì)用Authentication是否存在對(duì)應(yīng)的角色,如果存在則返回通過,如果不存在則返回拒絕。

public class RoleVoter implements AccessDecisionVoter<Object> {
    // ~ Instance fields
    // ================================================================================================

    private String rolePrefix = "ROLE_";

    // ~ Methods
    // ========================================================================================================

    public String getRolePrefix() {
        return rolePrefix;
    }

    /**
     * Allows the default role prefix of <code>ROLE_</code> to be overridden. May be set
     * to an empty value, although this is usually not desirable.
     *
     * @param rolePrefix the new prefix
     */
    public void setRolePrefix(String rolePrefix) {
        this.rolePrefix = rolePrefix;
    }

    public boolean supports(ConfigAttribute attribute) {
        if ((attribute.getAttribute() != null)
                && attribute.getAttribute().startsWith(getRolePrefix())) {
            return true;
        }
        else {
            return false;
        }
    }
}

AuthenticatedVoter
AuthenticatedVoter的使用場(chǎng)景就比較特殊,他并不是一個(gè)基于身份信息的訪問控制,而是對(duì)于對(duì)應(yīng)Auhentication的認(rèn)證形式的一個(gè)判斷。在之前的身份驗(yàn)證部分我們有了解過,在Spring Security設(shè)計(jì)中,我們可以銅鼓RememberMeService的方式不使用用戶名和密碼,而是通過存儲(chǔ)于Cookie的信息進(jìn)行授權(quán)登錄。在日常工程中,對(duì)于一些敏感操作,我們要求當(dāng)前的用戶并不是一個(gè)基于歷史進(jìn)行授權(quán)認(rèn)證的用戶,比如在進(jìn)行支付的情況下,如果我們希望用戶是在本次訪問中是通過用戶名和密碼進(jìn)行登錄展開的會(huì)話操作,而不是一個(gè)基于一個(gè)月前cookies進(jìn)行登錄都有用戶。在這個(gè)場(chǎng)景下我們需要便可以使用@Secured("IS_AUTHENTICATED_FULLY")去限定用戶是一個(gè)通過完全驗(yàn)證的用戶,而不是通過RememberMe方式認(rèn)證的用戶。
AuthenticatedVoter的supports方法中,便會(huì)判斷當(dāng)前的表達(dá)式是為他所支持的三種認(rèn)證方法的訪問控制:

  • IS_AUTHENTICATED_FULLY
  • IS_AUTHENTICATED_REMEMBERED
  • IS_AUTHENTICATED_ANONYMOUSLY
    如果完全匹配,則會(huì)當(dāng)前的Authentication對(duì)象的授權(quán)模式進(jìn)行判斷,返回相應(yīng)的投票結(jié)果。
public class AuthenticatedVoter implements AccessDecisionVoter<Object> {
    // ~ Static fields/initializers
    // =====================================================================================

    public static final String IS_AUTHENTICATED_FULLY = "IS_AUTHENTICATED_FULLY";
    public static final String IS_AUTHENTICATED_REMEMBERED = "IS_AUTHENTICATED_REMEMBERED";
    public static final String IS_AUTHENTICATED_ANONYMOUSLY = "IS_AUTHENTICATED_ANONYMOUSLY";
    // ~ Instance fields
    // ================================================================================================

    private AuthenticationTrustResolver authenticationTrustResolver = new AuthenticationTrustResolverImpl();

    // ~ Methods
    // ========================================================================================================

    private boolean isFullyAuthenticated(Authentication authentication) {
        return (!authenticationTrustResolver.isAnonymous(authentication) && !authenticationTrustResolver
                .isRememberMe(authentication));
    }

    public boolean supports(ConfigAttribute attribute) {
        if ((attribute.getAttribute() != null)
                && (IS_AUTHENTICATED_FULLY.equals(attribute.getAttribute())
                        || IS_AUTHENTICATED_REMEMBERED.equals(attribute.getAttribute()) || IS_AUTHENTICATED_ANONYMOUSLY
                            .equals(attribute.getAttribute()))) {
            return true;
        }
        else {
            return false;
        }
    }
}

三、 客制化實(shí)例:基于時(shí)間的AccessDecisionVoter

對(duì)于AccessDecisionVoter結(jié)構(gòu)、責(zé)任和Spring Security中提供的實(shí)現(xiàn)類有了一個(gè)基礎(chǔ)的了解后。我們通過一個(gè)客制化的實(shí)例來加強(qiáng)這部分的理解。
我們將客制化一個(gè)基于時(shí)間的訪問控制,在系統(tǒng)時(shí)間的分鐘數(shù)是奇數(shù)的情況下才可以被訪問,比如10點(diǎn)01分可以訪問,但是10點(diǎn)02分則不可以被訪問。

設(shè)計(jì)規(guī)則

首先,我們對(duì)訪問規(guī)則進(jìn)行設(shè)計(jì)。我們?nèi)缤?code>RoleVoter與AuthenticatedVoter一樣基于@Secured注解的表達(dá)式進(jìn)行擴(kuò)展。我們擬定的規(guī)則名為"MINUTE_ODD",當(dāng)方法級(jí)被注解了@Secured("MINUTE_ODD")情況下,表示當(dāng)前方法只有在滿足系統(tǒng)時(shí)間的分鐘數(shù)為奇數(shù)下才可以被訪問。

客制化MinuteBasedVoter

接下來,我們編寫一個(gè)MinuteBasedVoter擴(kuò)展AuthenticatedVoter

public class MinuteBasedVoter implements AccessDecisionVoter {
}

然后,我們實(shí)現(xiàn)對(duì)應(yīng)的suppors方法用于完成我們對(duì)我們擬定的規(guī)則的判斷。當(dāng)入?yún)onfigAttribute 的表達(dá)式屬性與我們預(yù)設(shè)的"MINUTE_ODD"一致時(shí),那么我們便返回true告知框架,MinuteBasedVoter需要對(duì)該規(guī)則進(jìn)行vote的投票操作。

public class MinuteBasedVoter implements AccessDecisionVoter {
    public static final String IS_MINUTE_ODD= "MINUTE_ODD";

    @Override
    public boolean supports(ConfigAttribute attribute) {
        if ((attribute.getAttribute() != null)
                && attribute.getAttribute().equals(IS_MINUTE_ODD)) {
            return true;
        }
        else {
            return false;
        }
    }


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

最后,我們將vote的投票核心業(yè)務(wù)邏輯完成:當(dāng)時(shí)間為奇數(shù)的時(shí)候則投贊同票,而在時(shí)間為偶數(shù)的時(shí)候則投一張明確的反對(duì)票

    @Override
    public int vote(Authentication authentication, Object object, Collection collection) {
        if(LocalDateTime.now().getMinute() % 2 != 0){
            return ACCESS_GRANTED;

        }else{
            return ACCESS_DENIED;
        }
    }

Java Config配置

最后,說一下如何將新的AccessDecisionVoter添加到現(xiàn)有的AccessDecisionManager中。我自己也百度了一下了中文世界和英文世界關(guān)于這方便的示例已經(jīng)官方文檔,真的是五花八門都有。最常見的是重新組織了一個(gè)AccessDecisionManager注入回Spring Security中,我很不推薦自己在方法中去new一個(gè)AccessDecisionManager。因?yàn)锳ccessDecisionManager的初始化過程中涉及的不只是AccessDecisionVoter,一不小心可能因?yàn)樯僭O(shè)置什么組件就導(dǎo)致一部分默認(rèn)行為沒被正確的配置上去。
我推薦初學(xué)者方法是對(duì)于擴(kuò)展Secured這類基于方法級(jí)的注解,單獨(dú)新建一個(gè)Java Config類,然后重寫原有框架中初始化AccessDecisionManager的方法:

@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)
@Configuration
public class MethodSecurityConfiguration extends GlobalMethodSecurityConfiguration {
    @Override
    protected AccessDecisionManager accessDecisionManager() {
        AffirmativeBased affirmativeBased = (AffirmativeBased) super.accessDecisionManager();
        affirmativeBased.getDecisionVoters().add(new MinuteBasedVoter());
        return affirmativeBased;
    }
}

雖然代碼可能丑、有對(duì)類型強(qiáng)轉(zhuǎn),相對(duì)來說好理解控制很多。
在添加了MethodSecurityConfiguration的Java Config之后,我們?cè)趯?duì)受到@Secured("MINUTE_ODD")注解限制的controller方式時(shí)便會(huì)看到以下的投票日志:

Secure object: ReflectiveMethodInvocation: public java.lang.String Attributes: [MINUTE_ODD]
Voter: org.springframework.security.access.prepost.PreInvocationAuthorizationAdviceVoter@456f4439, returned: 0
Voter: org.springframework.security.access.vote.RoleVoter@38b13fa8, returned: 0
Voter: org.springframework.security.access.vote.AuthenticatedVoter@590fa701, returned: 0
Voter: com.newnil.demo.security.MinuteBasedVoter@135c04e9, returned: 1
Authorization successful

AccessDecisionVoter組件們依次投票,而因?yàn)楫?dāng)前時(shí)間是奇數(shù),所以我們的MinuteBasedVoter投出一票值為1的贊同票。

結(jié)尾

這一期詳細(xì)介紹了AccessDecisionVoter這一為訪問控制提供核心判斷及投票的組件。同時(shí)也通過框架默認(rèn)提供與客制化實(shí)現(xiàn)了解了其工作原理。
下一期我們將最后一個(gè)核心組件AccessDecisionManager是如何對(duì)所有AccessDecisionVoter的投票結(jié)果進(jìn)行匯總,以及如何以什么評(píng)價(jià)規(guī)則告知框架最終的授權(quán)結(jié)果進(jìn)行說明。
我們下期再見。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 我不是一個(gè)孤陋寡聞的人。 幾年前,就聽說了《夜聽》這檔欄目非常火。劉筱這個(gè)人也是很多人知道的一號(hào)人物。 ...
    紅小楊閱讀 511評(píng)論 0 1
  • 2018-5-7 大雪 沒錯(cuò),今天的烏魯木齊下了大雪。 你在擔(dān)心一件事情,就是自己還可不可靠。 你說呢?無論如...
    蟋蟀王閱讀 170評(píng)論 0 0
  • 1970年陽春三月,沈家大閨女出世,這就是我的大姐。 那年沈家還居住在云巖鄉(xiāng)山前村,老屋前面有條清...
    我是貓小懶閱讀 1,096評(píng)論 0 2

友情鏈接更多精彩內(nèi)容