Shiro基础教程

Shiro介绍

什么Shiro

Shiro是一个功能强大且易于使用的Java安全框架,它执行身份验证、授权、加密和会话管理。使用Shiro易于理解的API,您可以快速轻松的保护任何应用程序——从最小的移动应用程序到最大的Web和企业应用程序。

Shiro核心架构

image-20230329165236795

认证流程

image-20230329165534487

Subject

Subject即主体,外部应用与subject进行交互,subject记录了当前的操作用户,将用户的概念理解为当前操作的主体。外部程序通过subject进行认证授权,而subject是通过SecurityManager安全管理器进行认证授权

SecurityManager

SecurityManager即安全管理器,对全部的subject进行安全管理,它是shiro的核心,负责对所有的subject进行安全管理。通过SecurityManager可以完成subject的认证、授权等,实质上SecurityManager是通过Authenticator进行认证,通过Authorizer进行授权,通过SessionManager进行会话管理等

SecurityManager是一个接口,继承了Authenticator, Authorizer, SessionManager这三个接口

Authenticator

Authenticator即认证器,对用户身份进行认证,Authenticator是一个接口,shiro提供ModularRealmAuthenticator实现类,通过ModularRealmAuthenticator基本上可以满足大多数需求,也可以自定义认证器

Authorizer

Authorizer即授权器,用户通过认证器认证通过,在访问功能时需要通过授权器判断用户是否有此功能的操作权限

Realm

Realm即领域,相当于datasource数据源,securityManager进行安全认证需要通过Realm获取用户权限数据,比如:如果用户身份数据在数据库那么realm就需要从数据库获取用户身份信息

不要把realm理解成只是从数据源取数据,在realm中还有认证授权校验的相关的代码

SessionManager

sessionManager即会话管理,shiro框架定义了一套会话管理,它不依赖web容器的session,所以shiro可以使用在非web应用上,也可以将分布式应用的会话集中在一点管理,此特性可使它实现单点登录

SessionDAO

SessionDAO即会话dao,是对session会话操作的一套接口,比如要将session存储到数据库,可以通过jdbc将会话存储到数据库

CacheManager

CacheManager即缓存管理,将用户权限数据存储在缓存,这样可以提高性能

Cryptography

Cryptography即密码管理,shiro提供了一套加密/解密的组件,方便开发。比如提供常用的散列、加/解密等功能。

Shiro中的认证

什么是认证

身份认证,就是判断一个用户是否为合法用户的处理过程。最常用的简单身份认证方式是系统通过核对用户输入的用户名和口令,看其是否与系统中存储的该用户的用户名和口令一致,来判断用户身份是否正确

三个概念

Subject

访问系统的用户,主体可以是用户、程序等,进行认证的都称为主体

Principal

身份信息,是主体(subject)进行身份认证的标识,标识必须具有唯一性,如用户名、手机号、邮箱地址等,一个主体可以有多个身份,但是必须有一个主身份(Primary Principal)

credential

凭证信息,是只有主体自己知道的安全信息,如密码、证书等

认证的实现

  1. 创建一个普通的maven项目,引入shiro的pom依赖

    1
    2
    3
    4
    5
    6
    <dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-core</artifactId>
    <version>1.5.3</version>
    </dependency>
    12345
  2. 引入shiro配置文件shiro.ini,并加入以下配置

    1
    2
    3
    4
    5
    6
    # 约定写法
    [users]
    # 用户名=密码
    christy=123456
    tide=654321
    12345

    shiro的配置文件是一个.ini文件,类似于.txt文件

    .ini文件经常用作某些软件的特定的配置文件,可以支持一些复杂的数据格式,shiro可以按照内部约定的某种格式读取配置文件中的数据

    之所以提供这个配置文件是用来学习shiro时书写我们系统中相关的权限数据,从而减轻配置数据库并从数据库读取数据的压力,降低学习成本,提高学习效率

  3. 测试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
    import org.apache.shiro.SecurityUtils;
    import org.apache.shiro.authc.AuthenticationToken;
    import org.apache.shiro.authc.IncorrectCredentialsException;
    import org.apache.shiro.authc.UnknownAccountException;
    import org.apache.shiro.authc.UsernamePasswordToken;
    import org.apache.shiro.mgt.DefaultSecurityManager;
    import org.apache.shiro.realm.text.IniRealm;
    import org.apache.shiro.subject.Subject;

    public class ShiroAuthenticatorTest {
    public static void main(String[] args){
    // 1、创建安全管理器对象
    DefaultSecurityManager securityManager = new DefaultSecurityManager();
    // 2、给安全管理器设置realm
    securityManager.setRealm(new IniRealm("classpath:shiro.ini"));
    // 3、给全局安全工具类SecurityUtils设置安全管理器
    SecurityUtils.setSecurityManager(securityManager);
    // 4、拿到当前的subject
    Subject subject = SecurityUtils.getSubject();
    // 5、创建令牌
    AuthenticationToken token = new UsernamePasswordToken("christy","123456");

    try {
    // 6、用户认证
    System.out.println("认证状态:"+subject.isAuthenticated());
    subject.login(token);
    System.out.println("认证状态:"+subject.isAuthenticated());
    } catch (UnknownAccountException e){
    e.printStackTrace();
    System.out.println("认证失败:用户不存在!");
    } catch (IncorrectCredentialsException e){
    e.printStackTrace();
    System.out.println("认证失败:密码不正确!");
    } catch (Exception e){
    e.printStackTrace();
    }
    }
    }

    认证的几种状态

    UnknownAccountException:用户名错误

    IncorrectCredentialsException:密码错误

    DisabledAccountException:账号被禁用

    LockedAccountException:账号被锁定

    ExcessiveAttemptsException:登录失败次数过多

    ExpiredCredentialsException:凭证过期

