1. 程式人生 > >Shiro原始碼研究之構建Subject例項

Shiro原始碼研究之構建Subject例項

接上一篇部落格Shiro原始碼研究之處理一次完整的請求,其中的第二小節中的這樣一行程式碼final Subject subject = createSubject(request, response);直接略過了。這篇文章就專門來對這段程式碼後面所發生的事情進行一下探討。

1. 前言

Subject作為Shiro對外API的核心,必然需要相當多的功能模組進行支撐。因此雖然構造Subject的程式碼看似只有一行,但後面的邏輯還是有點複雜度的。接下來就讓我們對這些模組進行一下了解。

2. AbstractShiroFilter.createSubject方法

// AbstractShiroFilter.createSubject
protected WebSubject createSubject(ServletRequest request, ServletResponse response) { // securityManager是shiro強制要求使用者必須自己配置的。 // 在Web環境下,其為DefaultWebSecurityManager型別。這一點隨便找個spring-shiro的配置檔案就能看到了。 return new WebSubject.Builder(getSecurityManager(), request, response).buildWebSubject(); }

3. WebSubject.Builder
建構函式

WebSubject也是個介面,繼承自Subject;而WebSubject.Builder 同樣繼承自Subject.Builder;所以WebSubjectSubject是有著相同的結構的。

// WebSubject.Builder 建構函式
public Builder(SecurityManager securityManager, ServletRequest request, ServletResponse response) {
    // 呼叫Subject.Builder的建構函式
    super(securityManager);
    if (request == null
) { throw new IllegalArgumentException("ServletRequest argument cannot be null."); } if (response == null) { throw new IllegalArgumentException("ServletResponse argument cannot be null."); } // 讓SubjectContext會話域附加上當前請求request; 貫穿整個執行過程。 setRequest(request); // 讓SubjectContext會話域附加上當前響應response; 貫穿整個執行過程。 setResponse(response); } // Subject.Builder 建構函式 public Builder(SecurityManager securityManager) { if (securityManager == null) { throw new NullPointerException("SecurityManager method argument cannot be null."); } this.securityManager = securityManager; // 構建一個SubjectContext會話域。 this.subjectContext = newSubjectContextInstance(); if (this.subjectContext == null) { throw new IllegalStateException("Subject instance returned from 'newSubjectContextInstance' " + "cannot be null."); } // 讓會話域帶著securityManager貫穿整個執行過程。 this.subjectContext.setSecurityManager(securityManager); }

4. WebSubject.Builder.buildWebSubject方法

// ---------- WebSubject.Builder.buildWebSubject
public WebSubject buildWebSubject() {
    // 呼叫基類Subject.Builder的buildSubject方法
    Subject subject = super.buildSubject();
    // 確保返回值為WebSubject型別
    if (!(subject instanceof WebSubject)) {
        String msg = "Subject implementation returned from the SecurityManager was not a " +
                WebSubject.class.getName() + " implementation.  Please ensure a Web-enabled SecurityManager " +
                "has been configured and made available to this builder.";
        throw new IllegalStateException(msg);
    }
    return (WebSubject) subject;
}

// ----------  Subject.Builder的buildSubject方法
public Subject buildSubject() {
    // 委託給了SecurityManager例項; 
    // 這裡的securityManager實際型別為DefaultWebSecurityManager型別
    // 而subjectContext的時機型別為DefaultWebSubjectContext, 而且按照之前的跟蹤,我們知道該Context中已經被填入了當前請求的request,response以及securityManager例項(而且請注意DefaultWebSubjectContext的實現是直接將這些例項以特定的鍵推入內部的Map容器, 只是暴露了專門的方法進行讀取罷了, 並沒有額外新增欄位)。
    return this.securityManager.createSubject(this.subjectContext);
}

5. DefaultSecurityManager.createSubject 方法

// DefaultSecurityManager.createSubject
public Subject createSubject(SubjectContext subjectContext) {
    //create a copy so we don't modify the argument's backing map:
    // copy方法被子類DefaultWebSecurityManager過載; 返回一個DefaultWebSubjectContext例項
    SubjectContext context = copy(subjectContext);

    //ensure that the context has a SecurityManager instance, and if not, add one:
    // 子類DefaultWebSecurityManager未進行過載, 
    // 此方法確保會話域持有一個SecurityManager來貫穿整個執行流程。
    context = ensureSecurityManager(context);

    //Resolve an associated Session (usually based on a referenced session ID), and place it in the context before
    //sending to the SubjectFactory.  The SubjectFactory should not need to know how to acquire sessions as the
    //process is often environment specific - better to shield the SF from these details:
    // 向會話域中存入一個Session例項; 
    // 此操作可能失敗, 屆時會構建一個缺少Session的會話域 
    // 注意這裡的構建Session出錯是被允許的, 所以異常是以Debug的方式輸出的.
    // Session的維護是交給了專門的SessionManager來負責
    // 注意這裡用的是SessionKey型別的Key,而不是簡單的string型別的sessionId
    // 因為Session我們操作的比較頻繁,所以下文會進行詳解
    context = resolveSession(context);

    //Similarly, the SubjectFactory should not require any concept of RememberMe - translate that here first
    //if possible before handing off to the SubjectFactory:
    // 這一步會向會話域中插入Principal; 此操作也有可能失敗, 即最終會話域context中缺少Principal資訊
    // rememberMe的功能也是交給了專門的RememberMeManager
    // 而且預設的RememberMe功能是通過Cookie來完成的, 所以預設的實現是CookieRememberMeManager; 而且cookie的預設名稱是rememberMe
    // 而且Shiro有自己專門的Cookie介面,而唯一的實現則是SimpleCookie
    context = resolvePrincipals(context);

    // 看過Spring原始碼的都知道這命名意味著什麼, 真正幹活的來了。
    // 建立Subject的工作又被委派給了專門的SubjectFactory, 七拐八繞啊。
    // SubjectFactory介面的預設實現為DefaultWebSubjectFactory
    // 觀察其對createSubject方法的實現正式將會話域context這一路收集來的資訊彙總生成一個WebDelegatingSubject例項(又增加一箇中間層)。
    Subject subject = doCreateSubject(context);

    //save this subject for future reference if necessary:
    //(this is needed here in case rememberMe principals were resolved and they need to be stored in the
    //session, so we don't constantly rehydrate the rememberMe PrincipalCollection on every operation).
    //Added in 1.2:
    // 專門的SubjectDAO介面負責對該subject進行儲存操作
    // SubjectDAO介面的預設實現類為DefaultSubjectDAO
    save(subject);

    return subject;
}

也就是說會話域Context一路收集來的principals, authenticated, host, session, sessionEnabled, request, response, securityManager ; 最終被存入到了返回的這個Subject中。

5.1 DefaultSecurityManager.resolveSession方法

Session是我們比較關注的。

@SuppressWarnings({"unchecked"})
protected SubjectContext resolveSession(SubjectContext context) {
    if (context.resolveSession() != null) {
        log.debug("Context already contains a session.  Returning.");
        return context;
    }
    try {
        //Context couldn't resolve it directly, let's see if we can since we have direct access to 
        //the session manager:
        // 獲取Session
        Session session = resolveContextSession(context);
        if (session != null) {
            context.setSession(session);
        }
    } catch (InvalidSessionException e) {
        log.debug("Resolved SubjectContext context session is invalid.  Ignoring and creating an anonymous " +
                "(session-less) Subject instance.", e);
    }
    return context;
}

// ---------- DefaultSecurityManager.resolveContextSession
protected Session resolveContextSession(SubjectContext context) throws InvalidSessionException {
    SessionKey key = getSessionKey(context);
    if (key != null) {
        // 委託給專門的sessionManager去提取Session
        return getSession(key);
    }
    return null;
}

// ---------- DefaultWebSecurityManager.getSessionKey
// DefaultWebSecurityManager覆寫了基類的getSessionKey方法
@Override
protected SessionKey getSessionKey(SubjectContext context) {
    if (WebUtils.isWeb(context)) {
        Serializable sessionId = context.getSessionId();
        ServletRequest request = WebUtils.getRequest(context);
        ServletResponse response = WebUtils.getResponse(context);
        return new WebSessionKey(sessionId, request, response);
    } else {
        // 呼叫基類DefaultSecurityManager的getSessionKey方法
        return super.getSessionKey(context);

    }
}

// ---------- DefaultSecurityManager.getSessionKey
protected SessionKey getSessionKey(SubjectContext context) {
    // 所以如果本次會話域中沒有帶過來SessionId, 那麼就不會建立一個SessionKey例項; 當然也就不會建立一個Session了
    Serializable sessionId = context.getSessionId();
    if (sessionId != null) {
        return new DefaultSessionKey(sessionId);
    }
    return null;
}

6. 繫結到執行緒上下文

上一篇部落格除了略過Subject例項的構造細節外,還省略了非常有意思的細節——Shiro將獲取到的subject例項儲存在了當前執行執行緒的執行緒本地儲存中。現在就讓我們來看看Shiro是如何透明化這一部分操作的。

subject.execute(new Callable() {
    public Object call() throws Exception {
        // 更新最後讀取session的時間
        updateSessionLastAccessTime(request, response);
        // 核心,下文進行講解
        executeChain(request, response, chain);
        return null;
    }
});

以上就是上一篇部落格裡的程式碼了,其中關於Callable匿名實現類裡的方法我們已經進行過講解;而且既然我們談到的是”透明化“,說明魔法其實應該是在execute方法裡了。

6.1 WebDelegatingSubject.execute方法

上一篇文章我們已經得知在Web環境下,subject欄位的實際型別是WebDelegatingSubject。這裡我放上一張相關堆疊圖
subject.execute堆疊圖

參考本人在簡書上的《如何加速原始碼利理解速度》一文,可以得出不少資訊:
1. 雖然當前的實際型別是WebDelegatingSubject,而execute實際是定義在其基類DelegatingSubject中的,而且WebDelegatingSubject並未對其進行過載。
2. DelegatingSubject類對execute方法的實現則是將其委託給了專門的SubjectCallable<V>
3. 正是在SubjectCallable<V>類中,Shiro將本次構造的Subject例項存放到了當前Thread的執行緒本地儲存中;也就是將該Subject例項與當前Thread進行繫結
4. 還可以看到Shiro對於ThreadLocal<T>的使用方式是將其封裝為對自定義的ThreadContext的呼叫。關於ThreadContext,其顯式繫結的就兩個例項:Subject和SecurityManager 。這個觀察其定義的bind方法就知曉了。

6.2 DelegatingSubject.execute方法

這裡還是貼出DelegatingSubject.execute的實現。

// DelegatingSubject.execute
public <V> V execute(Callable<V> callable) throws ExecutionException {
    // 構造出一個SubjectCallable<V>例項。
    Callable<V> associated = associateWith(callable);
    try {
        // 在SubjectCallable<V>中透明化對Subject繫結/解綁執行緒本地量的操作。
        return associated.call();
    } catch (Throwable t) {
        throw new ExecutionException(t);
    }
}

7. SecurityUtils.getSubject()方法

如果說Shiro被呼叫者最常接觸到的,應該就是這個方法了。所以在本文的最後我們順勢來看看這方法。

// SecurityUtils.getSubject()
public static Subject getSubject() {
    // ThreadContext是Shiro內部定義的, 對ThreadLocal<T>進行了封裝。
    // ThreadLocal<T>的講解,可以參見《精通Spring4.x企業應用開發實戰》 P363
    // 聯絡上面的內容,我們可以猜測,在自定義的Realm或者其他地方,以下方法是可以直接取到之前構建出的WebDelegatingSubject例項的。
    Subject subject = ThreadContext.getSubject();
    if (subject == null) {
        // Subject是介面,而這個Builder型別則是作為public訪問級別的靜態內部類, 被定義在Subject介面內部的; 
        // buildSubject方法裡則是直接將構建Subject例項的工作交給了SecurityManager介面實現類(這就是為了呼叫使用之前,必須呼叫SecurityUtils.setSecurityManager方法設定)。在Web環境下,這個securityManager的真實型別為DefaultWebSecurityManager
        subject = (new Subject.Builder()).buildSubject();
        // 將取出來的Subject儲存上當前執行緒的私有容器中。
        ThreadContext.bind(subject);
    }
    return subject;
}

8. 結語

綜合前後兩篇文章,大致應該可以瞭解在一次請求過程中,Shiro會啟用哪些元件,以及是怎樣的方式來完成許可權認證的。