Shiro中Subject物件的建立與繫結流程分析

bigfan發表於2021-01-31

我們在平常使用Shrio進行身份認證時,經常通過獲取Subject 物件中儲存的Session、Principal等資訊,來獲取認證使用者的資訊,也就是說Shiro會把認證後的使用者資訊儲存在Subject 中供程式使用

    public static Subject getSubject()
    {
        return SecurityUtils.getSubject();
    }

 Subject 是Shiro中核心的也是我們經常用到的一個物件,那麼Subject 物件是怎麼構造建立,並如何儲存繫結供程式呼叫的,下面我們就對其流程進行一下探究,首先是Subject 介面本身的繼承與實現,這裡我們需要特別關注下WebDelegatingSubject這個實現類,這個就是最終返回的具體實現類

 一、Subject的建立

 在Shiro中每個http請求都會經過SpringShiroFilter的父類AbstractShiroFilte中的doFilterInternal方法,我們看下具體程式碼

    protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain)
            throws ServletException, IOException {

        Throwable t = null;

        try {
            final ServletRequest request = prepareServletRequest(servletRequest, servletResponse, chain);
            final ServletResponse response = prepareServletResponse(request, servletResponse, chain);

            //建立Subject
            final Subject subject = createSubject(request, response);

            //執行Subject繫結
            //noinspection unchecked
            subject.execute(new Callable() {
                public Object call() throws Exception {
                    updateSessionLastAccessTime(request, response);
                    executeChain(request, response, chain);
                    return null;
                }
            });
        } catch (ExecutionException ex) {
            t = ex.getCause();
        } catch (Throwable throwable) {
            t = throwable;
        }

        if (t != null) {
            if (t instanceof ServletException) {
                throw (ServletException) t;
            }
            if (t instanceof IOException) {
                throw (IOException) t;
            }
            //otherwise it's not one of the two exceptions expected by the filter method signature - wrap it in one:
            String msg = "Filtered request failed.";
            throw new ServletException(msg, t);
        }
    }

繼續進入createSubject方法,也就是建立Subject物件的入口

    protected WebSubject createSubject(ServletRequest request, ServletResponse response) {
        return new WebSubject.Builder(getSecurityManager(), request, response).buildWebSubject();
    }

這裡使用了build的物件構建模式,進入WebSubject介面中檢視Builder與buildWebSubject()的具體實現

Builder()中主要用於初始化SecurityManager 、ServletRequest 、ServletResponse 等物件,構建SubjectContext上下文關係物件

         */
        public Builder(SecurityManager securityManager, ServletRequest request, ServletResponse response) {
            super(securityManager);
            if (request == null) {
                throw new IllegalArgumentException("ServletRequest argument cannot be null.");
            }
            if (response == null) {
                throw new IllegalArgumentException("ServletResponse argument cannot be null.");
            }
            setRequest(request);
            setResponse(response);
        }

 buildWebSubject方法中開始構造Subject物件

        public WebSubject buildWebSubject() {
            Subject subject = super.buildSubject();//父類build方法
            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;
        }

進入父類的buildSubject物件我們可以看到,具體實現是由SecurityManager來完成的

        public Subject buildSubject() {
            return this.securityManager.createSubject(this.subjectContext);
        }

 在createSubject方法中會根據你的配置從快取、redis、資料庫中獲取Session、Principals等資訊,並建立Subject物件

    public Subject createSubject(SubjectContext subjectContext) {
        //create a copy so we don't modify the argument's backing map:
        SubjectContext context = copy(subjectContext); //複製一個SubjectContext物件

        //ensure that the context has a SecurityManager instance, and if not, add one:
        context = ensureSecurityManager(context); // 檢查並初始化SecurityManager物件

        //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:
        context = resolveSession(context);//解析獲取Sesssion資訊

        //Similarly, the SubjectFactory should not require any concept of RememberMe - translate that here first
        //if possible before handing off to the SubjectFactory:
        context = resolvePrincipals(context);//解析獲取resolvePrincipals資訊

        Subject subject = doCreateSubject(context);//建立Subject

        //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:
        save(subject);

        return subject;
    }

在doCreateSubject中通過SubjectFactory建立合成Subject物件

    protected Subject doCreateSubject(SubjectContext context) {
        return getSubjectFactory().createSubject(context);
    }

我們可以看到最後返回的是具體實現類WebDelegatingSubject

    public Subject createSubject(SubjectContext context) {
        //SHIRO-646
        //Check if the existing subject is NOT a WebSubject. If it isn't, then call super.createSubject instead.
        //Creating a WebSubject from a non-web Subject will cause the ServletRequest and ServletResponse to be null, which wil fail when creating a session.
        boolean isNotBasedOnWebSubject = context.getSubject() != null && !(context.getSubject() instanceof WebSubject);
        if (!(context instanceof WebSubjectContext) || isNotBasedOnWebSubject) {
            return super.createSubject(context);
        }
        //獲取上下文物件中的資訊
        WebSubjectContext wsc = (WebSubjectContext) context;
        SecurityManager securityManager = wsc.resolveSecurityManager();
        Session session = wsc.resolveSession();
        boolean sessionEnabled = wsc.isSessionCreationEnabled();
        PrincipalCollection principals = wsc.resolvePrincipals();
        boolean authenticated = wsc.resolveAuthenticated();
        String host = wsc.resolveHost();
        ServletRequest request = wsc.resolveServletRequest();
        ServletResponse response = wsc.resolveServletResponse();

        //構造返回WebDelegatingSubject物件
        return new WebDelegatingSubject(principals, authenticated, host, session, sessionEnabled,
                request, response, securityManager);
    }

以上是Subject的建立過程,建立完成後我們還需要與當前請求執行緒進行繫結,這樣才能通過SecurityUtils.getSubject()方法獲取到Subject

二、Subject的繫結

Subject物件本質上是與請求所屬的執行緒進行繫結,Shiro底層定義了一個ThreadContext物件,一個基於ThreadLocal的上下文管理容器,裡面定義了一個InheritableThreadLocalMap<Map<Object, Object>>(),Subject最後就是被放到這個map當中,我們獲取時也是從這個map中獲取

首先我們看下繫結操作的入口,execuse是執行繫結,後續操作採用回撥機制來實現

         //執行Subject繫結
            //noinspection unchecked
            subject.execute(new Callable() {
                public Object call() throws Exception {
                    updateSessionLastAccessTime(request, response);
                    executeChain(request, response, chain);
                    return null;
                }
            });

 初始化一個SubjectCallable物件,並把回撥方法傳進去

    public <V> V execute(Callable<V> callable) throws ExecutionException {
        Callable<V> associated = associateWith(callable);//初始化一個SubjectCallable物件,並把回撥方法傳進去
        try {
            return associated.call();
        } catch (Throwable t) {
            throw new ExecutionException(t);
        }
    }


    public <V> Callable<V> associateWith(Callable<V> callable) {
        return new SubjectCallable<V>(this, callable);
    }

看下SubjectCallable類的具體實現

public class SubjectCallable<V> implements Callable<V> {

    protected final ThreadState threadState;
    private final Callable<V> callable;

    public SubjectCallable(Subject subject, Callable<V> delegate) {
        this(new SubjectThreadState(subject), delegate);//初始化構造方法
    }

    protected SubjectCallable(ThreadState threadState, Callable<V> delegate) {
        if (threadState == null) {
            throw new IllegalArgumentException("ThreadState argument cannot be null.");
        }
        this.threadState = threadState;//SubjectThreadState物件
        if (delegate == null) {
            throw new IllegalArgumentException("Callable delegate instance cannot be null.");
        }
        this.callable = delegate;//回撥物件
    }

    public V call() throws Exception {
        try {
            threadState.bind();//執行繫結操作
            return doCall(this.callable);//執行回撥操作
        } finally {
            threadState.restore();
        }
    }

    protected V doCall(Callable<V> target) throws Exception {
        return target.call();
    }
} 

具體繫結的操作是通過threadState.bind()來實現的

    public void bind() {
        SecurityManager securityManager = this.securityManager;
        if ( securityManager == null ) {
            //try just in case the constructor didn't find one at the time:
            securityManager = ThreadContext.getSecurityManager();
        }
        this.originalResources = ThreadContext.getResources();
        ThreadContext.remove();//首先執行remove操作

        ThreadContext.bind(this.subject);//執行繫結操作
        if (securityManager != null) {
            ThreadContext.bind(securityManager);
        }
    }

在上面bind方法中又會執行ThreadContext的bind方法,這裡就是之前說到的shiro底層維護了的一個ThreadContext物件,一個基於ThreadLocal的上下文管理容器,bind操作本質上就是把建立的Subject物件維護到resources 這個InheritableThreadLocalMap中, SecurityUtils.getSubject()方法其實就是從InheritableThreadLocalMap中獲取所屬執行緒對應的Subject

    private static final ThreadLocal<Map<Object, Object>> resources = new InheritableThreadLocalMap<Map<Object, Object>>();//定義一個InheritableThreadLocalMap

    public static void bind(Subject subject) {
        if (subject != null) {
            put(SUBJECT_KEY, subject);//向InheritableThreadLocalMap中放入Subject物件
        }
    }


    public static void put(Object key, Object value) {
        if (key == null) {
            throw new IllegalArgumentException("key cannot be null");
        }

        if (value == null) {
            remove(key);
            return;
        }

        ensureResourcesInitialized();
        resources.get().put(key, value);

        if (log.isTraceEnabled()) {
            String msg = "Bound value of type [" + value.getClass().getName() + "] for key [" +
                    key + "] to thread " + "[" + Thread.currentThread().getName() + "]";
            log.trace(msg);
        }
    }

 三、總結

從以上對Shiro原始碼的分析,我們對Subject物件的建立與繫結進行了基本的梳理,Subject物件的建立是通過不斷的對context上下文物件進行賦值與完善,並最終構造返回WebDelegatingSubject物件的過程;Subject物件建立後,會通過Shiro底層維護的一個基於ThreadLocal的上下文管理容器,即ThreadContext這個類,與請求所屬的執行緒進行繫結,供後續訪問使用。對Subject物件建立與繫結流程的分析,有助於理解Shiro底層的實現機制與方法,加深對Shiro的認識,從而在專案中能夠正確使用。希望本文對大家能有所幫助,其中如有不足與不正確的地方還望指出與海涵。

 

相關文章