Shiro认证的源码分析

上面我们已经简单实现了shiro的认证,但是shiro内部认证的具体流程是怎么样的,这次我们通过追踪源码的方式具体分析一下。我们在认证处打上断点,点击debug模式运行,然后一步步运行到最后,中间经过的类我们都记录下来

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

至此啊,在SimpleAccountRealm中完成了用户名的认证。

那么密码呢?在哪里校验的呢?我们继续点击下一步,直到这里

在这里插入图片描述

我们看到这里断言密码是否匹配的方法,点进去

在这里插入图片描述

我们看到了这里拿的是我们输入的密码和根据token取出的用户中的密码做的比较来验证密码是否正确,这是系统帮我们完成的

上面我们说了用户的认证是在SimpleAccountRealmdoGetAuthenticationInfo的方法中完成的,而SimpleAccountRealm继承自AuthorizingRealm,而AuthorizingRealm中有一个抽象方法

1
2
protected abstract AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection var1);
1

SimpleAccountRealm就是复写了AuthorizingRealm中的这个抽象方法实现的用户认证,所以后面我们需要自定义认证的时候我们就可以自定义一个realm继承自AuthorizingRealm来复写doGetAuthorizationInfo,在这个方法里面实现我们自己的认证逻辑

不仅认证,有意思的是AuthorizingRealm是继承自AuthenticatingRealm,而AuthenticatingRealm中有个抽象方法

1
2
protected abstract AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken var1) throws AuthenticationException;
1

这个方法是实现用户授权的方法。

也就是说通过我们自定义realm继承AuthorizingRealm就可以同时复写认证和授权两个方法

Realm的继承关系如下

在这里插入图片描述

4、身份认证流程

(1)首先调用 Subject.login(token) 进行登录,其会自动委托给 SecurityManager

(2)SecurityManager 负责真正的身份验证逻辑;它会委托给 Authenticator 进行身份

验证;

(3)Authenticator 才是真正的身份验证者,Shiro API 中核心的身份 认证入口点,此

处可以自定义插入自己的实现;

(4)Authenticator 可能会委托给相应的 AuthenticationStrategy 进 行多 Realm 身份

验证,默认 ModularRealmAuthenticator 会调用 AuthenticationStrategy 进行多 Realm

身份验证;

(5) Authenticator 会把相应的 token 传入 Realm,从 Realm 获取 身份验证信息,如

果没有返回/抛出异常表示身份验证失败了。此处 可以配置多个Realm,将按照相应的顺序

及策略进行访问。

Shiro使用自定义Relam实现认证

上面我们实现了简单的认证并且分析了认证的基本流程,通常情况下shiro的认证都是通过自定义relam来实现的

CustomerRealm

首先我们编写自定义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
31
32
33
34
35
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

/**
* 自定义realm
*/
public class CustomerRealm extends AuthorizingRealm {
// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}

// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 在token中获取用户名
String principal = (String) token.getPrincipal();
System.out.println(principal);
// 模拟根据身份信息从数据库查询
if("christy".equals(principal)){
// 参数说明:用户名 | 密码 | 当前realm的名字
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(principal,"123456",this.getName());
return simpleAuthenticationInfo;
}

return null;
}
}

CustomerRealmAuthenticatorTest

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
import com.christy.shiro.realm.CustomerRealm;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.subject.Subject;

