Shiro之实现授权

当用户的信息得到认证通过后,该用户在系统中具有哪些操作权限呢?所以在用户认证通过后,系统就要给该用户进行授权操作了。

写在前面的话:同前篇文章Shiro之实现认证那样,本篇文章也只是通过一个入门程序教大家学会使用Shiro实现授权功能(当然只有要在该用户的信息认证通过后才能对该用户进行授权),没有将Shiro同Spring整合起来使用(后面的文章我们会讲解Shiro整合web开发环境的项目中如何实现授权功能,见Shiro整合Web项目及整合后的开发),所以我们就采用上篇文章的工程结构即可。本篇文章的讲解结构跟前篇文章讲解结构也是一致的,便于大家理解。

1.授权的概念

授权,也叫访问控制,即在应用中控制谁能访问哪些资源(如访问页面/编辑数据/页面操作等)。在授权中需了解的几个关键对象:主体(Subject)、资源(Resource)、权限(Permission)、角色(Role)。

主体:即访问应用的用户,在Shiro中使用Subject代表该用户。用户只有授权后才允许访问相应的资源。

资源:在应用中用户可以访问的任何东西,比如访问JSP页面、查看/编辑某些数据、访问某个业务方法、打印文本等等都是资源。用户只要授权后才能访问。

权限:安全策略中的原子授权单位,通过权限我们可以表示在应用中用户有没有操作某个资源的权力。即权限表示在应用中用户能不能访问某个资源,如:访问用户列表页面、查看/新增/修改/删除用户数据(即很多时候都是CRUD(增查改删)式权限控制)、打印文档等等。

如上可以看出,权限代表了用户有没有操作某个资源的权利,即反映在某个资源上的操作允不允许,不反映谁去执行这个操作。所以后续还需要把权限赋予给用户,即定义哪个用户允许在某个资源上做什么操作(权限),Shiro不会去做这件事情,而是由实现人员提供。

角色:角色代表了操作集合,可以理解为权限的集合,一般情况下我们会赋予用户角色而不是权限,即这样用户可以拥有一组权限,赋予权限时比较方便。典型的如:项目经理、技术总监、CTO、开发工程师等都是角色,不同的角色拥有一组不同的权限。包括隐式角色和显示角色。

隐式角色:即直接通过角色来验证用户有没有操作权限,如在应用中CTO、技术总监、开发工程师可以使用打印机,假设某天不允许开发工程师使用打印机,此时需要从应用中删除相应代码;再如在应用中CTO、技术总监可以查看用户、查看权限;突然有一天不允许技术总监查看用户、查看权限了,需要在相关代码中把技术总监角色从判断逻辑中删除掉;即粒度是以角色为单位进行访问控制的,粒度较粗;如果进行修改可能造成多处代码修改。
显示角色:在程序中通过权限控制谁能访问某个资源,角色聚合一组权限集合;这样假设哪个角色不能访问某个资源,只需要从角色代表的权限集合中移除即可;无须修改多处代码;即粒度是以资源/实例为单位的;粒度较细。

授权的核心是权限控制,而权限控制分为基于角色的访问控制和基于资源的访问控制,二者概念我在初识Shiro的1.3.5节的权限控制中讲过,实际开发中都是采用的基于资源的访问控制。下面的介绍中,我在入门程序中会将基于角色的访问控制和基于资源的访问控制都进行讲解,但在讲到使用自定义Realm后我就只会讲解基于资源的访问控制了。

2.授权流程

在Shiro的代码中授权的流程如下图:

在写授权的代码时都要根据这个流程图来写。

3.授权的方式

Shiro支持三种授权的方式。

第一种:编程式:通过写if/else授权代码块完成:

1
2
3
4
5
6
Subject subject = SecurityUtils.getSubject();  
if(subject.hasRole(“admin”)) {
//有权限
} else {
//无权限
}

第二种:注解式:通过在执行的Java方法上放置相应的注解完成(没有权限将抛出相应的异常):

1
2
3
4
@RequiresRoles("admin")  
public void hello() {
//有权限
}

第三种:JSP/GSP标签:在JSP/GSP页面通过相应的标签完成:

1
2
3
<shiro:hasRole name="admin">  
<!— 有权限 —>
</shiro:hasRole>

对于上述授权的三种实现方式,我们只在写入门程序时使用第一种方式,在实际开发中我们使用后两种方式进行用户的授权判断。

