Spring Security源碼分析二:Spring Security授權(quán)過程

前言

本文是接上一章Spring Security源碼分析一:Spring Security認(rèn)證過程進(jìn)一步分析Spring Security用戶名密碼登錄授權(quán)是如何實(shí)現(xiàn)得;

類圖

調(diào)試過程

使用debug方式啟動(dòng)https://github.com/longfeizheng/logback該項(xiàng)目,瀏覽器輸入http://localhost:8080/persons,用戶名隨意习绢,密碼123456即可鞭铆;

源碼分析

如圖所示岭洲,顯示了登錄認(rèn)證過程中的 filters 相關(guān)的調(diào)用流程,作者將幾個(gè)自認(rèn)為重要的 filters 標(biāo)注了出來取刃,

從圖中可以看出執(zhí)行的順序吧彪。來看看幾個(gè)作者認(rèn)為比較重要的 Filter 的處理邏輯待侵,UsernamePasswordAuthenticationFilterAnonymousAuthenticationFilter姨裸,ExceptionTranslationFilter诫给,FilterSecurityInterceptor 以及相關(guān)的處理流程如下所述;

UsernamePasswordAuthenticationFilter

整個(gè)調(diào)用流程是啦扬,先調(diào)用其父類 AbstractAuthenticationProcessingFilter.doFilter() 方法凫碌,然后再執(zhí)行 UsernamePasswordAuthenticationFilter.attemptAuthentication() 方法進(jìn)行驗(yàn)證扑毡;

AbstractAuthenticationProcessingFilter

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;
        #1.判斷當(dāng)前的filter是否可以處理當(dāng)前請求,不可以的話則交給下一個(gè)filter處理
        if (!requiresAuthentication(request, response)) {
            chain.doFilter(request, response);

            return;
        }

        if (logger.isDebugEnabled()) {
            logger.debug("Request is to process authentication");
        }

        Authentication authResult;

        try {
            #2.抽象方法由子類UsernamePasswordAuthenticationFilter實(shí)現(xiàn)
            authResult = attemptAuthentication(request, response);
            if (authResult == null) {
                // return immediately as subclass has indicated that it hasn't completed
                // authentication
                return;
            }
            #2.認(rèn)證成功后盛险,處理一些與session相關(guān)的方法 
            sessionStrategy.onAuthentication(authResult, request, response);
        }
        catch (InternalAuthenticationServiceException failed) {
            logger.error(
                    "An internal error occurred while trying to authenticate the user.",
                    failed);
            #3.認(rèn)證失敗后的的一些操作
            unsuccessfulAuthentication(request, response, failed);

            return;
        }
        catch (AuthenticationException failed) {
            // Authentication failed
            unsuccessfulAuthentication(request, response, failed);

            return;
        }

        // Authentication success
        if (continueChainBeforeSuccessfulAuthentication) {
            chain.doFilter(request, response);
        }
        #3. 認(rèn)證成功后的相關(guān)回調(diào)方法 主要將當(dāng)前的認(rèn)證放到SecurityContextHolder中
        successfulAuthentication(request, response, chain, authResult);
    }

整個(gè)程序的執(zhí)行流程如下:

  1. 判斷filter是否可以處理當(dāng)前的請求瞄摊,如果不可以則放行交給下一個(gè)filter
  2. 調(diào)用抽象方法attemptAuthentication進(jìn)行驗(yàn)證勋又,該方法由子類UsernamePasswordAuthenticationFilter實(shí)現(xiàn)
  3. 認(rèn)證成功以后,回調(diào)一些與 session 相關(guān)的方法换帜;
  4. 認(rèn)證成功以后楔壤,認(rèn)證成功后的相關(guān)回調(diào)方法;認(rèn)證成功以后惯驼,認(rèn)證成功后的相關(guān)回調(diào)方法蹲嚣;
protected void successfulAuthentication(HttpServletRequest request,
            HttpServletResponse response, FilterChain chain, Authentication authResult)
            throws IOException, ServletException {

        if (logger.isDebugEnabled()) {
            logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
                    + authResult);
        }

        SecurityContextHolder.getContext().setAuthentication(authResult);

        rememberMeServices.loginSuccess(request, response, authResult);

        // Fire event
        if (this.eventPublisher != null) {
            eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
                    authResult, this.getClass()));
        }

        successHandler.onAuthenticationSuccess(request, response, authResult);
    }
1. 將當(dāng)前認(rèn)證成功的 Authentication 放置到 SecurityContextHolder 中;
2. 將當(dāng)前認(rèn)證成功的 Authentication 放置到 SecurityContextHolder 中祟牲;
3. 調(diào)用其它可擴(kuò)展的 handlers 繼續(xù)處理該認(rèn)證成功以后的回調(diào)事件隙畜;(實(shí)現(xiàn)`AuthenticationSuccessHandler`接口即可)

