也聊shiro

最近小小地对系统升级到二维码登录和提供OAuth2.0API接口,这些都涉及到了安全账户模块,加上传统的登录授权共同支撑现在的账户安全体系,以往的项目中用SpringSecret来解决这一块功能,原于这块内容相对门槛较高,在后来的一系列系统中统一使用了家喻户晓的Shiro来实现系统的账户安全认证框架.

Shiro框架在网上的文章铺天盖地,各种翻译的,转载的大都没营养价值,最难得的还是开涛的《跟我学Shiro》系列文章,深入人心,营养满满,我也受益匪浅,实际开发过程中的场景大部分都能包含并简单支持,我就从实际场景出发,跳过基础的学习过程,分别从源码和业务两个角度来补充Shiro的一些概念和使用。

Shiro应用场景的部分源码:https://github.com/xuminwlt/j360-shiro 欢迎Star,会不定期更新新的场景和功能

开涛的《跟我学Shiro》:http://jinnianshilongnian.iteye.com/blog/2018398

Shiro依赖

Shiro四大模块基本上涵盖了Java安全领域的知识,javax.secret中关于安全领域按照面向对象的概念提出的一些类在Shiro里面都能找得到,所以对于Shiro的了解程度很大程度上也能对java原生的安全支持有一定的了解帮助作用。

  • 认证
  • 授权
  • 会话
  • 加密

Shiro框架的依赖比较多,分别在不同的场景下通过简单的配置方式配合使用,减少了很多自己去开发时间,即提供了架构上的支持还提供了部分业务上的满足。实际场景下也可以自己去实现这些功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-lang</artifactId>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-config-core</artifactId>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-config-ogdl</artifactId>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-crypto-core</artifactId>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-crypto-hash</artifactId>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-crypto-cipher</artifactId>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-event</artifactId>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-aspectj</artifactId>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-guice</artifactId>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-hazelcast</artifactId>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-quartz</artifactId>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
</dependency>

平常使用的比较核心依赖有,其他的依赖按需添加,并不影响完整的Shiro安全生命周期使用。

  • core 核心内容
  • web web环境支持
  • spring spring环境支持

源码

从源码上理解Shiro可以从Shiro在容器中的生命周期去理解,Shiro在Web环境下多了一层非web环境下的容器的概念,也就是Shiro的生命周期会依赖容器的声明周期,在使用场景上和配置打交道的是环境准备阶段,和真正的代码开发一块是请求实例环节,所以这里主要从这2个方面理解源码。

环境准备

环境准备是整个Shiro生命周期的开始,也就是Shiro的入口,Shiro配置使用Spring的Bean生命周期来初始化整个Shiro环境,在Servlet容器中并且在Spring环境下统一使用代理Filter并声明成和Bean命名统一的名称来实现关联。在Spring boot中使用java config方式相似,但是有一点点区别。

web.xml片段

1
2
3
4
5
6
7
8
9
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<async-supported>true</async-supported>
<init-param>
<param-name>targetFilterLifecycle</param-name>
<param-value>true</param-value>
</init-param>
</filter>