public class CustomerRealmAuthenticatorTest {
public static void main(String[] args) {
// 创建SecurityManager
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
// 设置自定义realm
defaultSecurityManager.setRealm(new CustomerRealm());
// 设置安全工具类
SecurityUtils.setSecurityManager(defaultSecurityManager);
// 通过安全工具类获取subject
Subject subject = SecurityUtils.getSubject();
// 创建token
UsernamePasswordToken token = new UsernamePasswordToken("christy", "123456");
try {
// 登录认证
subject.login(token);
System.out.println(subject.isAuthenticated());
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("用户名错误");
} catch (IncorrectCredentialsException e) {
e.printStackTrace();
System.out.println("密码错误");
}
}
}

测试

以上代码编写完成后,我们运行CustomerRealmAuthenticatorTest里面的main方法,执行结果如下

在这里插入图片描述

Shiro的加密和随机盐

Shiro中密码的加密策略

实际应用中用户的密码并不是明文存储在数据库中的,而是采用一种加密算法将密码加密后存储在数据库中。Shiro中提供了一整套的加密算法,并且提供了随机盐。shiro使用指定的加密算法将用户密码和随机盐进行加密,并按照指定的散列次数将散列后的密码存储在数据库中。由于随机盐每个用户可以不同,这就极大的提高了密码的安全性。

Shiro中的加密方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import org.apache.shiro.crypto.hash.Md5Hash;

public class ShiroMD5Test {
public static void main(String[] args){
// MD5加密,无随机盐,无散列
Md5Hash md5Hash01 = new Md5Hash("123456");
System.out.println(md5Hash01.toHex());

// MD5+随机盐加密,无散列
Md5Hash md5Hash02 = new Md5Hash("123456","1q2w3e");
System.out.println(md5Hash02.toHex());

// MD5+随机盐加密+散列1024
Md5Hash md5Hash03 = new Md5Hash("123456","1q2w3e",1024);
System.out.println(md5Hash03.toHex());
}
}

运行结果如下

1
2
3
4
e10adc3949ba59abbe56e057f20f883e
9eab7472e164bb8c1b823ae960467f74
41a4e25bcf1272844e38b19047dd68a0
123

Shiro中自定义加密Realm

CustomerMD5Realm

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
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;

public class CustomerMD5Realm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}

// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 从token中获取用户名
String principal = (String) token.getPrincipal();
if("christy".equals(principal)){
/**
* 用户名
* 加密后的密码
* 随机盐
* 当前realm的名称
*/
return new SimpleAuthenticationInfo(principal,
"41a4e25bcf1272844e38b19047dd68a0",
ByteSource.Util.bytes("1q2w3e"),
this.getName());
}
return null;
}
}

CustomerMD5AuthenticatorTest

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
import com.christy.shiro.realm.CustomerMD5Realm;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.subject.Subject;

public class CustomerMD5AuthenticatorTest {
public static void main(String[] args) {
// 创建SecurityManager
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
// 设置自定义realm
CustomerMD5Realm realm = new CustomerMD5Realm();
// 为realm设置凭证匹配器
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
// 设置加密算法
credentialsMatcher.setHashAlgorithmName("md5");
// 设置hash次数
credentialsMatcher.setHashIterations(1024);
realm.setCredentialsMatcher(credentialsMatcher);
defaultSecurityManager.setRealm(realm);
// 设置安全工具类
SecurityUtils.setSecurityManager(defaultSecurityManager);
// 通过安全工具类获取subject
Subject subject = SecurityUtils.getSubject();
// 创建token
UsernamePasswordToken token = new UsernamePasswordToken("christy", "123456");
try {
// 登录认证
subject.login(token);
System.out.println("认证成功");
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("用户名错误");
} catch (IncorrectCredentialsException e) {
e.printStackTrace();
System.out.println("密码错误");
}
}
}

测试

在这里插入图片描述

Shiro中的授权

什么是授权

授权可简单理解为who对what(which)进行How操作:

Who,即主体(Subject),主体需要访问系统中的资源。

What,即资源(Resource),如系统菜单、页面、按钮、类方法、系统商品信息等。资源包括资源类型资源实例,比如商品信息为资源类型,类型为t01的商品为资源实例,编号为001的商品信息也属于资源实例。

How,权限/许可(Permission),规定了主体对资源的操作许可,权限离开资源没有意义,如用户查询权限、用户添加权限、某个类方法的调用权限、编号为001用户的修改权限等,通过权限可知主体对哪些资源都有哪些操作许可。

授权方式

基于角色的访问控制

RBAC基于角色的访问控制(Role-Based Access Control)是以角色为中心进行访问控制

