写在前面

本篇开始学习Shiro安全框架,这是一个开源的轻量级的Java安全框架,在一些中小型项目中推荐使用Shiro安全框架。

本篇主要学习以下内容:(1)Shiro简介;(2)Shiro功能;(3)Shiro优点;(4)Shiro架构;(5)Shiro概念;(6)Shiro认证流程;(7)Shiro授权流程;(8)Shiro标签;(9)权限。

Shiro

Shiro简介

Apache Shiro是一个开源的轻量级的Java安全框架,它提供身份验证、授权、密码管理以及会话管理等功能。相对于Spring Security, Shiro框架更加直观、易用,同时也能提供健壮的安全性。

Shiro功能

Shiro框架相比于Spring Security更加轻量级,但是它也提供了一些基础的,能满足日常开发所需的权限控制功能。

Shiro框架提供的功能如下所示:
(1)提供登录用户身份的验证;
(2)用户访问权限控制和登陆认证,注意这个有两层含义,其一是用户登录验证;其二是用户登录后授权,即用户拥有访问哪些接口的权限;
(3)可以响应认证、访问控制或者Session生命周期中发生的事件;
(4)支持单点登录SSO功能;
(5)支持将多个用户安全数据源组合为一个复合的用户视图View;
(6)提供“Remember Me”功能,即当用户第二次登录时,只要Session还可用,则无需再次登录。

Shiro优点

Shiro框架相比于Spring Security存在以下优点:
(1)上手容易。Shiro学习成本非常低,不像Spring Security需要花费大量的时间;
(2)非常灵活。Shiro可以在任何应用中使用,不存在较为严重的依赖关系;
(3)Web支持。Shiro拥有较为完善的Web应用程序支持,同时还允许用户基于应用程序的URL来创建更为灵活的安全策略和网络协议,而且还提供一组JSP库来控制页面的输出,不过JSP用的已经不多;
(4)低耦合性。Shiro优秀的设计理念和API使得它可以很容易的与许多其他框架和应用进行集成。Shiro可以无缝的与Spring、Grails、Wicket、Mule等框架进行整合使用;
(5)支持广泛。Shiro是Apache基金会的项目,因此受众用户和群体还是比较好的。

但是请注意,当开发者所开发的项目需要更为严苛的权限控制,那么还是推荐使用Spring Security,Shiro一般用在中小型项目上。

Shiro架构

下面是Shiro框架的架构图:

从图中可以看到它分为两大部分:Primary Concerns(主要内容)和Supporting Features(支持特点)。

Primary Concerns包含4大部分,分别是Authentication(认证)、Authorization(授权)、 Session Management(会话管理)、Cryptography(加密)。Supporting Features分为6个部分,分别是Web Support(Web支持)、Caching(缓存)、Concurrency(并发性)、Testing(测试)、Run As(运行方式)、Remember Me(记住我)。

(1)Authentication(认证):用于用户登录时的认证,即验证用户是不是拥有相应的身份;
(2)Authorization(授权):授权就是进行权限校验,校验已经认证的用户是否拥有某个权限。说白了就是用于访问控制,即哪些用户拥有访问哪些接口或者资源的权限;
(3)Session Management(会话管理):用户登录后就是一次会话,如果用户没有退出,那么它所有的信息都在会话中;
(4)Cryptography(加密):用于对用户的登录信息进行加密操作,以提升数据的安全性;
(5)Web Support(Web支持):Shiro提供的对于Web的支持,这样可以提高Web应用程序的安全性;
(6)Caching(缓存),它是Shiro API中的第一级,以确保安全操作保持快速和高效;
(7)Concurrency(并发性):Shiro支持具有并发功能的多线程应用程序,即在一个线程中开启另一个线程,它能把权限自动传播过去;
(8)Testing(测试):提供测试支持,这样可帮助开发者编写单元测试和集成测试;
(9)Run As(运行方式):也就是可允许用户承担另一个用户的身份,这一点在管理系统中非常有用;
(10)Remember Me(记住我):记住用户在会话中的身份,因此当用户第二次登录时,只要Session还可用,则无需再次登录。

