Shiro预览
差不多半年之前就看到很多人在群里面讨论Shiro,由于各种原因,一直没有关注它。最近比较空闲,于是把官方的文档过一遍。所以本系列文章是根据我个人了解学习Shiro过程的笔记,可能文字上面比较粗略,望大家谅解。
做一件事,我一般遵循三个步骤:What,How,When。那么要首先需要知道Shiro是什么,以及如何使用,什么场景可以使用。
Shiro是什么
要说的简单一点,它就是一个安全框架。说的官方一点,Shiro是一个强大并且灵活的开源安全框架,并且它能够轻巧的支持企业级应用的认证,授权,会话管理以及加密。
通过上面的图可以看出,它拥有作为安全框架必备的四个主要功能(认证,授权,Session管理以及信息加密),关于图中各个模块就不一一介绍,因为官方都有,我这里值列举一个我比较感兴趣的:
为什么下面还有一个Web Support呢?其实Shiro的核心内部并不是针对WEB应用而设计的安全控制,而是将核心模块抽离出来,从而可以基于核心模块很容易扩展以嵌入到所有类型应用中,比如桌面端的应用权限控制,命令行应用权限控制等等,这些Shiro都可以很容器的嵌入其中。基于这个特性,我们可以很容器将Shrio包装成复合我们应用所需要的安全组件。这很容易看出Shrio是一个微内核的框架,外部可以很容器对其进行扩展,当然,扩展的前提是必须熟悉Shrio。
Shiro初步窥探
###Hello World### Hello World!这可以说是编程里面永不灭的神话。这里面可以看到整体,也可以看到局部细节。至于怎么看到这些,那就是仁者见仁,智者见智。先不多说,先上一个Hellow World代码。
<!--lang:java-->
public static void main(String[] args) {
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
Subject currentUser = SecurityUtils.getSubject();
Session session = currentUser.getSession();
session.setAttribute("someKey", "aValue");
String value = (String) session.getAttribute("someKey");
if (value.equals("aValue")) {
log.info("Retrieved the correct value! [" + value + "]");
}
if (!currentUser.isAuthenticated()) {
UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
token.setRememberMe(true);
try {
currentUser.login(token);
} catch (UnknownAccountException uae) {
log.info("There is no user with username of " + token.getPrincipal());
} catch (IncorrectCredentialsException ice) {
log.info("Password for account " + token.getPrincipal() + " was incorrect!");
} catch (LockedAccountException lae) {
log.info("The account for username " + token.getPrincipal() + " is locked. " +
"Please contact your administrator to unlock it.");
}catch (AuthenticationException ae) {
//unexpected condition? error?
}
}
log.info("User [" + currentUser.getPrincipal() + "] logged in successfully.");
currentUser.logout();
System.exit(0);
}
上面代码则完成了用户的安全认证,这里没有像Spring Security里面一大堆的Filter(我表示至今我都被那些Filter给弄得云里雾里),上面代码接口如此清晰,一上来就是获取安全管理器,这基本上是所有安全框架都具备的,后面紧接着的是一个静态方法,将安全管理器注入到SecurityUtils
里面,SecurityUtils
是Shrio的最核心的API,它是安全认证的入口,之后看到SecurityUtils.getSubject()
,这里会去获取一个Subject(可以简单理解一个用户,后面会对其进行介绍)。这里我有一个问题,用户都没登录,哪来的Subject
?我们不放到这个方法里面去看看到底做了什么:
<!--lang:java-->
public static Subject getSubject() {
Subject subject = ThreadContext.getSubject();
if (subject == null) {
subject = (new Subject.Builder()).buildSubject();
ThreadContext.bind(subject);
}
return subject;
}
看到这个,相信不少人都明白了。ThreadContext
就是一个ThreadLocal
,如果当前线程是第一次获取,会通过Subject.Builder
来构造一个Subject
,后续直接从线程变量获取即可,那么问题又来了,用户不可能一直在一个线程上面操作,用户发起不同的请求,会有不同的线程,那不是用户没法发起一个请求Subject.Builder
都需要构造一个新的Subject吗?我们带着这个问题,不放看看Subject.Builder.buildSubject
做了什么。
<!--lang:java-->
public Subject buildSubject() {
return this.securityManager.createSubject(this.subjectContext);
}
发现,其实创建Subject
的并不是Subject.Builder
,而是SecurityManager
,到这里,我就暂时不往下跟了。因为SecurityManager
是一个接口,Shiro会根据不同的场景提供不同的安全管理器,比如我们会将用户的登录信息存储在Redis里面,这个时候可能需要使用和Redis相关的安全管理器,具体的安全管理器会提供用户登录信息具体存储在哪里。暂时将SecurityManager
理解为一个黑盒子,它能记录当前操作用户的登录信息。
继续回到Hello world,得到Subject
之后,获取当前用户的Session对象,这里的Session也是一个接口,因为不同的应用环境其Session格式不同,比如Web应用,Session则是HttpServletSession,而不是Web的引用,它的Session可能是另外一种形式,这里Shiro进行了封装,以提供对外的统一接口,屏蔽具体实现差异。这对开发者来说,使用非常方便。说到这里,我也不防继续YY一下,由于Shiro将安全底层的API都进行了封装,那我们是否可以想想一下,开发一个统一的安全认证组件,可以随意的嵌入各种类型的应用中去,比如我有一个应用,它提供WEB端和桌面端,其中都包含安全认证模块,由于它们两的运行环境不一样,所以我们需要开发两套安全认证的组件,但是如果用Shiro,我们只需要开发一套,只需要将上面的SecurityManager
进行替换成对应的应用环境的实现即可,想想是不是很激动(虽然其他方式也可以实现,但是这里是在讲Shiro,当然尽量往这边靠)。
又废话了不少,继续Hello World。之后的代码就是进行触发用户认证了,通过UsernamePasswordToken
对用户名和密码进行包装,在触发Subject
的登录操作。到此,基本上那个完成了用户的认证操作。
是不是很简单明了,比一大堆的Filter简单多了。虽然里面废话了不少。
###Overview Architecture###
上面知道了Shiro认证的相关API,下面简单聊一下Shiro的架构吧。话不多说,下上一下官方的图:
图里面亮出了Shiro的三大法宝,针对这三大法宝,下面做个简单的介绍:
- Subject:上面的Hello world已经介绍了,他是一个用户信息的载体,官方的描述是:Subject是用户的一个视图。可以透过它观察用户的认证信息以及状态,同时它也屏蔽了各种应用场景用户的差异性,使得结构和代码接口清晰了不少。
- SecurityManager:它是Shiro存在的目的,没有它,我觉得Shiro也就没必要存在了,为什么?因为Shiro就是干这个事情的。没有它,不就等于,要饭的砸了饭碗,写代码的没收了电脑吗?官方的说法是:它就是一把保护伞。同样Shiro对
SecurityManager
的设计以遵循了统一化的方式,将不同应用存在不同的安全管理抽象统一其接口,提供对外一致的调用。而具体实现细节,就交给Shiro吧。 - Realm:一开始,我还不知道这个单词是啥意思,百度翻译了一下,是“领域”的意思。这个概念有点抽象,什么是“领域”,我表达一下我的理解:就是不同业务有自己的特有的领域,那么这里的“领域”可以这样来理解,不同安全认证的业务有自己的领域,此话怎讲?比如说,我的系统安全认证是基于LDAP的,他的系统安全认证是基于缓存的,另一个系统的安全认证是基于数据库的,等等。这里的“基于”是什么意思?我的理解是用户以及权限信息存储介质。这就不难理解
Realm
,它其实就是抽离出了各个系统可能存在获取用户以及权限信息的方式不同。简单一点,它就是获取用户以及权限数据的组件。于是Shiro就提供了常用的实现,比如JDBCRealm,LDAPRealm以及上面Hello world的IniRealm等等,你可以基于你自己系统的需要,实现自己的Realm,并且将其注入到Shiro中。
上面上了一个简单一点的图,现在来个高大上一点的:
图上面能够看到的,我就不多嘴了。这里介绍三点:
Session DAO:看到这个,我就豁然开朗了。这个我就可以实现分布式的Session的同步,从而实现单点登录,这个在框架内部就提供了支持,这对开发来说,是一个福音。相对于之前在Spring security上来做单点登录,现在有种释然的感觉。在Shiro中提供了
SessionDao
接口,只需要实现复合自己系统Session持久化,再将这个实现注入到Shiro中即可。Realm:还是讲讲
Realm
,现在不讲它是干什么用的,讲讲Shiro是怎么用Realm
的。看到图中的Pluggable Realms(1 or more)。这句话是什么意思?难道Shiro支持多种方式获取用户以及权限数据?对的!Shiro就是这么强大,就是这么NB。我们可以向Shiro中注入多种Realm的实现,从而可以从多个“领域”获取用户以及权限信息。但是问题来了?假如我向Shiro中注入了多个Realm
的实现,Shiro怎么对权限校验的?有些系统是只要多个Realm
中一个即可,有可能是其中某一个,也有可能是全部。这些Shiro怎么做呢?为了解决这个问题,就有了下面一点。AuthenticationStrategy:这个是干嘛的?看到后面一个单词
Strategy
就应该是做决策用的,再加上前面Authentication
,就应该知道是做认证策略用的。上面说了,Shiro可以配置多个Realm
也可以是一个。当只配置一个时,AuthenticationStrategy
直接调用这个唯一的Realm
获取数据即可,如果是多个的时候,这个就需要配置具体是哪种策略了。我们稍微深入一点了解Shiro内部。在SecurityManager
有一个authenticator
制定是哪个认证器来完成当前安全管理器的安全认证。Shiro中抽象出一个认证器的接口Authenticator
,同时在Shiro中只提供了一个实现ModularRealmAuthenticator
,这里面会归集所有注入Shiro中的Realm
然后通过具体的AuthenticationStrategy
实现来进行认证。在ModularRealmAuthenticator
主要提供两个方法doSingleRealmAuthentication(Realm realm, AuthenticationToken token)
和doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token)
,Shiro会根据当前系统有是多个Realm还是只有一个Realm,如果是多个,调用doSingleRealmAuthentication
否则调用doMultiRealmAuthentication
,在doMultiRealmAuthentication
方法中则涉及到校验策略,默认策略是AtLeastOneSuccessfulStrategy
,即使用最后一个Realm。除了这个策略之外,Shiro还提供了AllSuccessfulStrategy
和FirstSuccessfulStrategy
,基于这个,可以实现自己的认证策略,将其注入到Shiro中即可。下面贴出doSingleRealmAuthentication
和doMultiRealmAuthentication
方法的具体实现,以达到更加深入的理解Shiro在这里的实现机制:
doSingleRealmAuthentication方法实现:
<!--lang:java-->
protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) {
if (!realm.supports(token)) {
String msg = "Realm [" + realm + "] does not support authentication token [" +
token + "]. Please ensure that the appropriate Realm implementation is " +
"configured correctly or that the realm accepts AuthenticationTokens of this type.";
throw new UnsupportedTokenException(msg);
}
AuthenticationInfo info = realm.getAuthenticationInfo(token);
if (info == null) {
String msg = "Realm [" + realm + "] was unable to find account data for the " +
"submitted AuthenticationToken [" + token + "].";
throw new UnknownAccountException(msg);
}
return info;
}
doMultiRealmAuthentication方法源码
<!--lang:java-->
protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) {
AuthenticationStrategy strategy = getAuthenticationStrategy();
AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token);
if (log.isTraceEnabled()) {
log.trace("Iterating through {} realms for PAM authentication", realms.size());
}
for (Realm realm : realms) {
aggregate = strategy.beforeAttempt(realm, token, aggregate);
if (realm.supports(token)) {
log.trace("Attempting to authenticate token [{}] using realm [{}]", token, realm);
AuthenticationInfo info = null;
Throwable t = null;
try {
info = realm.getAuthenticationInfo(token);
} catch (Throwable throwable) {
t = throwable;
if (log.isDebugEnabled()) {
String msg = "Realm [" + realm + "] threw an exception during a multi-realm authentication attempt:";
log.debug(msg, t);
}
}
aggregate = strategy.afterAttempt(realm, token, info, aggregate, t);
} else {
log.debug("Realm [{}] does not support token {}. Skipping realm.", realm, token);
}
}
aggregate = strategy.afterAllAttempts(token, aggregate);
return aggregate;}
上面对Shiro的整体架构废话了不少,里面也扯到了一下部分实现。下面将介绍项目中的Shiro如何配置。
##如何配置Shiro##
我对配置的理解,其实就是一个驯化的过程。Shiro提供了很多功能,如何让Shiro更好的服务系统,便捷的配置风格必不可少。Shiro核心模块支持两种配置,一种是编程风格的配置,另一种是通过INI文件进行配置。
###编程风格配置### 这种风格是通过手动在代码中通过代码的风格配置Shiro,通过调用Shiro各个模块的set方法来进行设置属性。具体例子如下:
<!--lang:java-->
DefaultSecurityManager securityManager = new DefaultSecurityManager(realm);
SessionDAO sessionDAO = new CustomSessionDAO();
((DefaultSessionManager)securityManager.getSessionManager()).setSessionDAO(sessionDAO);
关于这种风格的配置我就不再废话了,其实就是多写些代码。
###INI文件配置### 关于INI文件规范可以百度或者谷歌一下。在Shiro中如何引入INI配置,其实在Hello world中已经列举出来了,通过IniSecurityManagerFactory
来加载INI配置文件,从而初始化安全管理器。下面对在Shiro中怎么用INI进行配置。
在Shiro中对INI配置文件分成几个块,分别是[main],[users],[roles]以及[urls]。下面对这几个块的配置规则进行叙述一下: ####[main]#### 这部分主要是配置Shiro中的各个组件,比如上面说的Realm
,AuthenticationStrategy
,加密算法的配置等等,整体风格类似OGNL的风格将配置值注入到类的属性中去,具体实例如下:
[main]sha256Matcher = org.apache.shiro.authc.credential.Sha256CredentialsMatcher myRealm = com.company.security.shiro.DatabaseRealm myRealm.connectionTimeout = 30000 myRealm.username = jsmith myRealm.password = secret myRealm.credentialsMatcher = $sha256Matcher securityManager.sessionManager.globalSessionTimeout = 1800000 object1 = com.company.some.Class object2 = com.company.another.Class ... anObject = some.class.with.a.Map.property anObject.mapProperty = key1:$object1, key2:$object2//Map注入 sessionListener1 = com.company.my.SessionListenerImplementation ... sessionListener2 = com.company.my.other.SessionListenerImplementation ... securityManager.sessionManager.sessionListeners = $sessionListener1, $sessionListener2//注入LIST集合
Shiro是通过Apache的Beanutils工具来将这些配置想注入到对象的属性中。注意,上面关于对象名的引用是存在顺序的,需要应用的对象必须申明在引用之前,不然会找不到对象。比如object1
必须在anObject.mapProperty = key1:$object1, key2:$object2
之前。
####[users]#### 该部分配置的是用户信息,格式如下:
username=password[,role1,role2]
从上面可以理解为在配置用户的时候制定该用户所拥有的哪些角色。
####[roles]#### 该部分是配置角色和资源之间的关系,格式如下:
rolename=resource[:otherresource]
其中resource
可以是通配符*
表示所有资源的意思,而通过:
来分割资源,表示的是层级关系,类似大功能下面的小功能。
这里就简单的介绍了一下Shiro的基本配置。如果和Spring集成,还通过Spring风格的配置,其实上面配置User,role在开发过程中基本很少使用,因为大部分场景用户和角色以及角色和资源的关系是存储在数据库或者其他存储介质中的。针对这些我们需要怎么改变?就是通过上面的Realm
来达到目的。
##总结## 到此,Shiro整体的基本情况已经介绍的差不多了,其中知道Shiro提供了很多接口,比如Realm
,SecurityManager
,AuthenticationStrategy
以及SessionDao
等,以满足围绕Shiro核心开发出复核我们需要的安全组件。