2021SC@SDUSC
一.Demo代码
Quickstart.java
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.ini.IniSecurityManagerFactory;
import org.apache.shiro.lang.util.Factory;
import org.apache.shiro.mgt.SecurityManager;
public class Quickstart {
public static void main(String[] args) {
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
System.exit(0);
}
}
shiro.ini
[users]
# 用户名:root 密码:secret 角色:admin
root = secret, admin
# 用户名:guest 密码:guest 角色:guest
guest = guest, guest
[roles]
# admin拥有全部权限,用通配符‘*’表示
admin = *
二.抽象工厂模式创建SecurityManager实例
从Shiro抽象工厂模式的UML图中可以看到,IniSecurityManagerFactory类继承了IniFactorySupport类。IniFactorySupport类对基于INI配置生成实例的工厂实现提供基本支持,它继承了AbstractFactory类,而AbstractFactory类是Factory接口的实现。
IniSecurityManagerFactory类是基于INI配置创建SecurityManager实例的工厂。IniSecurityManagerFactory类有三个构造方法,分别为无参、接收一个Ini对象、接收一个表示ini文件路径的字符串。demo代码中使用的是下图中第三个构造方法,即传入一个表示ini文件路径的字符串。
首先分析工厂的构造过程:
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:myshiro.ini");
向构造方法中传入表示ini文件路径的字符串后,涉及的调用如下:
- 调用Ini类的静态方法fromResourcePath(),并将路径传入该方法中。根据给定路径创建一个新的Ini实例,加载ini文件中的数据。资源路径必须是ResourceUtils.getInputStreamForPath方法可解释的值。结果返回一个Ini实例。
- 调用this()并传入fromResourcePath()返回的Ini对象。此时执行的是本类接收Ini对象作为参数的构造方法,它会使用this()调用本类的无参构造方法,再调用父类IniFactorySupport中的setIni()方法,并将接收到的Ini对象传进去,为父类中的私有Ini类型变量ini赋值。
- 调用本类无参构造方法时,会调用ReflectionBuilder类的无参构造方法,然后将返回的ReflectionBuilder对象赋值给类中的私有成员变量builder。ReflectionBuilder类是一个对象生成器。使用反射和Apache Commons BeanUtils来构造给定“属性-值”映射的对象。通常,它们来自Shiro INI配置,用于构造或修改SecurityManager、其依赖项和基于web的安全过滤器。
- IniSecurityManagerFactory类的构造方法执行完毕后,工厂被成功创建。
接下来调用工厂的getInstance()方法,得到SecurityManager对象。
SecurityManager securityManager = factory.getInstance();
getInstance()是IniSecurityManagerFactory的父类AbstractFactory的方法:
工厂生产的对象类型用泛型T表示,在这个例子中是SecurityManager。在创建实例前,首先通过isSingleton()判断是否为单例模式。若是,则会先判断是否存在这个实例,存在则直接将已有的实例赋值给局部变量instance,不存在再进行创建;否则直接创建一个所需实例。
在本例中,初始化IniSecurityManagerFactory类时会先初始化其所有父类,因此AbstractFactory的无参构造方法会被执行,将布尔型成员变量singleton赋值为true,表示此为单例模式。此时尚未有SecurityManager实例,因此会调用createInstance()方法进行创建。在AbstractFactory中,createInstance()是一个抽象方法,由其唯一子类IniFactorySupport进行实现。它会根据INI配置一个新的对象实例,其工作方式是解析INI文件,如果不存在,就会通过createDefaultInstance()方法创建并返回一个默认的实例。
createInstance()的实现方式如下:
- 调用本类的resolveIni()方法,通过本类的getIni()方法获取Ini对象,如果对象是null或者空,就会尝试查找并加载默认路径的Ini,都没找到就会返回null。
- 通过CollectionUtils.isEmpty()静态方法检测ini对象是否为空。如果为空则调用createDefaultInstance()方法创建默认实例,如果调用后仍没有创建成功,则抛出IllegalStateException异常,表明该类的实现没有在Ini配置类为空或null的情况下返回一个默认的实例,并说明这是Factory接口必须实现的,请检查。ini对象不为空则调用createInstance()方法,并将ini对象传入,没有创建成功则会抛出IllegalStateException异常,表明该类的实现没有通过createInstance(Ini)方法返回构造的实例。
在Demo中,ini对象不为空,会调用createInstance(Ini)方法创建实例。 - 带有参数的createInstance(Ini)方法和createDefaultInstance()方法在IniFactorySupport类中为抽象方法,在子类IniSecurityManagerFactory中实现。
createDefaultInstance()方法会调用DefaultSecurityManager类的无参构造方法,返回一个默认的SecurityManager实例。
createInstance(Ini)方法会先检查传入的Ini对象是否为空,是则抛出NullPointerException,否则继续执行,调用本类的createSecurityManager(Ini)方法,传入Ini对象。在该方法中调用它的重载createSecurityManager(Ini, Section)。
重载的方法首先会调用createDefaults(Ini, Section),然后调用getReflectionBuilder()获得在初始化这个类时创建好的ReflectionBuilder实例,再调用这个实例的setObjects()方法传入createDefaults(Ini, Section)返回的Map对象。
在createDefaults(Ini, Section)方法中,首先会创建一个LinkedHashMap并将其赋值给局部变量defaults,它可以理解为双向链表和HashMap的结合,实现了有序的迭代。然后调用createDefaultInstance()方法创建一个新的SecurityManager实例,创建完成后将其放入LinkedHashMap中。接着调用shouldImplicitlyCreateRealm(Ini)方法判断ini文件中是否包含账户数据以及是否需要隐式创建的Realm。若需要,则会调用createRealm(Ini)方法根据Ini实例中包含的账户数据创建一个Realm。
创建成功后则会将Realm的名称和其实例放入LinkedHashMap中。然后调用父类IniFactorySupport的getDefaults()方法获取用“字符串-bean”映射的工厂的默认对象集,工厂可以将这些bean与从INI配置解析的对象结合使用。接下来就是遍历defaultBeans将其全部放入defaults中然后返回,传递给ReflectionBuilder实例的setObjects(Map<String, ?>)方法。
接着就是调用buildInstances(Section)将各种对象绑定到SecurityManager上
然后通过getSecurityManagerBean()方法获取SecurityManager实例,准备对其中的Realm进行配置。
再调用isAutoApplyRealms(SecurityManager)判断是否自动应用realm。
由于在shiro.ini中指定了users,前面创建了IniRealm,因此该方法返回的值为true
然后调用getRealms()方法从objects中将Realm取出来放入集合中,判断集合中是否为空后调用applyRealmsToSecurityManager()将Realm和SecurityManager绑定,完成后返回创建好的SecurityManager。
时序图
得到SecurityManager对象后调用SecurityUtils的静态方法setSecurityManager()从而能够在后续代码中使用getSubject()实现。