需要说明的是,Shiro不会去维护用户和权限,这些需要开发者自己来手动设计,然后将这些接口注入给Shiro。

Shiro概念

下面是Shiro框架中几个较为重要的概念:Subject、SecurityManager和Realm:

(1)Application Code是应用程序代码;
(2)应用程序代码直接交互对象是Subject,即Shiro对外API核心就是Subject。Subject代表了当前用户,请注意这个Subject可以是一个人,可以是第三方服务,守护进程账户,或者其他和当前软件交互的任何对象;注意与Subject的所有交互都会委托给SecurityManager,因此Subject其实是一个门面,SecurityManager才是实际的执行者;
(3)SecurityManager是安全管理器,也就是说所有与安全有关的操作都会与SecurityManager进行交互,它用来管理所有的Subject,因此它是Shiro框架的核心,负责与Shiro框架中的其他组件进行交互,相当于SpringMVC中的DispatcherServlet的角色;
(4)Realm用于权限信息的验证,这个需要开发者自己实现。Shiro从Realm中获取安全数据(用户、角色、权限),也就是说SecurityManager想要验证用户身份,那么它必须从Realm中获取安全数据进而来确定用户的身份是否合法,之后判断用户是否具有某个角色,某个权限。

你可以将Realm理解为一个DataSource,但是Realm本质上是一个特定的安全DAO,它封装了与数据源连接的细节,得到Shiro所需的相关数据,因此在使用Shiro框架的时候,开发者必须提供一个Realm来实现Authentication(认证)或者Authorization(授权)。其中Authentication用于验证用户身份,Authorization用于授权访问控制,即判断用户是否允许进行当前操作,如访问某个链接,某个资源文件等。

再来看一下SecurityManager(安全管理器)的内部示意图:

可以看到里面有Authenticator、Authorizer、SessionManager、SessionDAO、CacheManager、Realms和Cryptography。

(1)前面说过所有与安全有关的操作都会与SecurityManager进行交互,它用来管理所有的Subject,因此它是Shiro框架的核心,负责与Shiro框架中的其他组件进行交互,相当于SpringMVC中的DispatcherServlet的角色;
(2)Authenticator,它负责Subject的认证,可以扩展,用户通常都会使用认证策略(AuthenticationStrategy)来进行认证限制,也就是满足什么条件用户认证才会通过;
(3)Authorizer,它负责Subject的授权,即访问控制,可以扩展。用来对已认证的用户进行访问限制,控制用户能访问的资源;
(4)SessionManager,管理Session生命周期的组件,注意Shiro可用在JavaSE环境,也可用在Web环境;
(5)SessionDAO,可以把Session写到数据库里面,对session进行增删改查操作;
(6)CacheManager,缓存管理。可以对用户、角色、权限等的缓存进行管理,这些数据基本上不会发生变化,因此放到缓存中可以提高访问的性能;
(7)Cryptography,密码模块。Shiro提供了一些常用的加密组件,用于对密码进行加密和解密。

Shiro认证

Shiro认证概念

在Shiro框架中,用户需要提供principals(身份)和credentials(证明),这样才能对用户身份进行认证。

principals(身份),也就是主体的标识属性,可以是用户的任何属性,如用户名、邮箱、手机号等。注意一个主体可以有多个principals,但是只能有一个Primary principals,通常会设置为用户名/邮箱/手机号。

credentials(证明),也就是证明或者凭证,你光有主体不行,还需要知道主体的安全值,如密码或者数字证书。

综上所述,一般常用的principals(身份)和credentials(证明)组合就是用户名称和用户密码。

Shiro认证流程

了解了这个知识,接下来学习Authentication(认证)的流程,如下所示:

从上图中,开发者可以看到Shiro认证的全部过程:

第一步,获取当前的Subject;
第二步,校验当前Subject是否被认证。如果没有认证,则会创建一个表单页面,然后将请求提交到SpringMVC的Handler处理器,之后就获取到了用户名和密码,最后将用户名和用户密码封装为UsernamePasswordToken对象;
第三步,执行Subject.login()方法进行登录,注意它会自动委托给SecurityManager,SecurityManager负责身份验证逻辑,注意它会自动委托给Authenticator进行身份验证,Authenticator才是真正的身份验证者,Shiro API中核心的身份认证入口就是这里,开发者可以在此处自定义自己的实现。Authenticator可能会委托给相应的AuthenticationStrategy来进行多Realm身份验证,默认的ModularRealmAuthenticator会调用AuthenticationStrategy进行多Realm身份验证。开发者可以通过继承org.apache.shiro.realm.AuthorizingRealm类并重写其中的doGetAuthenticationInfo(AuthenticationToken authenticationToken)方法来完成用户认证。

为了加深印象和理解,开发者可以自己通过手动创建一个Maven项目来进行验证,这里的项目名称为envy-authentication,然后在pom.xml依赖文件中新增如下配置:

1
2
3
4
5
6
7
8
9
10
11
12
<dependencies>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.2</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
</dependencies>

之后新建一个EnvyAuthentication测试类,其中的代码如下所示:

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
public class EnvyAuthentication {
SimpleAccountRealm simpleAccountRealm = new SimpleAccountRealm();

/**
* 在Test方法执行之前添加一个用户
* */
@Before
public void addUser(){
simpleAccountRealm.addAccount("envy","1234");
}

/**
* 测试Shiro框架的Authentication认证
* */
@Test
public void testAuthentication(){
//1、创建SecurityManager
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
defaultSecurityManager.setRealm(simpleAccountRealm);

//2、主题提交认证请求
//2.1、设置SecurityManager环境
SecurityUtils.setSecurityManager(defaultSecurityManager);
//2.2、获取当前主体
Subject subject = SecurityUtils.getSubject();

//信息验证
UsernamePasswordToken token = new UsernamePasswordToken("envy","1234");
//登录
subject.login(token);

//subject.isAuthenticated()方法返回一个boolean值,用于判断用户是否认证成功
System.out.println("The user is authenticated:" + subject.isAuthenticated()); //输出true

//登出
subject.logout();

System.out.println("The user is authenticated:" + subject.isAuthenticated()); //输出false
}
}

之后运行该测试类中的testAuthentication方法,可以看到控制台依次输出如下信息:

也就是表明上述代码执行是先认证成功,之后由于退出登录,因此输出认证失败。

接下来再来看一张更为详细的认证流程图:

解释一下上述流程:
(1)调用Subject.login(token)方法进行登录,注意它会自动委托给SecurityManager,因此在调用该方法之前必须通过SecurityUtils.setSecurityManager()来设置SecurityManager
(2)SecurityManager负责真正的身份验证逻辑,注意它会自动委托给Authenticator进行身份验证;
(3)Authenticator才是真正的身份验证者,Shiro API中核心的身份认证入口就是这里,开发者可以在此处自定义自己的实现;
(4)Authenticator可能会委托给相应的AuthenticationStrategy来进行多Realm身份验证,默认的ModularRealmAuthenticator会调用AuthenticationStrategy进行多 Realm身份验证;
(5)Authenticator会将对应的token传入Realm,进而从Realm获取身份验证信息,如果没有返回或者抛出异常信息,则表示身份验证失败。注意此处可以配置多个Realm,它将按照相应的顺序及策略进行访问。

Shiro授权流程

Shiro授权概念

说完了Shiro中的认证流程,将下来再来学习Authorization(授权)流程。授权,也称为访问控制,即在控制用户能访问应用中的哪些资源,如访问页面、编辑页面、操作页面等。在授权中需要了解几个关键对象:主体(Subject)、资源(Resource)、权限(Permission)和角色(Role)。

(1)主体(Subject):访问应用的用户,Shiro框架使用Subject来代表当前访问用户。用户只有授权之后,才能访问相应的资源;
(2)资源(Resource):在应用中用户可以的URL都是资源,如访问HTML页面、查看和编辑某些数据、访问某个业务方法、打印文本等等。用户只有授权后才能访问;
(3)权限(Permission)。安全策略中的原子授权单位,通过权限开发者可以表示用户在应用中有没有操作某个资源的权利。说白了,权限就是用来表示用户在应用中,能不能访问某个资源,如访问用户列表页面时,能否进行查看、新增、修改、删除用户数据等操作,这就是通常我们所说的CRUD(增查改删)。也就是说权限代表了用户有没有操作某个资源的权利,即反映在某个资源上的操作允不允许;
(4)角色(Role):简单来说角色就是权限的集合。一般来说,我们都是赋予用户不同的角色,而不是权限,这样使得用户可以拥有一组权限,而且便于用户授权。