1
2
3
4
if(subject.hasRole("admin")){
//操作什么资源
}
123

基于资源的访问控制

RBAC基于资源的访问控制(Resource-Based Access Control)是以资源为中心进行访问控制

1
2
3
4
5
6
7
if(subject.isPermission("user:update:01")){ //资源实例
//对01用户进行修改
}
if(subject.isPermission("user:update:*")){ //资源类型
//对01用户进行修改
}
123456

权限字符串

权限字符串的规则是:资源标识符:操作:资源实例标识符,意思是对哪个资源的哪个实例具有什么操作,“:”是资源/操作/实例的分割符,权限字符串也可以使用*通配符。

例子:

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

权限的编码方式

编程式

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

注解式

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

标签式

1
2
3
4
5
6
JSP/GSP 标签:在JSP/GSP 页面通过相应的标签完成:
<shiro:hasRole name="admin">
<!— 有权限—>
</shiro:hasRole>
注意: Thymeleaf 中使用shiro需要额外集成!

授权的实现

我们基于上面MD5加密的例子进行修改

CustomerMD5Realm

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
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;

public class CustomerMD5Realm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 从系统返回的身份信息集合中获取主身份信息(用户名)
String primaryPrincipal = (String) principals.getPrimaryPrincipal();
System.out.println("用户名: "+primaryPrincipal);

//根据用户名获取当前用户的角色信息,以及权限信息
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();

//将数据库中查询角色信息赋值给权限对象
simpleAuthorizationInfo.addRole("admin");
simpleAuthorizationInfo.addRole("user");

//将数据库中查询权限信息赋值个权限对象
simpleAuthorizationInfo.addStringPermission("user:*:01");
simpleAuthorizationInfo.addStringPermission("product:create");

return simpleAuthorizationInfo;
}

// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 从token中获取用户名
String principal = (String) token.getPrincipal();
if("christy".equals(principal)){
/**
* 用户名
* 加密后的密码
* 随机盐
* 当前realm的名称
*/
return new SimpleAuthenticationInfo(principal,
"41a4e25bcf1272844e38b19047dd68a0",
ByteSource.Util.bytes("1q2w3e"),
this.getName());
}
return null;
}
}

CustomerMD5AuthenticatorTest

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import com.christy.shiro.realm.CustomerMD5Realm;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.subject.Subject;

public class CustomerMD5AuthenticatorTest {
public static void main(String[] args) {
// 创建SecurityManager
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
// 设置自定义realm
CustomerMD5Realm realm = new CustomerMD5Realm();
// 为realm设置凭证匹配器
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
// 设置加密算法
credentialsMatcher.setHashAlgorithmName("md5");
// 设置hash次数
credentialsMatcher.setHashIterations(1024);
realm.setCredentialsMatcher(credentialsMatcher);
defaultSecurityManager.setRealm(realm);
// 设置安全工具类
SecurityUtils.setSecurityManager(defaultSecurityManager);
// 通过安全工具类获取subject
Subject subject = SecurityUtils.getSubject();
// 创建token
UsernamePasswordToken token = new UsernamePasswordToken("christy", "123456");
try {
// 登录认证
subject.login(token);
System.out.println("认证成功");
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("用户名错误");
} catch (IncorrectCredentialsException e) {
e.printStackTrace();
System.out.println("密码错误");
}

//授权
if(subject.isAuthenticated()){
//基于角色权限控制
System.out.println(subject.hasRole("super"));

//基于多角色权限控制(同时具有)
System.out.println(subject.hasAllRoles(Arrays.asList("admin", "super")));

//是否具有其中一个角色
boolean[] booleans = subject.hasRoles(Arrays.asList("admin", "super", "user"));
for (boolean aBoolean : booleans) {
System.out.println(aBoolean);
}

System.out.println("==============================================");

//基于权限字符串的访问控制 资源标识符:操作:资源类型
System.out.println("权限:"+subject.isPermitted("user:update:01"));
System.out.println("权限:"+subject.isPermitted("product:create:02"));

//分别具有那些权限
boolean[] permitted = subject.isPermitted("user:*:01", "order:*:10");
for (boolean b : permitted) {
System.out.println(b);
}

//同时具有哪些权限
boolean permittedAll = subject.isPermittedAll("user:*:01", "product:create:01");
System.out.println(permittedAll);
}
}
}
12

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
用户名: christy
false
用户名: christy
用户名: christy
false
用户名: christy
用户名: christy
用户名: christy
true
false
true
==============================================
用户名: christy
权限:true
用户名: christy
权限:true
用户名: christy
用户名: christy
true
false
用户名: christy
用户名: christy
true