Shiro 介绍

权限体系在现代软件应用中有着非常重要的地位。一个应用如果没有权限体系都会显着这个系统“特别不安全”,无论是传统的 MIS 系统还是互联网项目出于对业务数据和应用自身的安全,都会设置自己的安全策略。

Apache Shiro 是一个强大的并且简单使用的 Java 权限框架。主要应用认证(Authentication)、授权(Authorization)、加密(Cryptography)和 Session Manager。Shiro 具有简单易懂的 API,使用Shiro 可以快速并且简单的应用到任何应用中,无论是从最小的移动 app 到最大的企业级 web 应用都可以使用。

核心功能

Authentication 认证。如用户的登录。

Authorization 授权。用户是否有权限访问指定 URL 等。

Cryptography 密码学。如密码的加密。

Session Management Session 管理。

Web Integration Web集成。Shiro不依赖于容器。

Shiro 的架构原理

Shiro 的架构如下图所示,接下来将介绍架构的每一个部分。

shiro架构

Subject

主体。每个“用户”登录成功后都会对应一个 Subject 对象,所有“用户”信息都存放在 Subject 中。可以理解成 Subject 就是 Shiro 提供的用户实体类。没有 Subject 就没有认证这个操作。

Security Manager

Shiro 最大的容器,此客器中包含了 Shiro 的绝大多数功能。在其它项目中,获取 Security Manager 是编写代码的第一步。但是 Spring Boot 中已经帮助我们进行自动化配置了。

Authenticator

认证器。执行认证过程时所调用的组件,其中包含了认证策略。

Authorizer

授权器。执行授权时调用的组件。

Session Manager

Shiro 被 Web 集成后,HttpSession 对象会由 Shiro 的 Session Manager 进行管理。

Cache Manager

缓存管理。Shiro 执行很多第三方缓存技术,如 EHCache 等。

Session DAO

操作 Session 内容的组件。

Realms

Shiro 框架实现权限控制不依赖于数据库,通过内置数据也可以实现权限控制,比如后面提到的 ini 文件。但是目前绝大多数应用的数据都存储在数据库中,所以 Shiro 提供了 Realms 组件,此组件的作用就是访问数据库。Shiro 内置的访问数据库的代码,通过简单配置就可以访问数据库,也可以自定义 Realms 实现访问数据库逻辑。

Shiro 的 INI 文件

INI 英文名称是 Initialization file,INI 文件是 Window 系统配置文件的扩展名。Shiro 的全局配置文件就是“.ini”文件,ini 中数据都是固定数据。

.ini 文件内容的语法和 .properties 类似,都是 key=value 格式,INI 文件中包含了 4 个部分。分别是 main、users、roles、urls。

Shiro 的配置文件大致有如下内容:

[main]
# Shiro 的一些全局配置

[users]
# 用户名=密码,角色名
xiaohehe=123456,role1,role2

[roles]
# 角色名=权限名
role1=user:inseet,role2=user:update
role2=insert,update,delete,select

