【詳解】Spring Security 之 SecurityContext


前言

  本文主要整理一下SecurityContext的存儲方式。

SecurityContext接口

顧名思義,安全上下文。即存儲認證授權的相關信息,實際上就是存儲"當前用戶"賬號信息和相關權限。這個接口只有兩個方法,Authentication對象的getter、setter。

package org.springframework.security.core.context;

import java.io.Serializable;
import org.springframework.security.core.Authentication;

public interface SecurityContext extends Serializable {
    Authentication getAuthentication();

    void setAuthentication(Authentication var1);
}

Authentication接口又是干嘛的?

注意:SecurityContext存儲的Authentication對象是經過認證的,所以它會帶有權限,它的getAuthorities()方法會返回相關權限。

package org.springframework.security.core;

import java.io.Serializable;
import java.security.Principal;
import java.util.Collection;

public interface Authentication extends Principal, Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();

    Object getCredentials();

    Object getDetails();

    Object getPrincipal();

    boolean isAuthenticated();

    void setAuthenticated(boolean var1) throws IllegalArgumentException;
}

SecurityContextHolder工具類

前面說的"當前用戶"實際上指的是當前這個請求所對應的用戶,那么怎么知道當前用戶是誰呢?由於一個請求從開始到結束都由一個線程處理,這個線程中途也不會去處理其他的請求。所以在這段時間內,相當於這個線程跟當前用戶是一一對應的。SecurityContextHolder工具類就是把SecurityContext存儲在當前線程中。

SecurityContextHolder可以用來設置和獲取SecurityContext。它主要是給框架內部使用的,可以利用它獲取當前用戶的SecurityContext進行請求檢查,和訪問控制等。

在Web環境下,SecurityContextHolder是利用ThreadLocal來存儲SecurityContext的。

請求結束,SecurityContext存儲在哪里?

我們知道Sevlet中線程是被池化復用的,一旦處理完當前的請求,它可能馬上就會被分配去處理其他的請求。而且也不能保證用戶下次的請求會被分配到同一個線程。所以存在線程里面,請求一旦結束,就沒了。如果沒有保存,不是每次請求都要重新認證登錄?想想看,如果沒有權限框架我們是怎么處理的?

想到了吧,如果不用權限框架,我們一般是把認證結果存在Session中的。同理,它也把認證結果存儲到Session了。

對應的Key是:"SPRING_SECURITY_CONTEXT"

流程視圖

SecurityContextPersistenceFilter攔截器

 SecurityContextPersistenceFilter是Security的攔截器,而且是攔截鏈中的第一個攔截器,請求來臨時它會從HttpSession中把SecurityContext取出來,然后放入SecurityContextHolder。在所有攔截器都處理完成后,再把SecurityContext存入HttpSession,並清除SecurityContextHolder內的引用。

注:其中repo對象是HttpSessionSecurityContextRepository

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

        if (request.getAttribute(FILTER_APPLIED) != null) {
            // ensure that filter is only applied once per request
            chain.doFilter(request, response);
            return;
        }

        final boolean debug = logger.isDebugEnabled();

        request.setAttribute(FILTER_APPLIED, Boolean.TRUE);

        if (forceEagerSessionCreation) {
            HttpSession session = request.getSession();

            if (debug && session.isNew()) {
                logger.debug("Eagerly created session: " + session.getId());
            }
        }

        HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request,
                response);
        //利用HttpSecurityContextRepository從HttpSesion中獲取SecurityContext對象
        //如果沒有HttpSession,即瀏覽器第一次訪問服務器,還沒有產生會話。
        //它會創建一個空的SecurityContext對象
        SecurityContext contextBeforeChainExecution = repo.loadContext(holder);

        try {
            //把SecurityContext放入到SecurityContextHolder中
            SecurityContextHolder.setContext(contextBeforeChainExecution);
            //執行攔截鏈,這個鏈會逐層向下執行
            chain.doFilter(holder.getRequest(), holder.getResponse());

        }
        finally { 
            //當攔截器都執行完的時候把當前線程對應的SecurityContext從SecurityContextHolder中取出來
            SecurityContext contextAfterChainExecution = SecurityContextHolder
                    .getContext();
            // Crucial removal of SecurityContextHolder contents - do this before anything
            // else.
            SecurityContextHolder.clearContext();
            //利用HttpSecurityContextRepository把SecurityContext寫入HttpSession
            repo.saveContext(contextAfterChainExecution, holder.getRequest(),
                    holder.getResponse());
            request.removeAttribute(FILTER_APPLIED);

            if (debug) {
                logger.debug("SecurityContextHolder now cleared, as request processing completed");
            }
        }
    }

Tomcat建立會話的流程

  有人可能對Tomcat建立會話的流程還不熟悉,這里稍微整理一下。是這樣的,當客戶瀏覽器打開后第一次訪問Tomcat服務器,Tomcat會創建一個HttpSesion對象,存入一個ConcurrentHashMap,Key是SessionId,Value就是HttpSession。然后請求完成后,在返回的報文中添加Set-Cookie:JSESSIONID=xxx,然后客戶端瀏覽器會保存這個Cookie。當瀏覽器再次訪問這個服務器的時候,都會帶上這個Cookie。Tomcat接收到這個請求后,根據JSESSIONID把對應的HttpSession對象取出來,放入HttpSerlvetRequest對象里面。

重點:

1.HttpSession會一直存在服務端,實際上是存在運行內存中。除非Session過期 OR Tomcat奔潰 OR 服務器奔潰,否則會話信息不會消失。

2.如無特殊處理,Cookie JSESSIONID會在瀏覽器關閉的時候清除。

3.Tomcat中HttpSesion的默認過期時間為30分鍾。

4.這些處理都在Security的攔截鏈之前完成。

——————————————————2019年1月21日更正——————————————

瀏覽器訪問Web項目並不會創建Session,只有在編程調用request.getSession()方法后Session才會建立,瀏覽器才會有JSESSIONID的Cookie。

一般是認證成功后,調用getSession()(其實在這之前Session並不存在),獲得Session對象,然后把信息存里面。

所以,一般的,在登錄之前,你不管怎么訪問網站,都不會建立會話。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM