shiro介绍和使用

框架介绍

1、shiro框架及其基本功能

Apache Shiro 是Java 的一个安全框架。Shiro 可以非常容易的开发出足够好的应用,其不仅可以用在JavaSE 环境,也可以用在JavaEE 环境。Shiro 可以帮助我们完成:认证、授权、加密、会话管理、与Web 集成、缓存等。
Shiro 的 API 也是非常简单;其基本功能点如下图所示:

20200421181655

Authentication :身份认证/登录,验证用户是不是拥有相应的身份;
Authorization :授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;
Session Manager :会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通 JavaSE 环境的,也可以是如 Web 环境的;
Cryptography :加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;
Web Support :Web 支持,可以非常容易的集成到 Web 环境;
Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率;
Concurrency :shiro 支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
Testing :提供测试支持;
Run As :允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
Remember Me :记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。

注:Shiro 不会去维护用户、维护权限;这 些需要我们 自己去 设计/ 提供 ; 然后通过相应的 接口注入给 给 Shiro 即可。

2、shiro架构原理

20200421181738

应用代码直接交互的对象是Subject,也就是说Shiro的对外API核心就是Subject;其每个 API 的含义:

Subject :主体,代表了当前"用户",这个用户不一定是一个具体的人,与当前应用交互的任何东西都是 Subject,如网络爬虫,机器人等;即一个抽象概念;所有 Subject 都绑定到 SecurityManager,与 Subject 的所有交互都会委托给 SecurityManager;可以把 Subject 认为是一个门面;SecurityManager 才是实际的执行者;SecurityManager :安全管理器;即所有与安全有关的操作都会与 SecurityManager 交互;且它管理着所有 Subject;可以看出它是 Shiro 的核心,它负责与后边介绍的其他组件进行交互,如果学习过 SpringMVC,你可以把它看成 DispatcherServlet 前端控制器;
Realm: 域,Shiro 从从 Realm 获取安全数据(如用户、角色、权限),就是说 SecurityManager要验证用户身份,那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法;也需要从 Realm 得到用户相应的角色/权限进行验证用户是否能进行操作;可以把 Realm 看成 DataSource,即安全数据源。

也就是说对于我们而言,最简单的一个 Shiro 应用:

1、 应用代码通过 Subject 来进行认证和授权,而 Subject 又委托给 SecurityManager;
2、 我们需要给 Shiro 的 SecurityManager 注入 Realm,从而让 SecurityManager 能得到合法的用户及其权限进行判断。

注:从以上也可以看出 ,Shiro 不提供维护用户/ 权限, 而是通过 Realm 让开发人员自己注入。

20200421181854

Subject :主体,可以看到主体可以是任何可以与应用交互的"用户";
SecurityManager :  相 当 于 SpringMVC 中 的 DispatcherServlet 或 者 Struts2 中 的FilterDispatcher;是 Shiro 的心脏;所有具体的交互都通过 SecurityManager 进行控制;它管理着所有 Subject、且负责进行认证和授权、及会话、缓存的管理。
Authenticator :认证器,负责主体认证的,这是一个扩展点,如果用户觉得 Shiro 默认的不好,可以自定义实现;其需要认证策略(Authentication Strategy),即什么情况下算用户认证通过了;
Authrizer :授权器,或者访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能;
Realm :可以有 1 个或多个 Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是 JDBC 实现,也可以是 LDAP 实现,或者内存实现等等;由用户提供;注意:Shiro不知道你的用户/权限存储在哪及以何种格式存储;所以我们一般在应用中都需要实现自己的 Realm;
SessionManager :如果写过 Servlet 就应该知道 Session 的概念,Session 呢需要有人去管理它的生命周期,这个组件就是 SessionManager;而 Shiro 并不仅仅可以用在 Web 环境,也可以用在如普通的 JavaSE 环境、EJB 等环境;所有呢,Shiro 就抽象了一个自己的 Session来管理主体与应用之间交互的数据;这样的话,比如我们在 Web 环境用,刚开始是一台Web 服务器;接着又上了台 EJB 服务器;这时想把两台服务器的会话数据放到一个地方,这个时候就可以实现自己的分布式会话(如把数据放到 Memcached 服务器);
SessionDAO:DAO 大家都用过,数据访问对象,用于会话的 CRUD,比如我们想把 Session保存到数据库,那么可以实现自己的 SessionDAO,通过如 JDBC 写到数据库;比如想把Session 放到 Memcached 中,可以实现自己的 Memcached SessionDAO;另外 SessionDAO中可以使用 Cache 进行缓存,以提高性能。
CacheManager :缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能。
Cryptography :密码模块,Shiro 提高了一些常见的加密组件用于如密码加密/解密的。

一个简单的示例

maven配置文件:

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
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.hu</groupId>
<artifactId>shiro_01</artifactId>
<version>0.0.1-SNAPSHOT</version>

