Shiro-入门

  • Apache Shiro 是Java的一个安全(权限)框架。
  • Shiro 可以非常容易的开发出足够好的应用,其不仅可以用在 JavaSE 环境,也可以用在 JavaEE 环境。
  • Shiro 可以完成:认证、授权、加密、会话管理、与Web 集成、缓存 等。

Shiro 架构

从外部看

从外部来看Shiro ,即从应用程序角度的来观察如何使用 Shiro 完成工作:

Shiro Basic Architecture Diagram

  • Subject:应用代码直接交互的对象是Subject,也就是说 Shiro 的对外 API 核心就是 Subject。Subject代表了当前“用户”, 这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等;与Subject的所有交互都会委托给SecurityManager,Subject其实是一个门面,SecurityManager才是实际的执行者;
  • SecurityManager安全管理器;即所有与安全有关的操作都会与SecurityManager交互;且其管理着所有 Subject;可以看出它是 Shiro的核心,它负责与Shiro的其他组件进行交互,它相当于 SpringMVC 中 DispatcherServlet 的角色
  • Realm:**Shiro 从Realm获取安全数据(如用户、角色、权限)**,就是说 SecurityManager 要验证用户身份,那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法;也需要从 Realm 得到用户相应的角色/ 权限进行验证用户是否能进行操作;可以把 Realm 看成 DataSource

从内部看

Shiro Architecture Diagram

  • Subject:任何可以与应用交互的“用户”。
  • SecurityManager:相当于SpringMVC 中的 DispatcherServlet;是 Shiro 的心脏;所有具体的交互都通过 SecurityManager 进行控制;它管理着所有 Subject、且负责进行认证、授权、会话及缓存的管理。
  • Authenticator:负责 Subject 认证,是一个扩展点,可以自定义实现;可以使用认证 策略(Authentication Strategy),即什么情况下算用户认证通过了。
  • Authorizer:授权器、即访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能。
  • Realm:可以有 1 个或多个 Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是JDBC 实现,也可以是内存实现等等;由用户提供,所以一般在应用中都需要实现自己的 Realm。
  • SessionManager:管理 Session 生命周期的组件;而 Shiro 并不仅仅可以用在 Web 环境,也可以用在如普通的 JavaSE 环境。
    • SessionDAO:SessionDAO代表SessionManager执行会话持久性(CRUD)操作,这允许将任何数据存储插入会话管理基础设施。
  • 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
27
28
29
30
31
32
33
34
35
36
37
38
<?xml version="1.0" encoding="UTF-8"?>
<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.monochrome</groupId>
<artifactId>shiro-demo</artifactId>
<version>1.0-SNAPSHOT</version>

<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-all</artifactId>
<version>1.9.1</version>
<type>pom</type>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-reload4j</artifactId>
<version>2.0.2</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
</dependencies>
</project>

INI文件

可以将用户信息存放在shiro.ini配置文件当中

1
2
3
[users]
zhangsan=z3
lisi=l4

登录认证

登录认证概念

  1. 身份验证:一般需要提供如身份ID等一些标识信息来表明登录者的身份,如提供 email,用户名/密码来证明。
  2. 在shiro中,用户需要提供principals(身份)和credentials(证明)给shiro,从而应用能验证用户身份。
  3. principals:身份,即主体的标识属性,可以是任何属性,如用户名、邮箱等,唯一即可。一个主体可以有多个principals,但只有一个Primary principals,一般是用户名/ 邮箱/手机号。
  4. credentials:证明/凭证,即只有主体知道的安全值,如密码/数字证书等。
  5. 最常见的principals和credentials组合就是用户名/密码

登录认证基本流程

  1. 收集用户身份/凭证,即如用户名/密码
  2. 调用 Subject.login 进行登录,如果失败将得到相应 的 AuthenticationException 异常,根据异常提示用户错误信息;否则登录成功
  3. 创建自定义的 Realm 类,继承 org.apache.shiro.realm.AuthenticatingRealm类, 实现 doGetAuthenticationInfo() 方法