[urls]
# 定义哪个控制器被哪个过滤器过滤。Shiro 内置很多过滤器。此部分主要在 WEB 应用中使用
# 控制器名称(可理解成url请求路径)=过滤器名称
/login=authc
/**=anon

Shiro 内置的过滤器

过滤器分为两类:认证相关过滤器和授权相关过滤器

认证相关的过滤器

过滤器名 过滤器类 功能
anon AnonymousFilter 匿名拦截器,不需要登录即可访问的资源,匿名用户或游客,一般用于过滤静态资源。
authc FormAuthenticationFilter 需要认证登录才能访问
authcBasic BasicHttpAuthenticationFilter httpBasic 身份验证拦截器。
user UserFilter 用户拦截器,表示必须存在用户。
logout authc.LogoutFilter 退出拦截器,执行后会直接跳转到shiroFilterFactoryBean.setLoginUrl() 设置的 url

授权相关过滤器

过滤器名 过滤器类 功能
noSessionCreation NoSessionCreationFilter 阻止在请求期间创建新的会话。以保证无状态的体验
perms PermissionsAuthorizationFilter 验证用户是否拥有权限,表示需要某些权限才能通过
port PortFilter 指定请求访问的端口号,如果不匹配则跳转到登录页面
rest HttpMethodPermissionFilter 根据请求的方法
roles RolesAuthorizationFilter 角色过滤器。判断当前用户是否指定角色。
ssl SslFilter 表示安全的url请求,协议为https。

以上的权限过滤器不一要全部用,按照项目的具体情况来分配相应的权限,使用相关的过滤器即可。后面在学习到整合到 SpringBoot 的时候会抽出一些用法。

在 INI 文件中的配置时,需要按照下面的方式配置。

anon:不认证也可以访问。例如:/admin/=anon/login=anno

authc:必须认证。/**=authc ——所有的资源都认证。

authcBasic:没有参数时表示 httpBasic 认证(客户端认证方式)。

logout:退出。

noSessionCreation:新增 Filter,表示没有 Session 创建。

perms:判断是有具有指定权限。例如:/admin/user/=perms[ "per1” " per2” ]。必须同时具有给定权限才可以访问。如果只有一个权限可以省略双引号。

port:限制端口。例如:/admin/**=port[8081]。只要请求不是 8081 端口就重新发送URL到8081端口。

rest:请求方式和权限的简便写法。例如:/admin/**=rest[user],相当于/admin/** = perms[user:方式],方式是http请求的方式: post 、get 等。

roles:判断是否具有指定角色。/admin/** =roles[role1]

ssl:表示是安全的请求。协议为 https。

user:表示必须存在用户。

Shiro 基本认证流程

  1. 获取 Subject 主体,通过主体 Subject 对象的 login 方法进行登录。
  2. 把 Subject 中内容传递给Security Manager。
  3. 把 Security Manager 内部组件 Authenticator 进行认证。
  4. 使用 Realm 来认证数据,比如使用 INI Realm 来认证,调用 ini 文件中数据。

简单小试一下

1、创建一个 Maven 工程、复制以下依赖。

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-core</artifactId>
    <version>1.8.0</version>
</dependency>
<dependency>
    <groupId>commons-logging</groupId>
    <artifactId>commons-logging</artifactId>
    <version>1.2</version>
</dependency>

2、在 resources 目录下,创建一个 ini 文件。新建一个 users 选项,按照 用户名=密码 的方式在 ini 文件中添加用户。

[users]
zs=123
sxc=123
xiaohehe=123

3、创建一个 Main 方法。

package top.bestguo.shirofirst;

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.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;

public class ShiroFirstApplication {

    public static void main(String[] args) {
        System.out.println("Hello World!");
        // 1、解析 ini 文件,创建 SecurityManager 工厂
        IniSecurityManagerFactory managerFactory = new IniSecurityManagerFactory("classpath:shiro.ini");
        // 2、获得 SecurityManager 实例对象
        SecurityManager instance = managerFactory.getInstance();
        // 3、将 SecurityManager 对象保存到 SecurityUtils 中
        SecurityUtils.setSecurityManager(instance);
        // 4、获取 Subject 主题对象
        Subject subject = SecurityUtils.getSubject();
        // 5、创建令牌对象,相当于是给我们用户自己输入用户名和密码
        UsernamePasswordToken token = new UsernamePasswordToken("sxc", "1123");
        // 6、进行认证的比较(Realms)
        try {
            subject.login(token);
            System.out.println("认证成功!");
        } catch (UnknownAccountException exception) {
            System.out.println("认证失败,未知的账户!");
        } catch (IncorrectCredentialsException exception) {
            System.out.println("认证失败,不正确的口令!");
        }
    }

}

4、运行结果

// 用户名为sxc、密码为 1123
Hello World!
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
认证失败,不正确的口令!
    
// 用户名为 sc、密码为 123
Hello World!
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
认证失败,未知的账户!

// 用户名为 sxc、密码为 123
Hello World!
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
认证成功!

认证的过程

在上面的示例中,使用了 UsernamePasswordToken 类来进行用户的认证,认证原理首先是比对用户名,判断用户名是否存在,然后再判断用户的密码是否匹配。如果用户名或者密码不正确,那么则通过异常抛出的形式提示用户名或者密码是否有效。

Shiro 的授权

若要判断一个用户是否有什么权限,有两种方式,一种是利用方法返回的布尔值进行判断,另一种方法没有返回值,但是会抛出异常,通过捕获异常的方式来处理用户有哪些权限。

判断用户是否有角色以及权限,前提是用户能够正确的通过认证。

判断角色

判断角色的方法有 hasRole、hasRoles、hasAllRole、checkRole 等,一般来说,带 has 前缀的都带返回值,类型为 boolean 型。

hasRole 方法

判断当前认证成功用户是否具有某一个角色,返回一个 boolean 值。

boolean hasRole = subject.hasRole("role2");
System.out.println(hasRole);

hasRoles 方法

判断当前用户是否具有多个角色,返回一组 boolean 值。

ArrayList<String> list = new ArrayList<>();
list.add("role1");
list.add("role2");
System.out.println(Arrays.toString(subject.hasRoles(list))); // boolean[] 型

hasAllRoles 方法

判断当前用户是否包含当前列出的全部的角色。

ArrayList<String> list = new ArrayList<>();
list.add("role1");
list.add("role2");
System.out.println(subject.hasAllRoles(list));

checkPermission 方法

功能上和 hasRole 一样,只是它是通过异常抛出的形式。

try {
    subject.checkRole("fsffsdf");
}
catch (UnauthorizedException exception) {
    exception.printStackTrace();
}

判断权限

权限是包含在角色中,也就是意味着当前用户包含着这些权限,此时可以判断该用户拥有哪些可以操作的权限了。

判断权限的方法有:isPermitted、isPermittedAll、checkPermission、checkPermissions 等,使用方法也和判断角色一样,但是传入的参数是权限名称而不是角色名称。一般来说,带 is 前缀的都带返回值,类型为 boolean 型。

授权方式

授权方式分为两种,分别是基于角色的访问控制(Role-Based Access Control)和基于资源的访问控制(Resource-Based Access Control),两者简称都是 RBAC。

基于角色的访问控制

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

if(subject.hasRole("admin")) {
    // 操作什么资源
}

基于资源的访问控制

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

if(subject.isPermission("user:create:*")) {
    // 对所有的用户具有创建的权限
}
if(subject.isPermission("user:update:01")) {
    // 对01的用户具有创建的权限
}

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

user:系统中的一个模块

create:创建

*:某个资源的实例