spring.xml片段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!-- Shiro的Web过滤器 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<property name="loginUrl" value="/login.jsp"/>
<property name="unauthorizedUrl" value="/unauthorized.jsp"/>
<property name="filters">
<util:map>
<entry key="authc" value-ref="formAuthenticationFilter"/>
</util:map>
</property>
<property name="filterChainDefinitions">
<value>
/index.jsp = anon
/unauthorized.jsp = anon
/login.jsp = authc
/logout = logout
/** = user
</value>
</property>
</bean>

Spring环境下的Shiro相关的类的声明周期使用了继承了Spring BeanProcessor的子类实现,通过实现Initializable和Destroyable接口实现接口的回调,在这里只需要声明出这个Bean即可。

1
2
3
<!-- Shiro生命周期处理器-->
<bean id="lifecycleBeanPostProcessor"
class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>

通过源码可以看到AuthorizingRealm和CachingSecurityManager的继承关系正是使用了这个接口,所有继承这两个类的子类也都在lifecycleBeanPostProcessor的处理方式之内。

1
2
public abstract class AuthorizingRealm extends AuthenticatingRealm
implements Authorizer, Initializable, PermissionResolverAware, RolePermissionResolverAware {
1
public abstract class CachingSecurityManager implements SecurityManager, Destroyable, CacheManagerAware {

在整个Shiro生命周期中,安全管理器是整个Shiro安全框架的核心,在ShiroFilter拦截器中,就作为一个成员赋值到了其中.

1
2
3
4
5
6
7
8
<!-- 安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.mgt.DefaultSecurityManager">
<property name="realms">
<list><ref bean="userRealm"/></list>
</property>
<property name="sessionManager" ref="sessionManager"/>
<property name="cacheManager" ref="cacheManager"/>
</bean>

安全管理器包含了整个安全管理框架(除特定情况的)配置信息,Shiro提供了web环境下和非web环境下的安全管理器,分别是DefaultWebSecurityManager,DefaultSecurityManager,前者继承了后者,同时在sessionManager属性上也分别对应了DefaultWebSessionManager,DefaultSessionManager。这里重点提一下Web环境下的SessionManager,Shiro提供了多个场景下的会话管理器,这些场景可以通过下一章节去理解。这里使用默认会话管理器。

DefaultSecurityManager下面有两个个非常重要的属性realms、realm,一个接收Collection realms集合,另一个接收Realm singleRealm,两者二选一通过构造方法传参生成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* Supporting constructor for a single-realm application.
*
* @param singleRealm the single realm used by this SecurityManager.
*/
public DefaultSecurityManager(Realm singleRealm) {
this();
setRealm(singleRealm);
}
/**
* Supporting constructor for multiple {@link #setRealms realms}.
*
* @param realms the realm instances backing this SecurityManager.
*/
public DefaultSecurityManager(Collection<Realm> realms) {
this();
setRealms(realms);
}

顾名思义支持多个就是支持多个singleRealm,在使用中无非就能想象得到和一个的区别,在运行时以哪个为准呢?还是都需要过一遍呢?还是优先使用谁呢?Shiro并没有告诉你,而是要你自己去选择,Shiro提供了这些功能并将选择权交给开发人员根据实际业务去管理,具体场景也需要到下一张讲场景的地方详谈。

会话管理器在初始化时还做了另外三件事情,分别是Subject工厂,Subject数据访问对象和缓存管理器。

1
2
3
4
public DefaultSecurityManager() {
super();
this.subjectFactory = new DefaultSubjectFactory();
this.subjectDAO = new DefaultSubjectDAO();

以及

1
2
3
4
5
protected void applyCacheManagerToSessionManager() {
if (this.sessionManager instanceof CacheManagerAware) {
((CacheManagerAware) this.sessionManager).setCacheManager(getCacheManager());
}
}

会话管理器中这么多的配置都有什么作用呢,接下来分别都看看各自的情况,他们之间的协同工作则是需要到下一小节请求实例中讲到。

Realm是Shiro一等公民,翻译过来就是域的意思,套用现在非常火爆的名字更加能体现他的特征:领域模型,也就是在特定的领域下针对该领域下的控制模型,具体到安全管理的颗粒度,也就是人、资源、权限的分配规则都在这里得到。

Shiro提供了一个抽象类AuthorizingRealm供开发人员继承,平常定义时需要实现这个类,主要实现2个功能,分别是授权(Class AuthenticatingRealm)和鉴权(Class AuthorizingRealm),对应到重写方法是:

1
2
3
4
5
//Class AuthenticatingRealm
protected abstract AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException;
//Class AuthorizingRealm
protected abstract AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals);

授权:对标识进行核对的过程
鉴权:对凭证进行验证的过程

DefaultSubjectDAO是针对Subject的数据访问对象,先简单理解抽象一下Subject的作用和概念,Subject是实例级别的,也就是说基于请求线程通过ThreadContext绑定的,完成实例过程中的一系列授权鉴权、Session数据绑定的功能,在实例中才会出现,正是因为这里有着和Session的关联,所以SubjectDAO正式定义了这些功能的使用类,Subject是线程级别的,当需要夸线程进行安全访问时,最简单的办法就是通过execute(runnable/callable实例)直接调用;或者通过associateWith(runnable/callable实例)得到一个包装后的实例;它们都是通过:1、把当前线程的Subject绑定过去;2、在线程执行结束后自动释放。。

另外Spring环境下需要执行一个工具类的赋值操作,在后面Subject初始化时需要调用getSecrityManager

1
2
3
4
5
<!-- 相当于调用SecurityUtils.setSecurityManager(securityManager) -->
<bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
<property name="staticMethod" value="org.apache.shiro.SecurityUtils.setSecurityManager"/>
<property name="arguments" ref="securityManager"/>
</bean>

请求实例

一个请求实例是完成一个Subject的生命周期,在web环境下通常是一个Request到达Servlet前经过Filter层面进行的认证和鉴权工作,web环境下通常使用对接口的url进行权限划分,有着其特定场景将资源视为一个个url,而在非web环境下是一样的理解方式,不同的是web场景仅仅是借用了Servlet容器提供的Filter作为整个校验的一个切面,而这种切面在不同的场景可以借助不同的切面进行,比如aop框架等。

接下来通过一个简单的Http的Request来看源码是如何进行的。

首先请求进入servlet容器,被每个Filter拦截过滤,当请求到达ShiroFilter时,此时Spring的代理Filter会去执行真正的名称和ShiroFilter一致的SpringBean,这个Bean就是在Spring中定义的工厂类org.apache.shiro.spring.web.ShiroFilterFactoryBean。

这个工厂类通过createInstance得到真正的ShiroFilter,看一下这个类到底是什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
protected AbstractShiroFilter createInstance() throws Exception {
log.debug("Creating Shiro Filter instance.");
SecurityManager securityManager = getSecurityManager();
if (securityManager == null) {
String msg = "SecurityManager property must be set.";
throw new BeanInitializationException(msg);
}
if (!(securityManager instanceof WebSecurityManager)) {
String msg = "The security manager does not implement the WebSecurityManager interface.";
throw new BeanInitializationException(msg);
}
FilterChainManager manager = createFilterChainManager();
//Expose the constructed FilterChainManager by first wrapping it in a
// FilterChainResolver implementation. The AbstractShiroFilter implementations
// do not know about FilterChainManagers - only resolvers:
PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver();
chainResolver.setFilterChainManager(manager);
//Now create a concrete ShiroFilter instance and apply the acquired SecurityManager and built
//FilterChainResolver. It doesn't matter that the instance is an anonymous inner class
//here - we're just using it because it is a concrete AbstractShiroFilter instance that accepts
//injection of the SecurityManager and FilterChainResolver:
return new SpringShiroFilter((WebSecurityManager) securityManager, chainResolver);
}

最后返回了一个静态内部类SpringShiroFilter,按照Filter接口的规范,这里应该会去执行SpringShiroFilter的doFilter方法,所以接下来就需要找到doFilter方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
private static final class SpringShiroFilter extends AbstractShiroFilter {
protected SpringShiroFilter(WebSecurityManager webSecurityManager, FilterChainResolver resolver) {
super();
if (webSecurityManager == null) {
throw new IllegalArgumentException("WebSecurityManager property cannot be null.");
}
setSecurityManager(webSecurityManager);
if (resolver != null) {
setFilterChainResolver(resolver);
}
}
}

SpringShiroFilter类继承了AbstractShiroFilter抽象类,而这里并没有doFilter方法,所以继续看下面的继承类OncePerRequestFilter。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
if ( request.getAttribute(alreadyFilteredAttributeName) != null ) {
log.trace("Filter '{}' already executed. Proceeding without invoking this filter.", getName());
filterChain.doFilter(request, response);
} else //noinspection deprecation
if (/* added in 1.2: */ !isEnabled(request, response) ||
/* retain backwards compatibility: */ shouldNotFilter(request) ) {
log.debug("Filter '{}' is not enabled for the current request. Proceeding without invoking this filter.",
getName());
filterChain.doFilter(request, response);
} else {
// Do invoke this filter...
log.trace("Filter '{}' not yet executed. Executing now.", getName());
request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);
try {
doFilterInternal(request, response, filterChain);
} finally {
// Once the request has finished, we're done and we don't
// need to mark as 'already filtered' any more.
request.removeAttribute(alreadyFilteredAttributeName);
}
}
}

业务

传统web场景

App客户端场景

混合场景(登录、二维码、Oauth2.0)

分布式场景

SSO(CAS)

另外还有3个不常用的场景,网上的案例也很多,这里不阐述了。

  • AOP实现的本地方法访问
  • RMI协议远程方法访问
  • 基于标签Tag的访问