目录
导读
本文用来记录学习Shiro安全框架,初衷是尽可能的小而全,短而精。但是文笔有限,建议看完本文以后阅读官方文档伴读。文档内容基本来自官网加上一点点一些自身理解,希望不要误导你。文中学习Shiro的重点是与Spring进行集成,如果你想看关于ini配置文件的内容请查阅官方文档。
阅读人群
如果你没有接触过Shiro或者其他安全框架建议从头部根据顺序看起,这样可能更容易理解。建议先了解相关术语,方便后面的阅读。举了一些例子用于说明这些术语可能不恰当。
如果你已经对Shiro有过一些了解可以根据目录接口选看。
如果你对Shiro足够了解,希望可以找到一些代码片段或者是想复制代码可以进入仓库(代码还没有写完无法跳转)。或者目录跳转到相关代码部分。
术语
- Authentication:身份验证,验证主题身份的过程 - 用来验证某个人确实是他所说的人,例如 张三说他是张三 我们需要查看他的身份证查验这个人确实是张三,而不是随便一个人说他自己是张三就是张三。
- Authorization:授权/访问控制,访问控制,用来验证某个主体或者人是否拥有某种权限,例如张三说天安门是他的不动产,我们需要验证天安门是否是他的不动产,如果张三有天安门的产权证那么天安门就是他的,这个过程就是访问控制。身份验证和访问控制的单词很像很容易混淆,其实根据举例应该可以区分这两个的区别, 身份验证用来验证某个主体是否是合法的,访问控制用来验证合法的主题是否拥有某种权限。
- Cipher:密码 用来执行加解密的算法
- Credential:凭据,用来验证主体身份的一条信息,身份验证期间,将一条或多条凭据和主体一起提交,与身份验证例子中的身份证一样。
- Cryptography:加密包 消息摘要,加密的一个过程和cipher 是分开的,暂时不知道区别
- Hash:哈希 使用Hash算法进行消息摘要
- Permission:权限 用于声明某个方法或者行为需要什么权限,安全策略中的最低级别,当需要验证权限时,就已经完成身份验证,角色验证了。实际就是一个字符串。例如在
- Principal:主要的标识 可以理解为用户的标识,类似于键值对的Key,根据key就可以从其他地方获取Value 避免主体过大
- Realm:数据源 数据访问对象,通过Realm对象获取用户的信息,底层用户可能会存储Ldap或Mariadb数据库中,通过Realm接口可以屏蔽底层数据库的差异,让获取用户的信息具有统一性
- Role:角色 相比于权限,可以把角色理解为一个权限的集合,例如张三需要打扫一座大楼的卫生,大楼的每层需要开门权限才可以开门进去打扫卫生,如果每层都给张三进行授权,权限太多且不好管理,我们使用角色进行分配的话,给张三一个保洁的角色就可以开启大楼的每层大门
- Session:会话 一段时间内与软件系统交互的单个用户/主题相关联有状态的数据上下文,举个例子我们去游乐园需要购买门票,游乐园有的项目只允许进去一次,所以游玩这些项目的时候门票会被扣个洞,用于记录我们已经体验过这个项目了,我们出游乐园的时候会回收门票,这个门票这就可以理解为数据上下文信息。
- Subject:主体 访问项目的不单单是指用户,也有可能是一段程序,主体用来表示这个意思
介绍
Apache Shiro 是一个强大而灵活的开源安全框架,可以干净地处理身份验证、授权、企业会话管理和加密。 首要目标是易于使用和理解。安全性有时可能非常复杂,甚至很痛苦,但并非必须如此。框架应尽可能掩盖复杂性,并公开一个干净直观的API,以简化开发人员确保其应用程序安全的工作。
使用Shiro可以做什么?
- 对用户进行身份验证以验证其身份
- 对用户执行访问控制,例如:
- 确定是否为用户分配了特定安全角色
- 确定是否允许用户执行某些操作
- 在任何环境中使用会话 API,即使没有 Web 或 EJB 容器也是如此。
- 在身份验证、访问控制或会话生存期内对事件做出反应。
- 聚合 1 个或多个用户安全数据源,并将其全部呈现为单个复合用户“视图”。
- 启用单点登录 (SSO) 功能
- 启用“记住我”服务以进行用户关联,而无需登录
-
...等等 - 全部集成到一个有凝聚力的易于使用的 API 中。
功能
主要的
- Authentication 用户的认证
- Authorization 访问控制
- Session Management 会话管理,即使在非Web 或EJB应用程序也是可以使用
- Cryptography 丰富的加密技术
附加的
- Web Support Shiro的Web支持Api 帮助轻松的保护Web应用程序
- Caching Shiro的一级缓存,确保安全操作的同时保证快速高效
- Concurrency 支持多线程应用程序的并发功能
- Testing 支持单元和继承测试
- Run As 允许用户可以使用其他用户的身份的功能
- Remember 记住我,跨会话记住用户的身份
架构
从架构上可以分为三个概念,subject、securityManager、reaim。Subject 可以理解为用户,实际不止代表用户也有可能是第三方服务、一个程序、定时任务等等,与软件进行交互的"实体"都可以称为Subject。securityManager 架构的核心,通过配置各种对象来完成业务逻辑,可以理解为钢铁侠的反应炉。Realm Shiro与数据源之间的桥梁,用于获取用户的数据。
为了提供灵活的配置和可插拔性,在设计上采用高度模块化,甚至SecurityManager实现也没有做什么具体的事情,反而是将所有行为委托给各个嵌套/包装的组件进行。
简单的架构图
详细的架构图
- Subject 软件交互的实体(用户、第三方服务、内外部程序)
- Security Manager Shrio的架构核心,负责协调调度其他组件
- authenticator 负责执行和响应用户身份的组件,当用户尝试登录时,由该逻辑执行
- authenticationStrategy 身份验证策略,如果配置多个(Realm)数据源,用于管理Realm的策略,例如一个realm中的用户数据与当前需要验证的用户数据对应上了就代表对应成功,还是所有的Realm都对应上才算成功
- Authorizer 负责确定用户在应用程序中访问控制的组件,用于验证用户的角色和权限
- SessionManager 创建和管理用户的生命周期,可以在没有Web/Servlet 或EJB容器的情况下,管理本地用户会话
- SessionDAO 持久性的管理用户的会话
- CacheManager 创建和管理其他Shiro组件使用的实例生命周期,因为Shiro可以访问许多后端数据源进行身份验证、授权和会话管理。
- Cryptography 加密包,对其他加密方式进行封装,让开发者更为简单使用加密功能
- Realms 充当Shiro和数据源之间的桥梁,用于获取底层实际用户数据。
认证
验证身份的过程,让用户证明自己的身份。需要提供主体和凭据提交给Shiro来完成验证。
主体:标识属性,例如用户的姓名、用户名、身份证号码或定义的某种的Id这种唯一值。
凭据:一般指密码或指纹、视网膜,证书等。只有用户自己知道的秘密
主体/凭据 一般使用的都是 用户名/密码
- 验证主体 - 身份验证可以分为三个步骤
- 收集使用者提交的主体和凭据
- 对主体和凭据进行校验
- 通过则放行,否则重新让使用者提交或阻止访问
认证的步骤
- Step1 接收到使用者提交的主体/凭据并产生一个实例对象,将主体/凭据转为Token对象
- Step2 实例对象调用核心程序(Security Manager)方法将Token对象交给SecurityManager
- Step3 身份验证组件接收Token对象,将Token对象委托给内部的身份验证器实例
- Step4 如果配置了多个数据源,身份验证实例使用配置的身份验证策略启动多身份验证,在身份验证前中后期间,可以调用多个Realm获取库中的数据并给出验证结果。
- Step5 检查每个Realm是否已经调用到。
访问控制
管理资源访问的过程,控制谁有权限可以访问
授权三要素
权限、角色、用户
权限
权限代表安全策略的原子性的元素,基本是行为限制,可以明确标识执行的操作,一般用来Controller层的方法。设置权限的基本思想至少基于资源和操作。
权限可以理解就是为一个普通的字符串,在校验用户权限时进行对比字符串。
权限的形式https://shiro.apache.org/permissions.html
角色
角色是一个命名实体,代表一组权限的集合。角色分配给用户进行关联。
- 隐式角色 不容易看到更加详细的信息
- 显示角色 更容易看到详细的访问限制信息
新的 RBAC:基于资源的访问控制https://stormpath.com/blog/new-rbac-resource-based-access-control
用户
使用者对应数据源中的用户,数据模型可以准确的定义如果允许A执行或者不执行某种操作
访问控制的方式
访问控制支持三种方式进行检查用户的权限。
基于编程的方式
角色检查
public void role(){
Subject subject = SecurityUtils.getSubject();
if (!subject.hasRole("admin")) {
throw new RuntimeException("此资源需要admin角色才能访问");
}
}
Boolean hasRole(String roleName) | 当前用户是否分配了指定的角色,分配了返回true,否则返回false |
Boolean[] hasRoles(List<String> roleNames) | 返回是否分配的Boolean结果数组 |
Boolean hasAllRoles(Collection<String> roleNames) | 是否分配了所有角色 |
角色断言
public void role(){
Subject subject = SecurityUtils.getSubject();
subject.checkRole("admin");
}
checkRole(String roleName) | 如果没有分配角色则抛出AuthorizationException异常 |
checkRoles(Collection<String> roleNames) | 如果没有分配给定的角色集合中的任意一个则抛出异常,异常同上 |
checkRoles(String… roleIdentifiers) | 同上方法,支持可变类型的方式 |
权限检查
public void per(){
Permission printPermission = new PrinterPermission("laserjet4400n", "print");
Subject currentUser = SecurityUtils.getSubject();
if (currentUser.isPermitted(printPermission)) {
//show the Print button
} else {
//don't show the button? Grey it out?
}
Subject currentUser = SecurityUtils.getSubject();
if (currentUser.isPermitted("printer:print:laserjet4400n")) {
//show the Print button
} else {
//don't show the button? Grey it out?
}
Subject currentUser = SecurityUtils.getSubject();
Permission p = new WildcardPermission("printer:print:laserjet4400n");
if (currentUser.isPermitted(p) {
//show the Print button
} else {
//don't show the button? Grey it out?
}
}
有该类权限返回true 反之 false | |
返回一个Boolean结果数组 | |
拥有全部权限返回true 反之 false |
权限的断言
checkPermission(Permission p) | 没有包含的权限则抛出AuthorizationException异常 |
checkPermission(String perm) | 同上 |
checkPermissions(Collection<Permission> perms) | 必须包含所有指定的权限,否则抛出异常 |
checkPermissions(String… perms) | 同上 |
基于注解的方式
AspectJ示例应用程序https://github.com/apache/shiro/tree/main/samples/aspectj
Spring 集成https://shiro.apache.org/spring-framework.html
Guice 集成https://shiro.apache.org/guice.html
前端标签库
JSP/GSP 标签库https://shiro.apache.org/web.html#tag_library
访问控制的执行顺序
- Step1 Subject调用.hasRole方法(其他方法..)或者通过注解的方式需要进行访问控制
- Step2 交由SecurityManager进行处理
- Step3 SecurityManager不做具体处理,委托给Authorizer组件进行具体判定,默认由ModularRealmAuthorizer 实例处理。
- Step4 检查每个配置Realm,从数据源获取用户数据后对比权限
- 如果Realm实现了Authorizer接口则会执行对应重写的方法,如果没有实现该接口则不执行。
- 如果重写的方法抛出异常,异常会作为AuthorizationException传播给调用者。如果是false则将不执行其他Realm中重写的方法。默认情况下一个Realm允许后将代表主体是允许的。
配置全局权限解析器
默认情况下会将字符串的权限转化为实例对象进行处理,例如源码默认使用WildcardPermissionResolver,且需要实现 PermisionResolverAware xx
配置全局角色权限解析器
实现RolePermissionResolver 、全局则加上实现 RolePermisionResolverAware 这个接口。
领域 Realm
可以访问特定应用程序的安全数据(用户、角色和权限)的组件。将应用程序的安全数据转化为Shiro理解的格式。
Realm 通常和数据源(数据库、LDAP、文件系统等)具有一对一的关系。所以接口的实现特定于数据库API来获取授权数据(JDBC、Hibernate,JPA)或者其他能获取数据库的API
Realm 本质是一个特定于安全的DAO
因为大多数数据都在数据库或存储服务器中,所以每个Realm都可以执行身份验证和授权操作。
领域认证
执行顺序
- Step1 检查识别用户信息,例如Id等
- Step2 根据识别的用户信息从数据源中获取用户信息(权限、角色等)
- Step3 检查用户提交的凭证和数据源的凭证是否匹配,不匹配则抛出异常处理
- Step4 返回一个AuthenticationInfo 实例,该实例为Shiro理解的格式封装用户数据
以上操作仅仅是最基本的工作流。实际可以在这个过程中添加你想做的步骤,例如添加日志信息等。
构造Realm
直接实现Realm接口非常耗时且容易出错,一般推荐继承AuthorizingRealm 进行,而不是从头开始。
凭据匹配
上面的流程中,Realm必须验证数据库中的凭证和用户提交的凭证是否一致。匹配则成功,反之失败。
凭据匹配过程在所有应用程序中几乎相同,一般只是比较的数据有差异。为了确保这个过程是可以插入的,可以再必要的时候进行定制。通过AuthenticatingRealm 及其子类支持 CredentialsMatcher (提供了一些常用算法的比较例如 MD5、Hash等)的概念来执行凭证比较。
Realm myRealm = new com.company.shiro.realm.MyRealm();
CredentialsMatcher customMatcher = new com.company.shiro.realm.CustomCredentialsMatcher();
myRealm.setCredentialsMatcher(customMatcher);
领域访问控制
基于角色的访问控制
- Step1 Subject 委托给SecurityManager 用于确认是否分配了给定的角色
- Step2 SecutiryManager 委托给Authorizer
- Step3 然后Authorizer逐个引用所有授权领域,直到找到分配给主题给定的角色,如果都没有找到则拒绝访问
- Step4 领域访问控制,根据getRoles()方法获取分配给主体的所有角色
- 如果 AuthorizationInfo.getRoles 调用返回的角色列表中找到给定的角色,则授予访问权限
基于权限的访问控制
- 在当主体调用重载方法isPermitted() / checkPermission()时
- Step1 Subject委托SecurityManager
- Step2 SecurityManager委托给Authorizer
- Step3 然后Authorizer逐个引用所有授权领域,直到找到分配给主题给定的角色,如果都没有找到则拒绝访问
- Step4 Realm 会执行以下操作检查是否允许主体
- Step4.1 首先通过调用 AuthorizationInfo 上的 getObjectPermissions()和 getStringPermissions 方法并聚合结果来直接识别分配给 Subject 的所有权限。
- Step4.2 如果配置了全局角色权限解析器,将会通过调用RolePermissionResolver.resolvePermissionsInRole()
- Step4.3 对于来自a和b的聚合权限,则调用 implies()方法来检查这些权限中的任何一个是否隐含了已检查的权限,具体查看通配符权限。
通配符权限https://shiro.apache.org/permissions.html#wildcard_permissions
会话管理
Shiro提供了完整的会话管理,从简单的命令行和企业集群Web应用程序。这个是Shiro单独的会话支持而不是Web容器中部署的应用程序或者EJB有状态的会话Bean。可以提供以下功能。
- 基于POJO/J2SE Shiro所有内容都是基于接口,并且通过POJO实现。这就意味着可以使用JavaBeans兼容的配置格式(JSON、YAML、springXML等)配置所有会话组件。轻松扩展Shiro的组件或者根据需求编写自己的组件,来完成会话管理。
- 自定义会话存储 由于会话对象是基于POJO的,所以会话数据可以存储到任意数量的数据源中,意味可以准确自定义会话数据的位置,例如数据库、NOsql、文件系统、内存等
- 独立于容器的集群 支持使用任何现成的网络缓存产品集群,例如Ehcache + Terracotta,Coherence 等,意味着配置一次会话集群,无论部署到哪台容器,会话都会以相同的方式进行集群化
- 异构客户端访问 Shiro会话可以在各种客户端之间"共享"。
- 事件侦听器 可以在会话的生命周期侦听事件,例如会话过期时做某种操作
- 主机地址保留 会话会保留用户的主机IP或者主机名
- 不活动/不过期 延长用户会话的事件touch()
- Web使用 支持Servlet2.5 规范。可以在Web应用程序中使用Shiro会话,不需要更改任何web代码
- SSO 因为基于POJO,所有会话可以存储到任意数据源,这样就可以在应用程序之间"共享",称之为简单版SSO。
使用会话
Subject currentUser = SecurityUtils.getSubject();
Session session = currentUser.getSession();
session.setAttribute( "someKey", someValue);
Subject.getSession(boolean create)
如果有一个,则忽略Boolean参数返回当前会话
如果没有则 Boolean为true 则新建一个 Boolean为false 不创建会话
会话管理器
管理应用程序中所有主体的会话,创建、删除、不活动和验证等,和Shiro中的其他核心架构组件一样,由SecurityManager配置。默认实现DefaultSessionManager
会话超时
默认情况下30分钟的会话超时时间,可以通过globalSessionTimeout设置超时时间。
侦听器
通过实现SessionListener 或者 SessionListenerAdapter 可以在发生重要会话事件时做某种操作。
会话存储
每次创建或更新会话时,都可以将数据保存到存储位置,已便应用程序可以访问到,通过实现SessionDAO 后交由SessionManager即可。如果不准备实现自己Dao层接口,可以使用 EHCache SessionDao
会话管理|阿帕奇四郎 (apache.org)https://shiro.apache.org/session-management.html
Web的支持
根据以上的学习,下面进入进阶学习,关于Web支持相关文档请查阅官方资料,本文重点主要以Spring集成相关。
Apache Shiro Web Support | Apache Shirohttps://shiro.apache.org/web.html
其他功能
缓存
保障安全操作尽可能快,缓存作为一个概念是Shiro的基本组成部分,但是实施完整的缓存机制将超出一个安全框架的范围,所以Shiro只是提供了一个抽象的接口,实际由用户去定义生产级别的缓存机制。
- CacheManager
- 缓存管理器组件,将返回实例 Cache
- Cache
- 维护键值对
- CacheManagerAware
- 由希望接收和使用CacheManager的组件实现
A 返回实例和Shiro组件根据需要使用这些实例来缓存。所有实现Shiro组件将自动实现接受已配置的,用于获取实例
Shiro SecurityManager 实现和 所有 AuthenticatingRealm 和 AuthorizingRealm 实现都实现了 CacheManagerAware。如果将 设置为 ,它将依次将其设置为 实现 CacheManagerAware 的各种 Realm 。
//TODO 缓存 缓存了什么数据怎么获取 怎么存储
主体
自定义主体 (程序运行时)https://shiro.apache.org/subject.html
Spring集成
将Apache Shiro集成到基于Spring的应用程序中|阿帕奇Shirohttps://shiro.apache.org/spring-framework.html
环境配置
Shiro依赖
<!-- Shiro 依赖 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.8.0</version>
</dependency>
完整依赖
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.8.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.my</groupId>
<artifactId>MyShiro</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>MyShiro</name>
<description>MyShiro</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Shiro 依赖 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.8.0</version>
</dependency>
<!-- 使用Thymeleaf整合Shiro标签 -->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<version>2.2.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>