<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.9</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.1.3</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.2.2</version>
</dependency>
</dependencies>
</project>

在src下创建shiro的ini配置文件/shiro_01/src/main/resources/shiro.ini:

1
2
[users]
zhang=123

登录测试java代码,这里使用junit测试:

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
public class LoginLogoutTest {
@Test
public void testHelloworld() {
// 1、获取 SecurityManager 工厂,此处使用ini配置文件初始化 SecurityManager
Factory<org.apache.shiro.mgt.SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
// 2、获得SecurityManager 实例 并绑定给 SecurityUtils,全局设置,设置一次即可
org.apache.shiro.mgt.SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
// 3、得到 Subject,并且通过用户名/密码创建 身份验证 Token(即用户身份/凭证)
Subject subject = SecurityUtils.getSubject();
//封装token信息
UsernamePasswordToken token = new UsernamePasswordToken("zhang", "123");
try {
// 4、登录,即身份验证
/**
* 用户认证:
* Subject接收token,通过其实现类DelegatingSubject将token委托给SecurityManager来完成认证。
* 默认情况下:SecurityManager接口是通过DefaultSecurityManager类来完成相关的功能,并且由DefaultSecurityManager中的login方法来完成认证过程。
* 在login中调用了该类的authenticate()方法来完成认证,而该方法由AuthenticatingSecurityManager来完成的。
* 在该类的authenticate()方法中,通过调用认证器authenticator来完成工作。
* 而认证器authenticator又是通过ModularRealmAuthenticator类来完成认证;
* 通过ModularRealmAuthenticator中的doAuthenticate方法来获取Realm信息。
* 如果是单个realm直接将token和realm中的数据进行比较判断是否认证成功;如果是多个realm则通过Authentication Strategy来完成对应的操作。
* subject.isAuthenticated()判断是否认证成功。
*/
subject.login(token);
if (subject.isAuthenticated()) {
System.out.println("登录成功");
}else {
System.out.println("用户名或密码错误!");
}
} catch (AuthenticationException e) {
// 5、身份验证失败
System.out.println("登录失败");
e.printStackTrace();
}
Assert.assertEquals(true, subject.isAuthenticated()); // 断言用户已经登录
// 6、退出
subject.logout();
}
}

运行结果输出登录成功

注:如果身份验证失败请捕获 AuthenticationException 或其子类

常见的如 :DisabledAccountException(禁用的帐号)、LockedAccountException(锁定的帐号)、UnknownAccountException(错误的帐号)、ExcessiveAttemptsException(登录失败次数过多)、IncorrectCredentialsException (错误的凭证)、ExpiredCredentialsException(过期的
凭证)等,具体请查看其继承关系;对于页面的错误消息展示,最好使用如”用户名/密码错误”而不是”用户名错误”/“密码错误”,防止一些恶意用户非法扫描帐号库;

最后可以调用 subject.logout 退出,其会自动委托给 SecurityManager.logout 方法退出。

1、收集用户身份/凭证,即如用户名/密码;
2、调用 Subject.login 进行登录,如果失败将得到相应的 AuthenticationException 异常,根据异常提示用户错误信息;否则登录成功;
3、最后调用 Subject.logout 进行退出操作。

认证Authentication

在应用中谁能证明他就是他本人。一般提供如他们的身份ID 一些标识信息来表明他就是他本人,如提供身份证,用户名/密码来证明。

在 shiro 中,用户需要提供principals (身份)和credentials(证明)给shiro,从而应用能验证用户身份:

principals:身份,即主体的标识属性,可以是任何东西,如用户名、邮箱等,唯一即可。一个主体可以有多个 principals,但只有一个 Primary principals,一般是用户名/密码/手机号。
credentials:证明/凭证,即只有主体知道的安全值,如密码/数字证书等。

最常见的 principals 和 credentials 组合就是用户名/密码了。接下来先进行一个基本的身份认证。

另外两个相关的概念是之前提到的 Subject 及 Realm,分别是主体及验证主体的数据源。

认证流程

20200421182228

1、首先调用 Subject.login(token)进行登录,其会自动委托给 Security Manager,调用之前必须通过 SecurityUtils. setSecurityManager()设置;
2、SecurityManager 负责真正的身份验证逻辑;它会委托给 Authenticator 进行身份验证;
3、Authenticator 才是真正的身份验证者,Shiro API 中核心的身份认证入口点,此处可以自定义插入自己的实现;
4、Authenticator 可能会委托给相应的 AuthenticationStrategy 进行多 Realm 身份验证,默认ModularRealmAuthenticator 会调用 AuthenticationStrategy 进行多 Realm 身份验证;
5、Authenticator 会把相应的 token 传入 Realm,从 Realm 获取身份验证信息,如果没有返回/抛出异常表示身份验证失败了。此处可以配置多个 Realm,将按照相应的顺序及策略进行访问。

Realm

域,Shiro 从从 Realm 获取安全数据(如用户、角色、权限),就是说 SecurityManager要验证用户身份,那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法;也需要从 Realm 得到用户相应的角色/权限进行验证用户是否能进行操作;可以把 Realm 看成 DataSource,安全数据源。前面的例子使用的是ini配置org.apache.shiro.realm.text.IniRealm。

Realm接口:

20200421182328

String getName(); //返回一个唯一的 Realm 名字
boolean supports(AuthenticationToken token); //判断此 Realm 是否支持此 Token
AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException; //根据 Token 获取认证信息

20200421182355

最基础的是Realm接口,CachingRealm负责缓存处理,AuthenticationRealm负责认证,AuthorizingRealm负责授权,通常自定义的realm继承AuthorizingRealm。

单个Realm配置

类可以通过实现接口

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
public class MyRealm implements Realm {

@Override
public String getName() {
return "my Realm ";
}

@Override
public boolean supports(AuthenticationToken token) {
// 仅支持 UsernamePasswordToken 类型的 Token
return token instanceof UsernamePasswordToken;
}

@Override
public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String username = (String) token.getPrincipal(); // 得到用户名
String password = new String((char[]) token.getCredentials()); // 得到密码
System.out.println(getName()+" "+username+" "+password);
if (!"zhang".equals(username)) {
throw new UnknownAccountException(); // 如果用户名错误
}
if (!"123".equals(password)) {
throw new IncorrectCredentialsException(); // 如果密码错误
}
// 返回认证信息:如果身份认证验证成功,返回一个 AuthenticationInfo 实现;
return new SimpleAuthenticationInfo(username, password, getName());
}
}