请注意,Shiro框架不仅支持粗粒度权限(如用户模块的所有权限),也支持细粒度权限(如某个用户的操作权限,实例级别)。

Shiro授权方式

Shiro存在三种授权方式:(1)编程式。通过书写if-else授权代码块来完成授权;(2)注解式。通过在执行的Java方法上添加相应的注解,进而来完成授权,若此时执行的方法没有权限,那么会抛出相应的异常;(3)JSP/GSP标签。可以通过在JSP/GSP页面添加相应的标签来完成。

在学习认证的时候,我们自定义的Realm类需要继承AuthorizingRealm类,并重写其中的doGetAuthenticationInfo方法,而在授权时,由于授权之前必须通过认证,因此也需要先重写其中的doGetAuthenticationInfo方法,之后重写其中的doGetAuthorizationInfo方法。在前面阅读源码的时候,我们发现这个AuthorizingRealm类继承了AuthenticatingRealm类,这也从侧面验证了这一点。

Shiro授权流程

Authorization(授权)流程,如下图所示:

从上图中,开发者可以看到Shiro授权的全部过程:

第一步,调用Subject.isPermitted()或者Subject.hasRole()方法,注意它会自动委托给SecurityManager,而SecurityManager接着会自动委托给Authorizer;
第二步,Authorizer是真正的授权者,如果调用 isPermitted(“user:view”),那么它首先会通过PermissionResolver将字符串转换成相应的Permission实例;
第三步,在进行授权之前,它会调用相应的Realm来获取Subject相应的角色或者权限用于匹配传入的角色或者权限;
第四步,Authorizer会判断Realm的角色或者权限是否和传入的匹配,如果有多个Realm,那么会委托给 ModularRealmAuthorizer进行循环判断, 如果匹配,那么isPermitted/hasRole方法会返回true表示授权成功,否则将返回false表示授权失败。

ModularRealmAuthorizer是进行多Realm匹配的类,它的匹配流程如下所示:
第一步,检查相应的Realm是否实现了Authorizer;
第二步,如果实现了Authorizer,那么就直接调用其相应的isPermitted/hasRole方法来进行判断;
第三步,如果有一个Realm匹配,那么将返回true,否则将返回false。

可以看出授权和认证流程差不多,这里同样通过代码来加深印象。开发者创建一个Maven项目来进行验证,这里的项目名称为envy-authorization,然后在pom.xml依赖文件中新增如下配置:

1
2
3
4
5
6
7
8
9
10
11
12
<dependencies>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.2</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
</dependencies>

之后新建一个EnvyAuthorization测试类,其中的代码如下所示:

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
public class EnvyAuthorization {
SimpleAccountRealm simpleAccountRealm = new SimpleAccountRealm();

/**
* 在Test方法执行之前添加一个用户,让它具备admin和user这两个角色
* */
@Before
public void addUser(){
simpleAccountRealm.addAccount("envy","1234","admin","user");
}

@Test
public void testAuthorizated(){
//1、创建SecurityManager
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
defaultSecurityManager.setRealm(simpleAccountRealm);

//2、主题提交认证请求
//2.1、设置SecurityManager环境
SecurityUtils.setSecurityManager(defaultSecurityManager);
//2.2、获取当前主体
Subject subject = SecurityUtils.getSubject();

//信息验证
UsernamePasswordToken token = new UsernamePasswordToken("envy","1234");
//登录
subject.login(token);

//subject.isAuthenticated()方法返回一个boolean值,用于判断用户是否认证成功
System.out.println("The user is authenticated:" + subject.isAuthenticated()); //输出true

subject.checkRoles("admin","user");
//subject.checkRole("test"); //无对应角色,运行抛出错误
}
}

