您的位置:首页 > 其它

安全认证框架Shiro (一)- ini配置文件

2017-04-22 22:31 357 查看
我不是语言的开发者,我只是它的搬运工。每进步一点,5年之后你也是个人物

为什么看网上的例子都喜欢用ini格式文件,为什么不用.propertes或xml。

我们来看看一个ini格式文件text.ini:

[main]
activeDirectoryRealm = org.apache.shiro.realm.activedirectory.ActiveDirectoryRealm
activeDirectoryRealm.systemUsername = uid=admin,ou=system
activeDirectoryRealm.systemPassword = secret
activeDirectoryRealm.searchBase = o=sevenSeas,ou=people
activeDirectoryRealm.url = ldap://localhost:10389
[users]
name=cy
pwd=123

看了源码才知道,原来shiro框架里新造了一个Ini类,当我们传入资源时,Ini里使用流一行一行的读资源,当遇到”#”或”;”开头的则直接跳过;

遇到“[*]”则将中括号里的字符串看过Section(区块)的key,后面一行一行都视做该区域的内容直到遇到新的中括号。随后再解读区域下面多行字符串(至少一行),如果遇到“:”或“=”或“”,则前面当做key,后面的则是为value(同时会过滤掉value里前后空格以及“=”前后空格),存到一个Section里,最后把所有行解析完后放到名为sections的HashMap里。

IniSecurityManagerFacotry继承自IniFactorySupport,而IniFactorySupport有个setIni()方法将解析出来的Ini结构数据保存到该类里,其它什么都不做。

1.[users]部分

#提供了对用户/密码及其角色的配置,用户名=密码,角色1,角色2 

username=password,role1,role2

例如:

配置用户名/密码及其角色,格式:“用户名=密码,角色1,角色2”,角色部分可省略。如:

[users] 

zhang=123,role1,role2 

wang=123    

 

2. [roles] 

#提供了角色及权限之间关系的配置,角色=权限1,权限2 

role1=permission1,permission2

例如:

配置角色及权限之间的关系,格式:“角色=权限1,权限2”;如:

[roles] 

role1=user:create,user:update 

role2=*  

如果只有角色没有对应的权限,可以不配roles

 

3. [main]部分

提供了对根对象securityManager及其依赖对象的配置。

创建对象

securityManager=org.apache.shiro.mgt.DefaultSecurityManager 

其构造器必须是public空参构造器,通过反射创建相应的实例。

1、对象名=全限定类名  相对于调用public无参构造器创建对象
2、对象名.属性名=值   相当于调用setter方法设置常量值
3、对象名.属性名=$对象引用   相当于调用setter方法设置对象引用
 

 

4.[urls] 

#用于web,提供了对web url拦截相关的配置,url=拦截器[参数],拦截器 

/index.html = anon 

