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的简易时序图
(完)