Shiro源码分析(三)——获取Subject

本文详细分析了Apache Shiro框架中Subject的获取过程,通过SecurityUtils.getSubject()方法,涉及IniSecurityManagerFactory、SecurityManager、ThreadContext等关键组件。在Subject的创建过程中,包括了从 Ini 文件配置、SecurityManager 实例化、Subject 的构建(使用Builder模式)以及线程绑定等多个步骤,展示了Shiro如何在内部管理和维护Subject。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

2021SC@SDUSC

一.Demo代码

这篇文章将对上一篇未分析完的代码继续进行分析。

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.ini.IniSecurityManagerFactory;
import org.apache.shiro.lang.util.Factory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class MyQuickStart {

    // 使用工厂模式创建日志工具,方便打印日志。
    private static final transient Logger log = LoggerFactory.getLogger(Quickstart.class);

    public static void main(String[] args) {
        // 创建Shiro SecurityManager并配置realms, users, roles 和权限的最简单方式是通过INI文件。
        // 我们向一个工厂中传入.ini文件,然后工厂会返回一个SecurityManager实例。
        Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:myshiro.ini");
        SecurityManager securityManager = factory.getInstance();

        // 在这个简单的demo中,将SecurityManager作为一个JVM单例进行访问。
        // 大多数应用的代码不这么写,而是依赖他们的容器或是在web应用中的web.xml文件
        SecurityUtils.setSecurityManager(securityManager);

        // 获取当前正在操作的用户->1
        Subject currentUser = SecurityUtils.getSubject();
        ...
}

二.源码分析

本篇文章分析1.3、1.4,关于1.1、1.2的分析请阅读Shiro源码分析(二)。

Subject currentUser = SecurityUtils.getSubject();

1

public abstract class SecurityUtils {
...
	// 根据当前运行时环境返回对调用代码可用的可访问Subject
	// 这个方法将获取Subject的实现封装起来,而不依赖特定的其他方法。
	// 这样可以让Shiro开发团队在修改获取Subject代码时不影响调用该方法的用户。
	public static Subject getSubject() {
			// 调用ThreadContext的getSubject()静态方法获取当前线程绑定的Subject
        	Subject subject = ThreadContext.getSubject(); // 1.1
        	// 如果当前线程没有绑定Subject,则创建一个新的Subject然后进行绑定
        	if (subject == null) { 
            	subject = (new Subject.Builder()).buildSubject(); // 创建 Subject.Builder()->1.2 buildSubject()->1.3
            	ThreadContext.bind(subject); // 绑定 1.4
        	}
        	return subject;
    }
...
}

1.3

public interface Subject {
...
	// 创建并返回一个新的Subject实例,该实例反映了该类其他方法获取的累积状态
	// 该方法的调用不影响Builder实例的基本状态,即Builder实例不会被清除。多次调用该方法则会返回多个相同的Subject实例。
	// 想要创建不同的Subject实例必须创建新的Builder对象。
	// 需要注意的是返回的Subject对象不会自动绑定到当前应用(线程)来后续使用。也就是说SecurityUtils.getSubject()方法不会直接返回builder返回的对象。如果需要,由框架开发人员绑定返回的Subject以继续使用。
	public Subject buildSubject() {
        return this.securityManager.createSubject(this.subjectContext);
    }
...
}

public class DefaultSecurityManager extends SessionsSecurityManager {
...
	// 创建一个反映特定上下文数据的Subject实例
	// 上下文的内容可以是任何SecurityManager用于构造Subject实例的内容。大部分Shiro用户不会直接调用这个方法,这个方法主要用于框架的开发以及支持被SecurityManager所使用的的底层自定义SubjectFactory的实现。
	public Subject createSubject(SubjectContext subjectContext) {
		// 实现步骤:
		// 1.确保将SubjectContext尽可能地填充满,使用启发式的方法来获取可能尚未可用的数据,如被引用的session或被记住的主体(principal)
		// 2.调用doCreateSubject(SubjectContext)来真正进行创建Subject实例的工作。
		// 3.调用save(subject)来确保构造好的Subject的状态在必要的时候可供将来的请求或调用访问。
		// 4.返回构造好的Subject实例。
		
        // 创建一个副本,这样就可以无需修改参数中的支持映射了。
        SubjectContext context = copy(subjectContext); // 1.3.1

        // 确保上下文中含有一个SecurityManager实例,没有的话添加一个
        context = ensureSecurityManager(context); // 1.3.2
        
        // 解析相关联的Session(通常基于一个关联的session ID),然后在传给SubjectFactory前将其放入上下文中。SubjectFactory无需知道如何获取session,因为这个过程通常是根据具体环境决定的,这样可以更好地保护SubjectFactory来免受影响。
        context = resolveSession(context); // 1.3.3

        // 同样地,SubjectFactory不应该要求任何“记住我”的概念,如果可以的话现在这里翻译然后再递交给SubjectFactory
        context = resolvePrincipals(context); // 1.3.4

        Subject subject = doCreateSubject(context); // 1.3.5

        // 保存该subject以供后续使用
        // 这个步骤是必要的。因为一旦解析到“记住我”principal的话就需要在session中存储。
        save(subject);

        return subject;
    }
 ...
}
1.3.1
public class DefaultSecurityManager extends SessionsSecurityManager {
	// 复制一个SubjectContext实例
	protected SubjectContext copy(SubjectContext subjectContext) {
        return new DefaultSubjectContext(subjectContext);
    }
}
1.3.2
public class DefaultSecurityManager extends SessionsSecurityManager {
	// 查明上下文中是否存在SecurityManager实例,如果没有则将本类(DefaultSecurityManager是默认的SecurityManager)添加到上下文中
	// 这保证SubjectFactory实例在构造Subject期间可以访问SecurityManager
	protected SubjectContext ensureSecurityManager(SubjectContext context) {
        if (context.resolveSecurityManager() != null) {
            log.trace("Context already contains a SecurityManager instance.  Returning.");
            return context;
        }
        log.trace("No SecurityManager found in context.  Adding self reference.");
        context.setSecurityManager(this); // 1.2.3
        return context;
    }
}
1.3.3
public class DefaultSecurityManager extends SessionsSecurityManager {
	// 根据上下文尝试获取关联的session,然后将session解析为上下文后返回以确保用于真正构造Subject实例的SubjectFactory在必要时可以引用。
	protected SubjectContext resolveSession(SubjectContext context) {
		// 如果上下文当中已经有解析好的session,则直接返回
        if (context.resolveSession() != null) {
            log.debug("Context already contains a session.  Returning.");
            return context;
        }
        try {
            // 上下文无法直接解析它,但是可以尝试直接访问session管理器。
            Session session = resolveContextSession(context);
            // 如果session不为空,则直接设置Session
            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;
    }
	
	protected Session resolveContextSession(SubjectContext context) throws InvalidSessionException {
		// 获取SessionKey。SessionKey可以用于查找一个特定的Session实例
        SessionKey key = getSessionKey(context);  // 1.3.3.1
        if (key != null) {
        	// 若成功获取SessionKey,则根据该SessionKey获取对应的Session并返回
            return getSession(key);  // 1.3.3.2
        }
        return null;
    }
}
1.3.3.1
public class DefaultSecurityManager extends SessionsSecurityManager {
    protected SessionKey getSessionKey(SubjectContext context) {
    	// 获取SessionID
        Serializable sessionId = context.getSessionId();
        if (sessionId != null) {
        	// 根据获取到的SessionID创建默认的SessionKey
            return new DefaultSessionKey(sessionId);
        }
        return null;
    }
}

// DefaultSubjectContext类是SubjectContext接口的默认实现。
// 类中的getter和setter不是指向底层属性的简单传递方法,getter使用了多种启发式方法尽可能最好地获取其数据属性。
// 例如调用getPrincipals方法时,如果principal不在map中,它就会检查在map中的subject或session,查看是否能通过它们获取到principal对象
public class DefaultSubjectContext extends MapContext implements SubjectContext {
	public Serializable getSessionId() {
		// 调用父类MapContext的getTypedValue方法获取SessionID
        return getTypedValue(SESSION_ID, Serializable.class);
    }
}

// MapContext类为基于存放在Map中的上下文数据提供了公共基础。
// getTypedValue(String,Class)方法为子类提供类型安全的属性检索。
public class MapContext implements Map<String, Object>, Serializable {
	private final Map<String, Object> backingMap;

	// 执行get操作,可以确保返回的值为指定类型。如果没有值,则返回null。
	protected <E> E getTypedValue(String key, Class<E> type) {
        E found = null;
        Object o = backingMap.get(key);
        if (o != null) {
            if (!type.isAssignableFrom(o.getClass())) { // 类型检查
                String msg = "Invalid object found in SubjectContext Map under key [" + key + "].  Expected type " +
                        "was [" + type.getName() + "], but the object under that key is of type " +
                        "[" + o.getClass().getName() + "].";
                throw new IllegalArgumentException(msg);
            }
            found = (E) o; // 类型转换
        }
        return found;
    }
}
1.3.3.2
// Shiro支持SecurityManager类层次结构,将所有会话操作委托给包装的SessionManager实例。
// 即此类实现了SessionManager接口中的方法,但实际上这些方法只是对底层“真实”SessionManager实例的传递调用。
public abstract class SessionsSecurityManager extends AuthorizingSecurityManager {
	// 此安全管理器使用的内部委托SessionManager,用于管理应用程序的所有会话。
	private SessionManager sessionManager;

	// 根据SessionKey获取Session
	public Session getSession(SessionKey key) throws SessionException {
        return this.sessionManager.getSession(key);
    }
}
1.3.4
public class DefaultSecurityManager extends SessionsSecurityManager {
	// 尝试使用启发式方法解析上下文的标识(PrincipalCollection)。该实现功能如下:
	// 1.检查上下文,看看它是否已经可以解析标识。如果是,则这个方法什么都不做,并返回方法参数不变。
	// 2.通过调用getRememberedIdentity来检查一个RememberMe标识。如果该方法返回一个非空值,则将记住的PrincipalCollection放在上下文中。
	// 方法的参数是subject的上下文数据,可能会直接或间接地提供PrincipalCollection标识。
	protected SubjectContext resolvePrincipals(SubjectContext context) {
		// 首先通过上下文解析Principal
        PrincipalCollection principals = context.resolvePrincipals();

        if (isEmpty(principals)) { // 如果principals为空,则在记住的identity中检查
            log.trace("No identity (PrincipalCollection) found in the context.  Looking for a remembered identity.");

            principals = getRememberedIdentity(context);

            if (!isEmpty(principals)) { // 如果成功解析principal,则调用setPrincipals()方法将其设定到当前上下文中
                log.debug("Found remembered PrincipalCollection.  Adding to the context to be used " +
                        "for subject construction by the SubjectFactory.");

                context.setPrincipals(principals);
            } else {
                log.trace("No remembered identity found.  Returning original context.");
            }
        }

        return context;
    }
}

1.4

public abstract class ThreadContext {
    public static final String SUBJECT_KEY = ThreadContext.class.getName() + "_SUBJECT_KEY";
...
	// 便捷方法,简化了绑定Subject到ThreadContext的操作
	// 该方法的存在是为了减少代码中的强制类型转换,并简化对ThreadContext中键名的记忆。实现很简单,如果Subject不为空,它将其绑定到线程
	public static void bind(Subject subject) {
        if (subject != null) {
            put(SUBJECT_KEY, subject);
        }
    }
    
    // 将给定键的值绑定到当前线程。
    // 空值的效果与对给定键调用remove相同,
    public static void put(Object key, Object value) {
        if (key == null) {
            throw new IllegalArgumentException("key cannot be null");
        }

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

        ensureResourcesInitialized(); // 1.4.2
        // 获取当前线程的上下文数据并进行绑定
        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);
        }
    }
...
}
1.4.1
public abstract class ThreadContext {
...
	// 从当前线程中取消给定键的值(解除绑定)。
	public static Object remove(Object key) {
		// 获取当前线程的上下文数据
        Map<Object, Object> perThreadResources = resources.get();
        // 进行移除
        Object value = perThreadResources != null ? perThreadResources.remove(key) : null;

        if ((value != null) && log.isTraceEnabled()) {
            String msg = "Removed value of type [" + value.getClass().getName() + "] for key [" +
                    key + "]" + "from thread [" + Thread.currentThread().getName() + "]";
            log.trace(msg);
        }
        return value;
    }
...
}
1.4.2
public abstract class ThreadContext {
...
	// 确保当前上下文的资源已经被初始化
	private static void ensureResourcesInitialized(){
        if (resources.get() == null){
           resources.set(new HashMap<Object, Object>());
        }
    }
...
}

三.获取Subject的简易时序图

在这里插入图片描述
(完)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值