UsernamePasswordAuthenticationFilter

public Authentication attemptAuthentication(HttpServletRequest request,
            HttpServletResponse response) throws AuthenticationException {
        #1.判斷請求的方法必須為POST請求
        if (postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException(
                    "Authentication method not supported: " + request.getMethod());
        }
        #2.從request中獲取username和password
        String username = obtainUsername(request);
        String password = obtainPassword(request);

        if (username == null) {
            username = "";
        }

        if (password == null) {
            password = "";
        }

        username = username.trim();
        #3.構(gòu)建UsernamePasswordAuthenticationToken(兩個(gè)參數(shù)的構(gòu)造方法setAuthenticated(false))
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                username, password);

        // Allow subclasses to set the "details" property
        setDetails(request, authRequest);
        #4. 調(diào)用 AuthenticationManager 進(jìn)行驗(yàn)證(子類ProviderManager遍歷所有的AuthenticationProvider認(rèn)證)
        return this.getAuthenticationManager().authenticate(authRequest);
    }
  1. 認(rèn)證請求的方法必須為POST
  2. 從request中獲取 username 和 password
  3. 封裝Authenticaiton的實(shí)現(xiàn)類UsernamePasswordAuthenticationToken,(UsernamePasswordAuthenticationToken調(diào)用兩個(gè)參數(shù)的構(gòu)造方法setAuthenticated(false))
  4. 調(diào)用 AuthenticationManagerauthenticate 方法進(jìn)行驗(yàn)證说贝;可參考ProviderManager部分议惰;

AnonymousAuthenticationFilter

從上圖中過濾器的執(zhí)行順序圖中可以看出AnonymousAuthenticationFilter過濾器是在UsernamePasswordAuthenticationFilter等過濾器之后,如果它前面的過濾器都沒有認(rèn)證成功乡恕,Spring Security則為當(dāng)前的SecurityContextHolder中添加一個(gè)Authenticaiton 的匿名實(shí)現(xiàn)類AnonymousAuthenticationToken;

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        #1.如果前面的過濾器都沒認(rèn)證通過言询,則SecurityContextHolder中Authentication為空
        if (SecurityContextHolder.getContext().getAuthentication() == null) {
            #2.為當(dāng)前的SecurityContextHolder中添加一個(gè)匿名的AnonymousAuthenticationToken
            SecurityContextHolder.getContext().setAuthentication(
                    createAuthentication((HttpServletRequest) req));

            if (logger.isDebugEnabled()) {
                logger.debug("Populated SecurityContextHolder with anonymous token: '"
                        + SecurityContextHolder.getContext().getAuthentication() + "'");
            }
        }
        else {
            if (logger.isDebugEnabled()) {
                logger.debug("SecurityContextHolder not populated with anonymous token, as it already contained: '"
                        + SecurityContextHolder.getContext().getAuthentication() + "'");
            }
        }

        chain.doFilter(req, res);
    }

    #3.創(chuàng)建匿名的AnonymousAuthenticationToken
    protected Authentication createAuthentication(HttpServletRequest request) {
        AnonymousAuthenticationToken auth = new AnonymousAuthenticationToken(key,
                principal, authorities);
        auth.setDetails(authenticationDetailsSource.buildDetails(request));

        return auth;
    }
    
        /**
     * Creates a filter with a principal named "anonymousUser" and the single authority
     * "ROLE_ANONYMOUS".
     *
     * @param key the key to identify tokens created by this filter
     */
     ##.創(chuàng)建一個(gè)用戶名為anonymousUser 授權(quán)為ROLE_ANONYMOUS
    public AnonymousAuthenticationFilter(String key) {
        this(key, "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
    }
  1. 判斷SecurityContextHolder中Authentication為否為空;
  2. 如果空則為當(dāng)前的SecurityContextHolder中添加一個(gè)匿名的AnonymousAuthenticationToken(用戶名為 anonymousUser 的AnonymousAuthenticationToken

ExceptionTranslationFilter

ExceptionTranslationFilter 異常處理過濾器,該過濾器用來處理在系統(tǒng)認(rèn)證授權(quán)過程中拋出的異常(也就是下一個(gè)過濾器FilterSecurityInterceptor),主要是 處理 AuthenticationExceptionAccessDeniedException 傲宜。

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        try {
            chain.doFilter(request, response);

            logger.debug("Chain processed normally");
        }
        catch (IOException ex) {
            throw ex;
        }
        catch (Exception ex) {
            // Try to extract a SpringSecurityException from the stacktrace
            #.判斷是不是AuthenticationException
            Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
            RuntimeException ase = (AuthenticationException) throwableAnalyzer
                    .getFirstThrowableOfType(AuthenticationException.class, causeChain);

            if (ase == null) {
                #. 判斷是不是AccessDeniedException
                ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(
                        AccessDeniedException.class, causeChain);
            }

            if (ase != null) {
                handleSpringSecurityException(request, response, chain, ase);
            }
            else {
                // Rethrow ServletExceptions and RuntimeExceptions as-is
                if (ex instanceof ServletException) {
                    throw (ServletException) ex;
                }
                else if (ex instanceof RuntimeException) {
                    throw (RuntimeException) ex;
                }

                // Wrap other Exceptions. This shouldn't actually happen
                // as we've already covered all the possibilities for doFilter
                throw new RuntimeException(ex);
            }
        }
    }

FilterSecurityInterceptor

此過濾器為認(rèn)證授權(quán)過濾器鏈中最后一個(gè)過濾器运杭,該過濾器之后就是請求真正的/persons 服務(wù)

public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {
        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) {
            // 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);
            }
            #1. before invocation重要
            InterceptorStatusToken token = super.beforeInvocation(fi);

            try {
                #2. 可以理解開始請求真正的 /persons 服務(wù)
                fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
            }
            finally {
                super.finallyInvocation(token);
            }
            #3. after Invocation
            super.afterInvocation(token, null);
        }
    }
  1. before invocation重要
  2. 請求真正的 /persons 服務(wù)
  3. after Invocation