之后运行该测试类中的testAuthorizated方法,可以看到控制台依次输出如下信息:

1
The user is authenticated:true

由于 subject.checkRoles("admin","user");方法无返回值,因此只会输出认证相关信息。

Shiro标签

Shiro框架提供了JSTL标签用于在JSP 面进行权限控制,如根据登录用户不同来显示不同的页面按钮,常用的JSTL标签如下所示:
(1)guest标签:用户没有进行身份认证时就能显示相应信息,即游客访问信息;
(2)user标签:用户已经通过认证或者记住我登录后显示相应的信息;
(3)authenticated标签:用户已经通过身份认证,即Subject.login()方法执行成功,注意这个不是“记住我”登录的;
(4)notAuthenticated标签:用户未进行身份认证,即未调用Subject.login()方法,注意它包括 由于选择了“记住我”而自动登录这一情况;
(5)pincipal标签:显示用户身份信息,默认调用Subject.getPrincipal()方法来获取,即 PrimaryPrincipal对象;
(6)hasRole标签:如果当前Subject拥有角色,那么将会显body体内容;
(7)hasAnyRoles标签:如果当前Subject有任意一个角色(或的关系)那么将会显body体内容;
(8)lacksRole标签:如果当前Subject没有角色,那么将显示body体内容;
(9)hasPermission标签:如果当前Subject拥有权限,那么将显示body体内容;
(10)lacksPermission标签:如果当前Subject没有权限,那么将显示body体内容。

权限

接下来学习权限的配置规则,这个非常重要。规则:资源标识符,操作:对象实例的ID,即对哪个资源的哪个实例允许进行什么操作,其默认支持通配符权限字符串。其中:(冒号)表示对于资源、操作和实例进行分隔,,(逗号)表示操作的分隔;*(星号)表示任意资源、操作或者实例。

一般来说我们都会对资源和操作这两者进行组合,进而形成权限管理。如user:queryuser:viewuser:edit等这些,其中冒号用于分隔权限字符串,冒号之前的表示权限被操作的领域这里是user(用户相关资源),冒号之后的表示被执行的操作,这里是指queryviewedit等操作。请注意也可以直接通过user:query,view,edit来赋予上述权限。还可以通过使用*号来代替所有的值,如user:*表示可以在与用户相关的资源上执行所有权限;*:query表示在所有资源上都可以执行query操作。

Shiro权限控制

Shiro权限控制粒度比前面介绍的传统方式更加细化,除了资源和操作,Shiro还支持实例,因此Shiro框架还支持实例级别的访问控制。

所谓实例级别的访问控制,是指使用资源+操作+实例三者来组合进而实现权限控制。如user:view:manager,其中user表示和user相关的资源,view表示查看操作,manager表示manager实例。可以使用通配符来定义,如user:view:user::user::manager、等,也就是说它支持省略通配符,缺少对应的部分则表示可以访问所有与之匹配的值。如user:view等价于user:view:user等价于user::*,请注意通配符只能从字符串的结尾处省略,也就是说user:view不能等价于user:*:view,而正确的应该是user:view:

Shiro权限注解

Shiro框架中存在一些特殊注解,这些注解用来判断用户权限,如下所示:
(1)@RequiresAuthentication注解:表示当前Subject已经通过login进行了身份认证,即Subject.isAuthenticated()方法返回true;
(2)@RequiresUser注解:表示当前Subject已经通过身份认证或者通过“记住我”登录;
(3)@RequiresGuest注解:表示当前Subject没有通过login进行身份认证,或者是通过“记住我”登录;
(4)@RequiresRoles(value={“admin”, “user”}, logical= Logical.AND)注解:表示当前Subject需要admin和user角色。Logical.AND表示逻辑与,而Logical.OR表示逻辑或,这里没有逻辑非;
(5)@RequiresPermissions (value={“user:a”, “user:b”}, logical= Logical.OR)注解:表示当前Subject需要权限user:a或者user:b

那么本篇关于Shrio框架的快速入门学习就到此为止,后续学习其他知识。

参考文章:Shiro权限管理框架入门到实战Shiro官网