/admin/** = authc, roles[admin],perms["permission1"]

 

5.非标签。不同种类数据注入方式

  5.1 Map setter注入

即格式是:map=key:value,key:value,可以注入常量及引用值,常量的话都看作字符串

例如:

authenticator.map=$jdbcRealm:$jdbcRealm,1:1,key:abc

 

  5.2Array/Set/List setter注入 

多个之间通过“,”分割。

例如:

authenticator.array=1,2,3 

authenticator.set=$jdbcRealm,$jdbcRealm

 

5.3嵌套属性setter注入 

例如:

securityManager.authenticator.authenticationStrategy=$authenticationStrategy

 

5.4对象引用setter注入

authenticator=org.apache.shiro.authc.pam.ModularRealmAuthenticator 

securityManager.authenticator=$authenticator

 

 5.5创建对象

其构造器必须是public空参构造器,通过反射创建相应的实例

securityManager=org.apache.shiro.mgt.DefaultSecurityManager

源码解析:

当配置文件里出现[users]或[roles]时,IniSecurityManagerFacotry会初始化一个IniRealm做为数据源,把ini传入到IniRealm里,IniRealm的name是“iniRealm”。

protected Realm createRealm(Ini ini) {
//IniRealm realm = new IniRealm(ini); changed to support SHIRO-322
IniRealm realm = new IniRealm();
realm.setName(INI_REALM_NAME);
realm.setIni(ini); //added for SHIRO-322
return realm;
  }

并把realm存到securityManager的realms属性集合里。

当出现[main]时,说明是主配置。看下面的解析图:

public Map<String, ?> buildObjects(Map<String, String> kvPairs) {
if (kvPairs != null && !kvPairs.isEmpty()) {

// Separate key value pairs into object declarations and property assignment
// so that all objects can be created up front

//https://issues.apache.org/jira/browse/SHIRO-85 - need to use LinkedHashMaps here:
Map<String, String> instanceMap = new LinkedHashMap<String, String>();
Map<String, String> propertyMap = new LinkedHashMap<String, String>();

for (Map.Entry<String, String> entry : kvPairs.entrySet()) {
//不出现“.”或者以“.class”结尾
if (entry.getKey().indexOf('.') < 0 || entry.getKey().endsWith(".class")) {
instanceMap.put(entry.getKey(), entry.getValue());
} else {
propertyMap.put(entry.getKey(), entry.getValue());
}
}

// Create all instances
for (Map.Entry<String, String> entry : instanceMap.entrySet()) {
createNewInstance((Map<String, Object>) objects, entry.getKey(), entry.getValue());
}

// Set all properties
for (Map.Entry<String, String> entry : propertyMap.entrySet()) {
applyProperty(entry.getKey(), entry.getValue(), objects);
}
}

//SHIRO-413: init method must be called for constructed objects that are Initializable
LifecycleUtils.init(objects.values());

return objects;
}

这里当key里不出现“.”或者以“.class”结尾,说明是需要实例化的类,value值即为类的全限名,这些实例最张会被反射注入到DefaultSecurityManager的实例securityManager里。否则视为属性,用反射去设置上次实例化的对象属性值。其中objects是包含着key是“securityManager”,value为DefaultSecurityManager对象的Map对象。所有被main标记的都会被注入到securityManager”。

当不出现“[]”时,“”空即为sections的key。只要第一行没出现”[]”则一定会出现key为空的map的键值对。(注意一点,如果没有[main],则取sections里””即空为的key的数据做为主配置)

 

当我们调用IniSecurityManagerFacotry里getInstance()方法时,会根据是否有ini数据来调用不同的方法创建不同的SecurityManager.当有ini时调用

protected SecurityManager createInstance(Ini ini) {
if (CollectionUtils.isEmpty(ini)) {
throw new NullPointerException("Ini argument cannot be null or empty.");
}
//createSecurityManager()才是重点
SecurityManager securityManager = createSecurityManager(ini);
if (securityManager == null) {
String msg = SecurityManager.class + " instance cannot be null.";
throw new ConfigurationException(msg);
}
return securityManager;
  }

当没有时调用:
protected SecurityManager createDefaultInstance() {
return new DefaultSecurityManager();
}


一般将realm标记为[main],那么会生成Realm的实例,保存到DefaultSecurityManager的realms集合里,这样securityManager就有数据源了。

再看看网上常用的用shiro的API的例子:
Factory<org.apache.shiro.mgt.SecurityManager> factory =
new IniSecurityManagerFactory("auth.ini");
// Setting up the SecurityManager...
org.apache.shiro.mgt.SecurityManager securityManager
= factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
Subject user = SecurityUtils.getSubject();

前面两句很容易理解,就是把配置文件里配置的参数放到Ini里,并把ini传给Realm实例,同时造一个DefaultSecurityManager实例。那后面两句如何理解呢,从字面上其实很容易看出来,将securityManager放到SecurityUtils里,同时从SecurityUtils里取Subject。Subject相当于当前线程里的相当“用户”,体现在程序里即是保存用户相关身份和凭证等的信息,以及操作方法。

看看SecurityUtils的setSecurityManager()代码:

public static void setSecurityManager(SecurityManager securityManager) {
SecurityUtils.securityManager = securityManager;
}

和getSubject():

public static Subject getSubject() {
//从线程的上下文环境里取Subject,如果有就返回,没有则创建一个Subect返回并绑定到ThreadContext上。
Subject subject = ThreadContext.getSubject();
if (subject == null) {
subject = (new Subject.Builder()).buildSubject();
ThreadContext.bind(subject);
}
return subject;
}

再沿深一个SecurityUtils,其实里面只操作了Subject和SecurityManager.



如果我们跳出Ini配置的束缚,我们应该能得到结论,我们应该给SecurityManager提供一个或多个Realm对象在到realms里,比如在spring框架的xml里配,SecurityUtils作用把Subject和SecurityManager关联起来了,只要能把当前用户信息(Subject)和SecurityManager(包含验证的数据来源信息等配置)搭上话,那么要么我们自己用API做事,要么Spring帮我们管理都很方便。

 

6.断点分析

我们以如下的配置文件为便说明怎么解析的:

[main]
jdbcRealm=org.apache.shiro.realm.jdbc.JdbcRealm
dataSource=com.alibaba.druid.pool.DruidDataSource
dataSource.driverClassName=com.mysql.jdbc.Driver
dataSource.url=jdbc:mysql://localhost:3306/shiro
dataSource.username=root
jdbcRealm.dataSource=$dataSource

[users]
zhang=123,role1,role2
wang=123

[roles]
#对资源user拥有create、update权限
role1=user:create,user:update
#对资源user拥有create、delete权限
role2=user
de5c
:create,user:delete

[urls]
/index.html = anon
/admin/** = authc, roles[admin], perms["permission1"]
1.


当执行到Ini里的load()方法里时。读取到main块区域出的多行,看到sectionContext内容是”[main]”标签下的多行内容,直到遇到新的标签为止。

2.


在addSection()将上面的sectionContext的内容解析放到Section里,看红色标注的内容,其实Section里用LinkedHaspMap保存Key-value。同时也会将”[users]”,”[roles]”,”[urls]”解析到Section里,并用key-value保存用“=”设置的读,section就是标签名如“main”,最后所以的section会保存到Ini里的sections里,该类是LinkedHaspMap类型。

users标签处理:



SimpleAccount结构体保存users标签里数据并最终放到SimpleAccountRealm的users字典里。

到此解析ini文件已经结束。

再看看配置的数据怎么用的。iniSecurityManagerFactory.getInstance() 方法里最重要的是createSecurityManager()方法,看看这个方法的信息:



第一行createDefaults();返回的Map里包含了DefaultSecurityManager和IniRealm,其中IniRealm是包含ini配置文件的Realm。

第二行buildInstances()是将ini配置文件里的类实例化属性反射set。

结里如下图:



是不是和配置文件里一样:

[main]
jdbcRealm=org.apache.shiro.realm.jdbc.JdbcRealm
dataSource=com.alibaba.druid.pool.DruidDataSource
dataSource.driverClassName=com.mysql.jdbc.Driver
dataSource.url=jdbc:mysql://localhost:3306/shiro
dataSource.username=root
jdbcRealm.dataSource=$dataSource

下面确定是否SecurityManager实例里realms是否有数据,如果没有则遍历上图里所以实例,如果是Realm接口的子类则将该实例放入realms里,同时SecurityManager实例里的authenticator验证器如果是ModularRealmAuthenticator(默认是ModularRealmAuthenticator验证器)则也将realms数据放入authenticator里。可见authenticator已经拿到了数据源,如果我们做验证的话,authenticator会从realm里拿数据做比较。

至些iniSecurityManagerFactory.getInstance()方法执行完成,主要逻辑就是把main标记的类实例话,将属性设置到类上,且最终实例话的类设置到securityManager的属性里并返回instancee。其它什么也不做,roles,users,urls不会在此处理。

SecurityUtils.setSecurityManager(securityManager)方法没什么逻辑,只是set方法,不值得跟踪。

Subjectsubject = SecurityUtils.getSubject():从当前线程上下文里取,如果没有则创建一个并缓存起来。

重点看subject. login(token),token会传到authenticator里,看看数据:



在验证器里的doAuthenticate方法里取上次赋值的2个realm实例。根据数量选择不同方法,我们有2个,所以执行doMultiRealmAuthentication()方法。在doMultiRealmAuthentication()会遍历realms里所以realm对象,并将相关数据传入realm里。

先看IniRealm:



先看有没有缓存,如果有则说明已经验证过了,直接跳过,如果没有则去执行doGetAuthenticationInfo()方法。



如果查询到信息则返回tokenInfo,取得的TokenInfo与token在assertCredentialsMatch方法里验证是否匹配。



匹配器里方法做最终凭证匹配,如果相同则返回true,否则为false.



验证成功后会一层层返回,

1.会通知该验证器所以的监听器



2.  创建SubjectContext,缓存一些数据,并保存到Subject。下次验证时先从缓存数据里取,如果验证成功则无需再做验证。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: