Shiro源码研究之构建Subject实例

原文地址:https://blog.csdn.net/lqzkcx3/article/details/78801403

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。这里我放上一张相关堆栈图

 

 

 

参考本人在简书上的《如何加速源码利理解速度》一文,可以得出不少信息:
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);
    }
}
// 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会启用哪些组件,以及是怎样的方式来完成权限认证的。

7. SecurityUtils.getSubject()方法

如果说Shiro被调用者最常接触到的,应该就是这个方法了。所以在本文的最后我们顺势来看看这方法。

 

posted @ 2020-12-10 16:53  八方鱼  阅读(38)  评论(0编辑  收藏  举报