或者继承来实现

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
public class MyRealm1 extends AuthorizingRealm {
@Override
public String getName() {
return "my Realm 1";
}

/*
* 用于授权
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// TODO Auto-generated method stub
return null;
}

/*
* 用于认证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 从token中获取信息
String username = (String) token.getPrincipal(); // 得到用户名
// 根据用户名到数据库中取出用户信息 如果查询不到 返回null
String password = "123";// 假如从数据库中获取密码为123
System.out.println(getName()+" "+username+" "+password);
// 返回认证信息:如果身份认证验证成功,返回一个 AuthenticationInfo 实现;
return new SimpleAuthenticationInfo(username, password, getName());
}
}

ini配置:

[users]
hu=123

[main]

#引入 自定义 realm
userRealm=com.hu.test.MyRealm
#将realm设置到securityManager
securityManager.realms=$userRealm

多个Realm配置

ini配置:
#声明一个 realm
myRealm1=com.hu.realm.MyRealm1
myRealm2=com.hu.realm.MyRealm2
#指定 securityManager 的 realms 实现
securityManager.realms=$myRealm1,$myRealm2

securityManager 会按照 realms 指定的顺序进行身份认证。此处我们使用显示指定顺序的方式指定了 Realm 的顺序,如果删除”securityManager.realms=$myRealm1,$myRealm2”,那么 securityManager 会按照 realm 声明的顺序进行使用(即无需设置 realms 属性,其会自动发现 )。
当我们显示指定realm后,其他没有指定realm将被忽略,”securityManager.realms=$myRealm1”,那么 myRealm2 不会被自动设置进去。

Shiro默认提供的的Realm

20200421182617

一般使用继承 AuthorizingRealm(授权),其继承了 AuthenticatingRealm(即身份验证),而且也间接继承了 CachingRealm(带有缓存实现)。其中主要默认实现如下:

org.apache.shiro.realm.text.IniRealm:[users]部分指定用户名/密码及其角色;[roles]部分指定角色即权限信息;

org.apache.shiro.realm.text.PropertiesRealm: user.username=password,role1,role2 指定用户名/密码及其角色;role.role1=permission1,permission2 指定角色及权限信息;

org.apache.shiro.realm.jdbc.JdbcRealm: 通过 sql 查询相应的信息,如:
select password fromusers where username = ? 获取用户密码
select password, password_salt from users whereusername = ? 获取用户密码
select role_name from user_roles where username = ? 获取用户角色;
select permission from roles_permissions where role_name = ? 获取角色对应的权限信息,也可以调用相应的 api 进行自定义 sql;

JDBC Realm 使用

数据库及依赖: mysql 数据库及 druid 连接池

1
2
3
4
5
6
7
8
9
10
11
<!-- 数据库依赖  使用 mysql 数据库及 druid 连接池 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.25</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>0.2.23</version>
</dependency>

ini配置

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/maven_shiro
dataSource.username=root
dataSource.password=123456
jdbcRealm.dataSource=$dataSource
securityManager.realms=$jdbcRealm

变量名=全限定类名 会自动创建一个类实例

变量名.属性=值 自动调用相应的 setter 方法进行赋值

$变量名 引用之前的一个对象实例

Junit测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class LoginLogoutTest2 {
@Test
public void testHelloworld() {
Factory<org.apache.shiro.mgt.SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro-jdbc-realm.ini");
org.apache.shiro.mgt.SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken("hu", "123");
try {
subject.login(token);
} catch (AuthenticationException e) {
System.out.println("登录失败");
e.printStackTrace();
}
Assert.assertEquals(true, subject.isAuthenticated());
subject.logout();
}
}

结果:信息: {dataSource-1} inited 登录成功

Authenticator 及 AuthenticationStrategy

Authenticator 的职责是验证用户帐号,是 Shiro API 中身份验证核心的入口点。

public AuthenticationInfo authenticate(AuthenticationToken authenticationToken) throws AuthenticationException;
如果验证成功,将返回 AuthenticationInfo 验证信息;此信息中包含了身份及凭证;如果验证失败将抛出相应的 AuthenticationException 实现。

SecurityManager 接口继承了 Authenticator,另外还有一个 ModularRealmAuthenticator 实现,其委托给多个 Realm 进行验证,验证规则通过 AuthenticationStrategy 接口指定,默认提供的实现,三种认证策略:

FirstSuccessfulStrategy:只要有一个 Realm 验证成功即可,只返回第一个 Realm 身份验证成功的认证信息,其他的忽略;
AtLeastOneSuccessfulStrategy:只要有一个 Realm 验证成功即可,和 FirstSuccessfulStrategy不同,返回所有 Realm 身份验证成功的认证信息;
AllSuccessfulStrategy:所有 Realm 验证成功才算成功,且返回所有 Realm 身份验证成功的认证信息,如果有一个失败就失败了。

ModularRealmAuthenticator 默认使用 AtLeastOneSuccessfulStrategy 策略。

ini 配置文件:

#配置验证器  指定 securityManager 的 authenticator 实现
authenticator=org.apache.shiro.authc.pam.ModularRealmAuthenticator
securityManager.authenticator=$authenticator
#配置验证策略  指定 securityManager.authenticator 的 authenticationStrategy
allSuccessfulStrategy=org.apache.shiro.authc.pam.AllSuccessfulStrategy
securityManager.authenticator.authenticationStrategy=$allSuccessfulStrategy

假如有三个realm:
realm1,realm2,realm3

realm1=com.hu.realm.Realm1
realm2=com.hu.realm.Realm2
realm3=com.hu.realm.Realm3
securityManager.realms=$realm1,$realm3

测试成功

自定义Realm

散列算法

一般用于生成数据的摘要信息,是一种不可逆的算法,一般适合存储密码之类的数据,常见的散列算法如MD5、SHA等。一般进行散列时最好提供一个salt,比如加密密码admin产生的散列值是21232f297a57a5a743894a0e4a801fc3,可以到一些md5 解密网站很容易的通过散列值得到密码admin,即如果直接对密码进行散列相对来说破解更容易,此时我们可以加一些只有系统知道的干扰数据,如用户名和ID(即盐);这样散列的对象是”密码+用户名+ID”,这样生成的散列值相对来说更难破解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ShiroMD5 {
//shiro提供了现成的加密类 Md5Hash
@Test
public void testMd5() {
// MD5加密
String password = new Md5Hash("123").toString();
System.out.println("直接加密:" + password);
// 加盐 salt 默认一次散列
String password_salt = new Md5Hash("123", "hu").toString();
System.out.println("加盐salt后散列:" + password_salt);
// 散列2次
String password_salt_2 = new Md5Hash("123", "hu", 2).toString();
System.out.println("散列2次:" + password_salt_2);
// 使用SimpleHash
SimpleHash hash = new SimpleHash("MD5", "123", "hu", 2);
System.out.println("simpleHash:" + hash.toString());
}
}

自定义散列

ini配置:
[users]
hu=123

[main]
#定义凭证匹配器
credentialsMatcher=org.apache.shiro.authc.credential.HashedCredentialsMatcher
#散列算法
credentialsMatcher.hashAlgorithmName=md5
#散列次数
credentialsMatcher.hashIterations=2

#引入 自定义 realm
userRealm=com.hu.test.MyRealmMD5
userRealm.credentialsMatcher=$credentialsMatcher
#将realm设置到securityManager
securityManager.realms=$userRealm

自定义Realm

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
public class MyRealmMD5 extends AuthorizingRealm {
@Override
public String getName() {
return "my Realm MD5";
}
/*
* 用于授权
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
/*
* 用于认证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println(getName());
// 从token中获取信息
String username = (String) token.getPrincipal(); // 得到用户名
// 根据用户名到数据库中取出用户信息 如果查询不到 返回null
// 按照固定规则加密码结果 ,此密码 要在数据库存储,原始密码 是123,盐是hu 2次散列
String password = "5f97546d9e0f0d775b9175286a484c09";// 假如从数据库中获取密码加密
// 返回认证信息
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(username, password, ByteSource.Util.bytes("hu"), getName());
return simpleAuthenticationInfo;


}
}

测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
public void testHelloworld() {
Factory<org.apache.shiro.mgt.SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiromd5.ini");
org.apache.shiro.mgt.SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken("hu", "123");
try {
subject.login(token);
System.out.println("登录成功");
} catch (AuthenticationException e) {
System.out.println("登录失败");
e.printStackTrace();
}
Assert.assertEquals(true, subject.isAuthenticated());
subject.logout();
}

结果:

SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
my Realm MD5
登录成功

授权Authorization

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

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

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

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

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

授权流程

20200421184616

授权方式

Shiro 支持三种方式的授权。

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

Subject subject = SecurityUtils.getSubject();
if(subject.hasRole("admin")) {
//有权限
} else {
    //无权限
}

注解式:通过在执行的 Java 方法上放置相应的注解完成:

@RequiresRoles("admin")
public void hello() {
    //有权限
}
没有权限将抛出相应的异常;

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

<shiro:hasRole name="admin">
    <!- 有权限 ->
</shiro:hasRole>

授权实现

ini配置文件配置用户拥有的角色及角色-权限关系(shiro-permission.ini)

[users]
hu=123,role1,role2
yun=121,role1
[roles]
role1=user:create,user:update
role2=user:create,user:delete

规则:"用户名=密码,角色1,角色2" "角色=权限1,权限2",即首先根据用户名找到角色,然后根据角色再找到权限,角色是权限集合。
Shiro  不进行权限的维护,需要我们通过Realm返回相应的权限信息。只需要维护"用户--角色"之间的关系即可。
权限字符串的规则是:"资源标识符:操作:资源实例标识符",意思是对哪个资源的哪个实例具有什么操作,":"是资源/操作/实例的分割符,权限字符串也可以使用*通配符。

例子:
用户创建权限:user:create,或user:create:*
用户修改实例001的权限:user:update:001
用户实例001的所有权限:user:*:001

示例

[users]
hu=123,role1,role2
yun=121,role2
[roles]
role1=user:create,user:update
role2=user:create,user:delete

java伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 用户认证状态
boolean isAuthenticated = subject.isAuthenticated();
System.out.println("用户认证状态:" + isAuthenticated+"\n-------------------------");

// 判断拥有角色:role1
System.out.println(subject.hasRole("role1"));
// 判断拥有角色:role1 and role2
System.out.println(subject.hasAllRoles(Arrays.asList("role1", "role2")));
// 判断拥有角色:role1 role2 role3
boolean[] result = subject.hasRoles(Arrays.asList("role1", "role2", "role3"));
System.out.println(Arrays.toString(result));

//还可以通过checkRole来检测是否具有某个角色,如果不具有这个角色,则抛出UnauthorizedException异常
subject.checkRole("role1");

// 判断拥有权限:user:create
System.out.println(subject.isPermitted("user:create"));
// 判断拥有权限:user:update and user:delete
System.out.println(subject.isPermittedAll("user:update", "user:delete"));
// 判断没有权限:user:view
System.out.println(subject.isPermitted("user:view"));

subject.logout();

结果:

登录成功
用户认证状态:true
-------------------------
true
true
[true, true, false]
true
true
false

Permission

字符串通配符权限

规则:资源标识符:操作:对象实例ID,即对哪个资源的哪个实例可以进行什么操作。
其默认支持通配符权限字符串,:表示资源/操作/实例的分割;,表示操作的分割;*表示任意资源/操作/实例。

1 、单个资源单个权限
    subject().checkPermissions("system:user:update");
2、单个资源多个权限
ini:role1=system:user:update,system:user:delete
Java:subject().checkPermissions("system:user:update", "system:user:delete");
用户拥有资源system:user的update和delete权限。如上可以简写成:
ini:role1="system:user:update,delete"
Java:subject().checkPermissions("system:user:update,delete");

3、单个资源全部权限
ini:role1=system:user:create,update,delete,view
Java:subject().checkPermissions("system:user:create,delete,update:view");
用户拥有资源system:user的create、update、delete和view所有权限。
如上可以简写成:
ini:role1=system:user:*

4、所有资源全部权限
ini:role1=*:view
Java:subject().checkPermissions("user:view");
用户拥有所有资源的"view"所有权限。

5、实例级别的权限
单个实例单个权限
ini:roll=user:view:1
Java:subject().checkPermissions("user:view:1");

单个实例多个权限
ini:roll="user:update,delete:1"
Java:
subject().checkPermissions("user:delete,update:1");
subject().checkPermissions("user:update:1", "user:delete:1");

单个实例所有权限
ini:roll=user:*:1
Java:subject().checkPermissions("user:update:1", "user:delete:1", "user:view:1");

所有实例单个权限
ini:roll=user:auth:*
Java:subject().checkPermissions("user:auth:1", "user:auth:2");

所有实例所有权限
ini:roll=user:*:*
Java:subject().checkPermissions("user:view:1", "user:auth:2");

自定义Realm实现授权

ini:
[users]
hu=123,role1
yun=121,role2
[roles]
role1=user:create,user:update
role2=user:create,user:delete

[main]
#自定义 realm
userRealm=com.hu.test.MyRealmRole
#将realm设置到securityManager
securityManager.realms=$userRealm

Realm:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*
* 用于授权
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 获取身份信息
String username = (String) principals.getPrimaryPrincipal();
// 根据身份信息获取权限数据
// 模拟
List<String> permissions = new ArrayList<String>();
permissions.add("user:create");
permissions.add("user:update");
// 将权限信息保存到AuthorizationInfo中
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
for (String permission : permissions) {
simpleAuthorizationInfo.addStringPermission(permission);
}
return simpleAuthorizationInfo;
}

测试:

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
public class LoginLogoutTestRole {
@Test
public void testHelloworld() {
Factory<org.apache.shiro.mgt.SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
org.apache.shiro.mgt.SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
Subject subject = SecurityUtils.getSubject();
// 封装token信息
UsernamePasswordToken token = new UsernamePasswordToken("hu", "123");
try {
subject.login(token);
if (subject.isAuthenticated()) {
System.out.println("登录成功");
} else {
System.out.println("用户名或密码错误!");
}
} catch (AuthenticationException e) {
System.out.println("登录失败");
e.printStackTrace();
}
// 用户认证状态
boolean isAuthenticated = subject.isAuthenticated();
System.out.println("用户认证状态:" + isAuthenticated+"\n-------------------------");

// 判断拥有权限:user:create
System.out.println(subject.isPermitted("user:create"));
System.out.println(subject.isPermitted("user:update"));
System.out.println(subject.isPermitted("user:delete"));

subject.logout();
}
}

结果:

my Realm Role    hu        123
登录成功
用户认证状态:true
-------------------------
true
true
false

Authorizer 的职责是进行授权(访问控制),是 Shiro API 中授权核心的入口点,其提供了相应的角色/权限判断接口,具体请参考其 Javadoc。SecurityManager 继承了 Authorizer 接口,且提供了 ModularRealmAuthorizer 用于多 Realm 时的授权匹配。PermissionResolver 用于解析权限字符串到 Permission 实例,而 RolePermissionResolver 用于根据角色解析相应的权限集合。

我们可以通过如下 ini 配置更改 Authorizer 实现:

authorizer=org.apache.shiro.authz.ModularRealmAuthorizer
securityManager.authorizer=$authorizer

对于 ModularRealmAuthorizer,相应的 AuthorizingSecurityManager 会在初始化完成后自动将相应的 realm 设置进去,我们也可以通过调用其 setRealms()方法进行设置。

设置 ModularRealmAuthorizer 的 permissionResolver,其会自动设置到相应的 Realm 上(其实现了 PermissionResolverAware 接口)

permissionResolver=org.apache.shiro.authz.permission.WildcardPermissionResolver
authorizer.permissionResolver=$permissionResolver

设置 ModularRealmAuthorizer 的 rolePermissionResolver,其会自动设置到相应的 Realm 上(其实现了 RolePermissionResolverAware 接口)

rolePermissionResolver=com.hu.test.MyRolePermissionResolver
authorizer.rolePermissionResolver=$rolePermissionResolver

会话管理

Shiro 提供了完整的企业级会话管理功能,不依赖于底层容器(如 web 容器 tomcat),不管JavaSE 还是 JavaEE 环境都可以使用,提供了会话管理、会话事件监听、会话存储/持久化、容器无关的集群、失效/过期支持、对 Web 的透明支持、SSO 单点登录的支持等特性。即直接使用 Shiro 的会话管理可以直接替换如 Web 容器的会话管理。是完整的会话模块。

登录成功后使用 Subject.getSession()即可获取会话;其等价于 Subject.getSession(true),即如果当前没有创建 Session 对象会创建一个;另外 Subject.getSession(false),如果当前没有创建 Session 则返回 null(不过默认情况下如果启用会话存储功能的话在创建 Subject 时会主动创建一个 Session)。

Subject subject = SecurityUtils.getSubject();
Session session = subject.getSession();

session.getId();//获取当前会话的唯一标识。
session.getHost();//获取当前 Subject 的主机地址,该地址是通过 HostAuthenticationToken.getHost()提供的。

session.getTimeout();
session.setTimeout(毫秒);

获取/设置当前 Session 的过期时间;如果不设置默认是会话管理器的全局过期时间。

session.getStartTimestamp();
session.getLastAccessTime();
获取会话的启动时间及最后访问时间;如果是 JavaSE 应用需要自己定期调用 session.touch()去更新最后访问时间;如果是 Web 应用,每次进入 ShiroFilter 都会自动调用 session.touch()来更新最后访问时间。

session.touch();
session.stop();

更新会话最后访问时间及销毁会话;当 Subject.logout()时会自动调用 stop 方法来销毁会话。如果在web中,调用javax.servlet.http.HttpSession. invalidate()也会自动调用Shiro Session.stop方法进行销毁 Shiro 的会话。

session.setAttribute("key", "123");
session.getAttribute("key")
session.removeAttribute("key");

设置/获取/删除会话属性;在整个会话范围内都可以对这些属性进行操作。

会话管理器

会话管理器管理着应用中所有 Subject 的会话的创建、维护、删除、失效、验证等工作。是Shiro 的核心组件,顶层组件 SecurityManager 直接继承了 SessionManager,且提供了SessionsSecurityManager 实 现 直 接 把 会 话 管 理 委 托 给 相 应 的 SessionManager ,DefaultSecurityManager 及 DefaultWebSecurityManager 默认 SecurityManager 都继承了SessionsSecurityManager。

Session start(SessionContext context); //启动会话
Session getSession(SessionKey key) throws SessionException; //根据会话 Key 获取会话

用于 Web 环境的 WebSessionManager 又提供了如下接口

boolean isServletContainerSessions();//是否使用 Servlet 容器的会话  
void validateSessions();//验证所有会话是否过期

Shiro 提供了三个默认实现:

DefaultSessionManager:DefaultSecurityManager 使用的默认实现,用于 JavaSE 环境;
ServletContainerSessionManager:DefaultWebSecurityManager 使用的默认实现,用于 Web环境,其直接使用 Servlet 容器的会话;
DefaultWebSessionManager : 用于Web环境的实现 , 可以替代ServletContainerSessionManager,自己维护着会话,直接废弃了 Servlet 容器的会话管理。


替换 SecurityManager 默认的 SessionManager 可以在 ini 中配置(shiro.ini)
[main]
sessionManager=org.apache.shiro.session.mgt.DefaultSessionManager
securityManager.sessionManager=$sessionManager

Web 环境下的 ini 配置(shiro-web.ini):
[main]
sessionManager=org.apache.shiro.web.session.mgt.ServletContainerSessionManager
securityManager.sessionManager=$sessionManager

设置会话的全局过期时间(毫秒为单位),默认 30 分钟

sessionManager. globalSessionTimeout=1800000

默认情况下 globalSessionTimeout 将应用给所有 Session。可以单独设置每个 Session 的timeout 属性来为每个 Session 设置其超时时间。

如果使用 ServletContainerSessionManager 进行会话管理,Session 的超时依赖于底层Servlet 容器的超时时间,可以在 web.xml 中配置其会话的超时时间(分钟为单位)

<session-config>
<session-timeout>30</session-timeout>
</session-config>

在 Servlet 容器中,默认使用 JSESSIONID Cookie 维护会话,且会话默认是跟容器绑定的;在某些情况下可能需要使用自己的会话机制,此时我们可以使用DefaultWebSessionManager来维护会话:

sessionIdCookie=org.apache.shiro.web.servlet.SimpleCookie
sessionManager=org.apache.shiro.web.session.mgt.DefaultWebSessionManager
sessionIdCookie.name=sid
#sessionIdCookie.domain=sishuok.com
#sessionIdCookie.path=
sessionIdCookie.maxAge=1800
sessionIdCookie.httpOnly=true
sessionManager.sessionIdCookie=$sessionIdCookie
sessionManager.sessionIdCookieEnabled=true
securityManager.sessionManager=$sessionManager

会话监听

会话监听器用于监听会话创建、过期及停止事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class MySessionListener1 implements SessionListener {
@Override
public void onStart(Session session) {//会话创建时触发
System.out.println("会话创建:" + session.getId());
}
@Override
public void onExpiration(Session session) {//会话过期时触发
System.out.println("会话过期:" + session.getId());
}
@Override
public void onStop(Session session) {//退出/会话过期时触发
System.out.println("会话停止:" + session.getId());
}
}

如果只想监听某一个事件,可以继承 SessionListenerAdapter 实现:
public class MySessionListener2 extends SessionListenerAdapter {
@Override
public void onStart(Session session) {
System.out.println("会话创建:" + session.getId());
}
}

在 shiro-web.ini 配置文件中可以进行如下配置设置会话监听器:

sessionListener1=com.github.zhangkaitao.shiro.chapter10.web.listener.MySessionListener1
sessionListener2=com.github.zhangkaitao.shiro.chapter10.web.listener.MySessionListener2
sessionManager.sessionListeners=$sessionListener1,$sessionListener2

会话存储/持久化

Shiro 提供 SessionDAO 用于会话的 CRUD,即 DAO(Data Access Object)模式实现,如 DefaultSessionManager 在创建完 session 后会调用该方法;如保存到关系数据库/文件系统/NoSQL 数据库;即可以实现会话的持久化;返回会话 ID;主要此处返回的ID.equals(session.getId());

Serializable create(Session session);
//根据会话 ID 获取会话
Session readSession(Serializable sessionId) throws UnknownSessionException;
//更新会话;如更新会话最后访问时间/停止会话/设置超时时间/设置移除属性等会调用
void update(Session session) throws UnknownSessionException;
//删除会话;当会话过期/会话停止(如用户退出时)会调用
void delete(Session session);
//获取当前所有活跃用户,如果用户量多此方法影响性能

20200421184538

AbstractSessionDAO提供了SessionDAO的基础实现,如生成会话ID等;CachingSessionDAO提供了对开发者透明的会话缓存的功能,只需要设置相应的 CacheManager 即可;MemorySessionDAO 直接在内存中进行会话维护;而 EnterpriseCacheSessionDAO 提供了缓存功能的会话维护,默认情况下使用 MapCache 实现,内部使用 ConcurrentHashMap 保存缓存的会话。

sessionDAO=org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO
sessionManager.sessionDAO=$sessionDAO

Shiro 提供了使用 Ehcache 进行会话存储,Ehcache 可以配合 TerraCotta 实现容器无关的布式集群。

缓存机制

Shiro 提供了类似于 Spring 的 Cache 抽象,即 Shiro 本身不实现 Cache,但是对 Cache 进行了又抽象,方便更换不同的底层 Cache 实现。

Shiro 提供的 的 Cache :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 public interface Cache<K, V> {
//根据 Key 获取缓存中的值
public V get(K key) throws CacheException;
//往缓存中放入 key-value,返回缓存中之前的值
public V put(K key, V value) throws CacheException;
//移除缓存中 key 对应的值,返回该值
public V remove(K key) throws CacheException;
//清空整个缓存
public void clear() throws CacheException;
//返回缓存大小
public int size();
//获取缓存中所有的 key
public Set<K> keys();
//获取缓存中所有的 value
public Collection<V> values();
}

Shiro 提供的 的 CacheManager 接口:

public interface CacheManager {
    //根据缓存名字获取一个 Cache
    public <K, V> Cache<K, V> getCache(String name) throws CacheException;
}

Shiro 还 提供了 了 CacheManagerAware 用 于注入 CacheManager :

public interface CacheManagerAware {
    //注入 CacheManager
    void setCacheManager(CacheManager cacheManager);
}

Shiro 内部相应的组件(DefaultSecurityManager)会自动检测相应的对象(如 Realm)是实现了 CacheManagerAware 并自动注入相应的 CacheManager。

Realm缓存

Shiro 提供了 CachingRealm,其实现了 CacheManagerAware 接口,提供了缓存的一些基础实现;另外 AuthenticatingRealm 及 AuthorizingRealm 分别提供了对 AuthenticationInfo 和AuthorizationInfo 信息的缓存。

userRealm=com......realm.UserRealm
userRealm.credentialsMatcher=$credentialsMatcher
userRealm.cachingEnabled=true
userRealm.authenticationCachingEnabled=true
userRealm.authenticationCacheName=authenticationCache
userRealm.authorizationCachingEnabled=true
userRealm.authorizationCacheName=authorizationCache
securityManager.realms=$userRealm
cacheManager=org.apache.shiro.cache.ehcache.EhCacheManager
cacheManager.cacheManagerConfigFile=classpath:shiro-ehcache.xml
securityManager.cacheManager=$cacheManager

userRealm.cachingEnabled:启用缓存,默认 false;
userRealm.authenticationCachingEnabled:启用身份验证缓存,即缓存 AuthenticationInfo 信息,默认 false;
userRealm.authenticationCacheName:缓存 AuthenticationInfo 信息的缓存名称;
userRealm. authorizationCachingEnabled:启用授权缓存,即缓存 AuthorizationInfo 信息,默认 false;
userRealm. authorizationCacheName:缓存 AuthorizationInfo 信息的缓存名称;
cacheManager:缓存管理器,此处使用 EhCacheManager,即 Ehcache 实现,需要导入相应的 Ehcache 依赖,

Session缓存

当我们设置了 SecurityManager 的 CacheManager 时,如:securityManager.cacheManager=$cacheManager
当我们设置 SessionManager 时:

sessionManager=org.apache.shiro.session.mgt.DefaultSessionManager
securityManager.sessionManager=$sessionManager

如 securityManager 实现了 SessionsSecurityManager,其会自动判断 SessionManager 是否实现了 CacheManagerAware 接口,如果实现了会把 CacheManager 设置给它。然后sessionManager 会判断相应的 sessionDAO(如继承自 CachingSessionDAO)是否实现了CacheManagerAware,如果实现了会把 CacheManager 设置给它;其会先查缓存,如果找不到才查数据库。

对于 CachingSessionDAO,可以通过如下配置设置缓存的名称:

sessionDAO=com.github.zhangkaitao.shiro.chapter11.session.dao.MySessionDAO
sessionDAO.activeSessionsCacheName=shiro-activeSessionCache

activeSessionsCacheName 默认就是 shiro-activeSessionCache。