三個(gè)部分中,最重要的是 #1蛋哭,該過程中會(huì)調(diào)用 AccessDecisionManager 來驗(yàn)證當(dāng)前已認(rèn)證成功的用戶是否有權(quán)限訪問該資源县习;

before invocation: AccessDecisionManager

protected InterceptorStatusToken beforeInvocation(Object object) {
        ...

        Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
                .getAttributes(object);

        ...
        Authentication authenticated = authenticateIfRequired();

        // Attempt authorization
        try {
            #1.重點(diǎn)
            this.accessDecisionManager.decide(authenticated, object, attributes);
        }
        catch (AccessDeniedException accessDeniedException) {
            publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,accessDeniedException));

            throw accessDeniedException;
        }

        ...
    }

authenticated就是當(dāng)前認(rèn)證的Authentication,那么objectattributes又是什么呢谆趾?

attributes和object 是什么躁愿?

Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
                .getAttributes(object);

調(diào)試

我們發(fā)現(xiàn)object為當(dāng)前請求的 url:/persons, 那么getAttributes方法就是使用當(dāng)前的訪問資源路徑去匹配我們自己定義的匹配規(guī)則。

protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()//使用表單登錄沪蓬,不再使用默認(rèn)httpBasic方式
                .loginPage(SecurityConstants.DEFAULT_UNAUTHENTICATION_URL)//如果請求的URL需要認(rèn)證則跳轉(zhuǎn)的URL
                .loginProcessingUrl(SecurityConstants.DEFAULT_SIGN_IN_PROCESSING_URL_FORM)//處理表單中自定義的登錄URL
                .and()
                .authorizeRequests().antMatchers(SecurityConstants.DEFAULT_UNAUTHENTICATION_URL,
                SecurityConstants.DEFAULT_SIGN_IN_PROCESSING_URL_FORM,
                SecurityConstants.DEFAULT_REGISTER_URL,
                "/**/*.js",
                "/**/*.css",
                "/**/*.jpg",
                "/**/*.png",
                "/**/*.woff2")
                .permitAll()//以上的請求都不需要認(rèn)證
                .anyRequest()//剩下的請求
                .authenticated()//都需要認(rèn)證
                .and()
                .csrf().disable()//關(guān)閉csrd攔截
        ;
    }

0-7返回 permitALL即不需要認(rèn)證 ,8對應(yīng)anyRequest返回 authenticated即當(dāng)前請求需要認(rèn)證;

可以看到當(dāng)前的authenticated為匿名AnonymousAuthentication用戶名為anonymousUser

AccessDecisionManager 是如何授權(quán)的彤钟?

Spring Security默認(rèn)使用AffirmativeBased實(shí)現(xiàn)AccessDecisionManagerdecide 方法來實(shí)現(xiàn)授權(quán)

public void decide(Authentication authentication, Object object,
            Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
        int deny = 0;
        #1.調(diào)用AccessDecisionVoter 進(jìn)行vote(投票)
        for (AccessDecisionVoter voter : getDecisionVoters()) {
            int result = voter.vote(authentication, object, configAttributes);

            if (logger.isDebugEnabled()) {
                logger.debug("Voter: " + voter + ", returned: " + result);
            }

            switch (result) {
            #1.1只要有voter投票為ACCESS_GRANTED,則通過 直接返回
            case AccessDecisionVoter.ACCESS_GRANTED://1
                return;
            @#1.2只要有voter投票為ACCESS_DENIED跷叉,則記錄一下
            case AccessDecisionVoter.ACCESS_DENIED://-1
                deny++;

                break;

            default:
                break;
            }
        }

        if (deny > 0) {
        #2.如果有兩個(gè)及以上AccessDecisionVoter(姑且稱之為投票者吧)都投ACCESS_DENIED逸雹,則直接就不通過了
            throw new AccessDeniedException(messages.getMessage(
                    "AbstractAccessDecisionManager.accessDenied", "Access is denied"));
        }

        // To get this far, every AccessDecisionVoter abstained
        checkAllowIfAllAbstainDecisions();
    }
  1. 調(diào)用AccessDecisionVoter 進(jìn)行vote(投票)
  2. 只要有投通過(ACCESS_GRANTED)票,則直接判為通過云挟。
  3. 如果沒有投通過則 deny++ ,最后判斷if(deny>0 拋出AccessDeniedException(未授權(quán))

WebExpressionVoter.vote()

public int vote(Authentication authentication, FilterInvocation fi,
            Collection<ConfigAttribute> attributes) {
        assert authentication != null;
        assert fi != null;
        assert attributes != null;

        WebExpressionConfigAttribute weca = findConfigAttribute(attributes);

        if (weca == null) {
            return ACCESS_ABSTAIN;
        }

        EvaluationContext ctx = expressionHandler.createEvaluationContext(authentication,
                fi);
        ctx = weca.postProcess(ctx, fi);

        return ExpressionUtils.evaluateAsBoolean(weca.getAuthorizeExpression(), ctx) ? ACCESS_GRANTED
                : ACCESS_DENIED;
    }

到此位置authentication當(dāng)前用戶信息梆砸,fl當(dāng)前訪問的資源路徑及attributes當(dāng)前資源路徑的決策(即是否需要認(rèn)證)。剩下就是判斷當(dāng)前用戶的角色Authentication.authorites是否權(quán)限訪問決策訪問當(dāng)前資源fi园欣。

時(shí)序圖

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末帖世,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子沸枯,更是在濱河造成了極大的恐慌日矫,老刑警劉巖赂弓,帶你破解...
    沈念sama閱讀 217,084評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異哪轿,居然都是意外死亡盈魁,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,623評論 3 392
  • 文/潘曉璐 我一進(jìn)店門窃诉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來杨耙,“玉大人,你說我怎么就攤上這事褐奴“唇牛” “怎么了?”我有些...
    開封第一講書人閱讀 163,450評論 0 353
  • 文/不壞的土叔 我叫張陵敦冬,是天一觀的道長辅搬。 經(jīng)常有香客問我,道長脖旱,這世上最難降的妖魔是什么堪遂? 我笑而不...
    開封第一講書人閱讀 58,322評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮萌庆,結(jié)果婚禮上溶褪,老公的妹妹穿的比我還像新娘。我一直安慰自己践险,他們只是感情好猿妈,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,370評論 6 390
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著巍虫,像睡著了一般彭则。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上占遥,一...
    開封第一講書人閱讀 51,274評論 1 300
  • 那天俯抖,我揣著相機(jī)與錄音,去河邊找鬼瓦胎。 笑死芬萍,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的搔啊。 我是一名探鬼主播柬祠,決...
    沈念sama閱讀 40,126評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼负芋!你這毒婦竟也來了瓶盛?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,980評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎惩猫,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蚜点,經(jīng)...
    沈念sama閱讀 45,414評論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡轧房,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,599評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了绍绘。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片奶镶。...
    茶點(diǎn)故事閱讀 39,773評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖陪拘,靈堂內(nèi)的尸體忽然破棺而出厂镇,到底是詐尸還是另有隱情,我是刑警寧澤左刽,帶...
    沈念sama閱讀 35,470評論 5 344
  • 正文 年R本政府宣布捺信,位于F島的核電站,受9級特大地震影響欠痴,放射性物質(zhì)發(fā)生泄漏迄靠。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,080評論 3 327
  • 文/蒙蒙 一喇辽、第九天 我趴在偏房一處隱蔽的房頂上張望掌挚。 院中可真熱鬧,春花似錦菩咨、人聲如沸吠式。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,713評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽特占。三九已至,卻和暖如春缨硝,著一層夾襖步出監(jiān)牢的瞬間摩钙,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,852評論 1 269
  • 我被黑心中介騙來泰國打工查辩, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留胖笛,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,865評論 2 370
  • 正文 我出身青樓宜岛,卻偏偏與公主長得像长踊,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子萍倡,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,689評論 2 354

推薦閱讀更多精彩內(nèi)容