Shiro Authentication Sequence

登录认证实例

创建测试类,获取认证对象,进行登录认证,如下:

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 class ShiroRun {
public static void main(String[] args) {
// 1.初始化获取SecurityManager
IniSecurityManagerFactory factory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
// 2.获取subject对象
Subject subject = SecurityUtils.getSubject();
// 3.创建token对象,web应用用户名密码从页面传递
UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", "z3");
// 4.完成登录
try {
subject.login(token);
System.out.println("login successful");
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("user dose not exist");
} catch (IncorrectCredentialsException e) {
System.out.println("password is wrong");
e.printStackTrace();
} catch (AuthenticationException e) {
e.printStackTrace();
System.out.println("login failed");
}
}
}

身份认证流程

  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,将按照相应的顺序 及策略进行访问

角色、授权

授权概念

  1. 授权,也叫**访问控制,即在应用中控制谁访问哪些资源(如访问页面/编辑数据/页面操作等)**。在授权中需了解的几个关键对象:主体(Subject)、资源(Resource)、权限 (Permission)、角色(Role)。
  2. **主体(Subject)**:访问应用的用户,在 Shiro 中使用 Subject 代表该用户。用户只有授权后才允许访问相应的资源。
  3. 资源(Resource)在应用中用户可以访问的 URL,比如访问 JSP 页面、查看/编辑 某些 数据、访问某个业务方法、打印文本等等都是资源。用户只要授权后才能访问。
  4. 权限(Permission):安全策略中的原子授权单位,通过权限我们可以表示在应用中用户有没有操作某个资源的权力。即权限表示在应用中用户能不能访问某个资源,如:访问用 户列表页面查看/新增/修改/删除用户数据(即很多时候都是CRUD(增查改删)式权限控制)等。权限代表了用户有没有操作某个资源的权利,即反映在某个资源上的操作允不允许。
  5. Shiro 支持粗粒度权限(如用户模块的所有权限)和细粒度权限(操作某个用户的权限, 即实例级别的)
  6. 角色(Role)权限的集合,一般情况下会赋予用户角色而不是权限,即这样用户可 以拥有 一组权限,赋予权限时比较方便。典型的如:项目经理、技术总监、CTO、开发工程师等 都是角色,不同的角色拥有一组不同的权限

授权方式

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

    1
    2
    3
    4
    5
    if(subject.hasRole("admin")){
    // 有权限
    }else{
    // 没权限
    }
  2. 注解式:通过在执行的Java方法上放置相应的注解完成,没有权限将抛出相应的异常

    1
    2
    3
    4
    @RequiresRoles("admin")
    pubic void hello(){
    // 有权限
    }
  3. JSP/GSP 标签:在JSP/GSP 页面通过相应的标签完成

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

授权流程

  1. 首先调用Subject.isPermitted/hasRole接口,其会委托给SecurityManager,而 SecurityManager接着会委托给 Authorizer;
  2. Authorizer是真正的授权者,如果调用如isPermitted(“user:view”),其首先会通 过PermissionResolver把字符串转换成相应的Permission实例;
  3. 在进行授权之前,其会调用相应的Realm获取Subject相应的角色/权限用于匹配传入的角色/权限;
  4. Authorizer会判断Realm的角色/权限是否和传入的匹配,如果有多个Realm,会委托 给ModularRealmAuthorizer进行循环判断,如果匹配如isPermitted/hasRole 会返回 true,否则返回false表示授权失败

ShiroAuthorizationSequence

授权实例

  1. 获取角色信息

    1. 给shiro.ini增加角色配置[users]

      1
      2
      zhangsan=z3,admin,user
      lisi=l4
    2. 给例子添加代码,沟通过hasRole()判断用户是否有指定角色

      1
      2
      3
      4
      5
      6
      7
      try {
      subject.login(token);
      System.out.println("login successful");
      // 判断角色
      boolean isAdmin = subject.hasRole("admin");
      System.out.println("has admin role? " + isAdmin);
      }
  2. 判断权限信息信息

    1. 给shiro.ini增加权限配置

      1
      2
      [roles]
      admin=user:insert,user:select
    2. 给例子添加代码,判断用户是否有指定权限

      1
      2
      3
      4
      // 判断权限
      boolean isPermitted = subject.isPermitted("user:insert");
      System.out.println("has this permission? " + isPermitted);
      // 也可以用 checkPermission 方法,但没有返回值,没权限抛 AuthenticationException subject.checkPermission("user:select");

Shiro 加密

实际系统开发中,一些敏感信息需要进行加密,比如说用户的密码。Shiro 内嵌很多常用的加密算法,比如 MD5 加密。Shiro 可以很简单的使用信息加密。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ShiroMD5 {
public static void main(String[] args) {
// 密码明文
String password = "z3";
// 使用 md5 加密
Md5Hash md5Hash = new Md5Hash(password);
System.out.println("md5 加密:" + md5Hash.toHex());
// 带盐的 md5 加密,盐就是在密码明文后拼接新字符串,然后再进行加密
Md5Hash md5Hash2 = new Md5Hash(password, "salt");
System.out.println("md5 带盐加密:" + md5Hash2.toHex());
//为了保证安全,避免被破解还可以多次迭代加密,保证数据安全
Md5Hash md5Hash3 = new Md5Hash(password, "salt", 3);
System.out.println("md5 带盐三次加密:" + md5Hash3.toHex());
// 使用父类实现加密
SimpleHash simpleHash = new SimpleHash("MD5", password, "salt", 3);
System.out.println("父类带盐三次加密:" + simpleHash.toHex());
}
}

Shiro 自定义登录认证

Shiro 默认的登录认证是不带加密的,如果想要实现加密认证需要自定义登录认证,自定义 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
36
37
38
39
40
package com.monochrome.custom;

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.crypto.hash.SimpleHash;
import org.apache.shiro.realm.AuthenticatingRealm;
import org.apache.shiro.util.ByteSource;

/**
* @author monochrome
* @date 2022/10/7
*/
public class CustomRealm extends AuthenticatingRealm {
// 自定义的登录认证方法,Shiro 的 login 方法底层会调用该类的认证方法完成登录认证
// 需要配置自定义的 realm 生效,在 ini 文件中配置,或 Springboot 中配置
// 该方法只是获取进行对比的信息,认证逻辑还是按照 Shiro 的底层认证逻辑完成认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 1 获取身份信息
String principal = authenticationToken.getPrincipal().toString();
// 2 获取凭证信息
String password = new String((char[])
authenticationToken.getCredentials());
System.out.println("认证用户信息:" + principal + "---" + password);
// 3 获取数据库中存储的用户信息
if (principal.equals("zhangsan")) {
// 3.1 数据库存储的加盐迭代 3 次密码
SimpleHash simpleHash = new SimpleHash("MD5", password, "salt", 3);
// 3.2 创建封装了校验逻辑的对象,将要比较的数据给该对象
AuthenticationInfo info = new SimpleAuthenticationInfo(
authenticationToken.getPrincipal(),
simpleHash,
ByteSource.Util.bytes("salt"), authenticationToken.getPrincipal().toString());
return info;
}
return null;
}
}

在custom_shiro.ini中添加配置信息

custom_shiro.ini

1
2
3
4
5
6
7
8
9
10
11
[main]
md5CredentialsMatcher=org.apache.shiro.authc.credential.Md5CredentialsMatcher
md5CredentialsMatcher.hashIterations=3
customRealm= com.monochrome.custom.CustomRealm
customRealm.credentialsMatcher=$md5CredentialsMatcher
securityManager.realms=$customRealm
[users]
zhangsan=7174f64b13022acd3c56e2781e098a5f,admin
lisi=l4
[roles]
admin=user:insert,user:select