4.入门程序

在src包下创建一个shiro-permission.ini配置文件,用于模拟数据库中的权限数据,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#基于用户的访问控制
[users]
#用户zhangsan的密码是123,此用户具有role1和role2两个角色;用户wang具有role2一个角色
zhangsan=123,role1,role2
wang=123,role2

#基于权限的访问控制
[roles]
#角色role1对资源user拥有create、update权限
role1=user:create,user:update
#角色role2对资源user拥有create、delete权限
role2=user:create,user:delete
#角色role3对资源user拥有create权限
role3=user:create

对配置文件中的解释如下:

权限标识符号规则:资源:操作:实例(中间使用半角:分隔)。如下:

user:create:01,表示对用户资源的01实例进行create操作。
user:create,表示对用户资源进行create操作,相当于user:create:*,对所有用户资源实例进行create操作。
user:*:01,表示对用户资源实例01进行所有操作。

然后写一个测试类Authorization.java,代码如下:

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
public class AuthorizationTest
{

//角色授权测试和资源授权测试
@Test
public void testAuthorization()
{

//第一步,创建SecurityManager工厂
Factory<SecurityManager> factory=new IniSecurityManagerFactory("classpath:shiro-permission.ini");

//第二步:创建SecurityManager
SecurityManager securityManager=factory.getInstance();

//第三步,将SecurityManager设置到系统运行环境,和spring整合后会将SecurityManager配置到spring容器中,一般单例管理
SecurityUtils.setSecurityManager(securityManager);

//第四步,创建subject
Subject subject=SecurityUtils.getSubject();

//创建token令牌,这里的用户名和密码以后由用户输入
UsernamePasswordToken token=new UsernamePasswordToken("zhangsan","123");

try {
//执行认证,将用户输入的信息同数据库(即.ini配置文件)中信息进行对比
subject.login(token);
}catch (AuthenticationException e)
{
e.printStackTrace();
}

System.out.println("认证状态:"+subject.isAuthenticated());

//认证通过后才能执行授权
//第一种授权方式是基于角色的授权,hasRole传入角色的标识
boolean ishasRole=subject.hasRole("role1");//该用户是否有role1这个角色
System.out.println("单个角色判断"+ishasRole);

//hasAllRoles是否拥有多个角色
boolean hasAllRoles=subject.hasAllRoles(Arrays.asList("role1","role2"));
System.out.println("多个角色判断"+hasAllRoles);//角色的就讲到这里了,后面我们都是通过资源进行权限讲解


//使用check方法进行授权,如果授权不通过会抛出异常
subject.checkRole("role3");

//第二种授权方式是基于资源的授权,isPermitted传入权限标识符
boolean isPermitted=subject.isPermitted("user:create");//该用户是否有对user资源进行创建的权限
System.out.println("单个权限判断"+isPermitted);

//多个权限判断
boolean isPermittedAll=subject.isPermittedAll("user:create:1","user:update");
System.out.println("多个权限判断:"+isPermittedAll);

}
}

从测试代码中我们可以知道,只有当用户信息得到认证后才能对用户进行授权操作。

上面只是一个简单的入门程序,从代码中直接读取数据库(即配置文件)中的权限数据,然后将其与代码中的给定权限进行判断看该用户是否具有该权限,而实际操作中我们是通过Realm进行读取数据库中的数据的。所以接下来讲自定义Realm进行授权。

5.自定义Realm进行授权

5.1需求

上边的程序通过shiro-permission.ini对权限信息进行静态配置,实际开发中从数据库中获取权限数据。就需要自定义realm,由realm从数据库查询权限数据。

realm根据用户身份查询权限数据,将权限数据返回给authorizer(授权器)。

5.2自定义Realm

在上篇文章自定义的realm中即CustomRealm.java,修改doGetAuthorizationInfo()方法,代码如下:

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

//上边是进行认证的方法,在上篇文章中已完成



//用于授权,当然首先要实现认证
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {

//从principals获取主身份信息
//将getPrimaryPrincipal方法返回值转为真实身份类型(在上边的goGetAuthenticationInfo认证通过填充到SimpleAuthenticationInfo)
String userCode= (String) principals.getPrimaryPrincipal();

//根据身份信息获取权限信息,
//模拟从数据库中获取到的动态权限数据
List<String> permissions=new ArrayList<>();
permissions.add("user:create");//模拟user的创建权限
permissions.add("items:add");//模拟商品的添加权限

//查到权限数据,返回授权信息(包括上边的permissions)
SimpleAuthorizationInfo simpleAuthorizationInfo=new SimpleAuthorizationInfo();

//将上边查询到授权信息填充到simpleAuthorizationInfo对象中
simpleAuthorizationInfo.addStringPermissions(permissions);
return simpleAuthorizationInfo;
}

然后要对该自定义Realm进行配置,即将自定义Realm放入SecurityManager中,上篇文章中的shiro-realm.ini配置文件中我们已实现。

然后便可以进行测试了,测试代码如下:

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
//自定义realm进行资源授权测试
@Test
public void testAuthorizationCustomRealm()
{

//第一步,创建SecurityManager工厂
Factory<SecurityManager> factory=new IniSecurityManagerFactory("classpath:config/shiro-realm.ini");

//第二步:创建SecurityManager
SecurityManager securityManager=factory.getInstance();

//第三步,将SecurityManager设置到系统运行环境,和spring整合后会将SecurityManager配置到spring容器中,一般单例管理
SecurityUtils.setSecurityManager(securityManager);

//第四步,创建subject
Subject subject=SecurityUtils.getSubject();

//创建token令牌,这里的用户名和密码以后由用户输入
UsernamePasswordToken token=new UsernamePasswordToken("zhangsan","111111");

try {
//执行认证,将用户输入的信息同数据库(即.ini配置文件)中信息进行对比
subject.login(token);
}catch (AuthenticationException e)
{
e.printStackTrace();
}

System.out.println("认证状态:"+subject.isAuthenticated());



//基于资源的授权,调用isPermitted方法会调用CustomRealm从数据库查询正确的权限数据
// isPermitted传入权限标识符,判断user:create:1是否在CustomRealm查询到的权限数据之内
boolean isPermitted=subject.isPermitted("user:create:1");//该用户是否有对user的1资源进行创建的权限
System.out.println("单个权限判断"+isPermitted);

//多个权限判断
boolean isPermittedAll=subject.isPermittedAll("user:create:1","user:create");
System.out.println("多个权限判断:"+isPermittedAll);
}

代码(即授权流程讲解)讲解如下:

  1. 对subject进行授权,调用方法isPermitted(”permission串”)。
  2. SecurityManager执行授权,通过ModularRealmAuthorizer执行授权。
  3. ModularRealmAuthorizer执行realm(自定义的CustomRealm)从数据库查询权限数据。即调用realm的授权方法:doGetAuthorizationInfo()
  4. realm从数据库查询权限数据,返回ModularRealmAuthorizer。
  5. ModularRealmAuthorizer调用PermissionResolver进行权限串比对。
  6. 如果比对后,isPermitted中”permission串”在realm查询到权限数据中,说明用户访问permission串有权限,否则 没有权限,抛出异常。

上述我们也是对数据库中的明文进行的讲解,这里就不对数据库中的密文进行讲解了。经过如上入门程序,我们便了解了如何通过Shiro来完成授权功能了,接下来我要完成Shiro与web项目整合的各个功能讲解了,详情请见我往后的文章Shiro系列。

2018.3.19更

欢迎加入我的Java交流1群:659957958。群里目前已有1800人,每天都非常活跃,但为了筛选掉那些不怀好意的朋友进来搞破坏,所以目前入群方式已改成了付费方式,你只需要支付9块钱,即可获取到群文件中的所有干货以及群里面各位前辈们的疑惑解答;为了鼓励良好风气的发展,让每个新人提出的问题都得到解决,所以我将得到的入群收费收入都以红包的形式发放到那些主动给新手们解决疑惑的朋友手中。在这里,我们除了谈技术,还谈生活、谈理想;在这里,我们为你的学习方向指明方向,为你以后的求职道路提供指路明灯;在这里,我们把所有好用的干货都与你分享。还在等什么,快加入我们吧!

2018.4.21更:如果群1已满或者无法加入,请加Java学习交流2群:305335626 。群2作为群1的附属群,除了日常的技术交流、资料分享、学习方向指明外,还会在每年互联网的秋春招时节在群内发布大量的互联网内推方式,话不多说,快上车吧!

6.联系

If you have some questions after you see this article,you can tell your doubts in the comments area or you can find some info by clicking these links.

坚持原创技术分享,您的支持将鼓励我继续创作!