文章目录
01 | Shiro框架介绍
- 知识点
- 什么是 Shiro
- Shiro 三大关键组件:Subject,SecurityManager 和 Realm
- Shiro 的体系结构
1. 什么是Shiro
-
Apache Shiro 是一个开源安全框架,它为开发人员提供了直观简洁的 API 来支持应用程序中的四个安全性基石:
- 身份验证(Authentication)。
- 授权(Authorization)。
- 会话管理(Session Management)。
- 加密(Cryptography)。
-
Shiro 可实现的功能
管理员可以为应用系统创建不同的角色,角色可以绑定一组权限,之后管理员可以为不同用户分配一个或多个角色,并在应用正常提供服务期间在后台管理网页中随时更改所有这些功能。
2. Shiro 关键概念和术语
- 认证
(Authentication)
认证是验证用户身份的过程,在用户访问系统时,通过比对用户提供的身份信息和预先存储在系统中的身份信息,确定用户是其“本人”。 (登陆) - 授权
(Authorization)
授权,也称为访问控制,是通过检查用户的角色和权限确定用户是否被允许做某事的过程。 - 凭证
(Credential)
凭证是用来核实用户身份的一类信息,在验证尝试期间,凭证与用户信息一起提交,应用程序对用户提交的凭证进行验证以确定是相关用户。凭证通常是非常秘密的东西,只有特定的用户才会知道,一般为密码,密钥以及一些生物识别数据,如指纹、面容等。 - 权限
(Permission)
权限是一个描述应用程序中原始功能的声明,权限是安全策略中最底层的结构,它们只定义了应用程序可以做“什么”,而不描述“谁”能够执行这些操作,权限只是一个行为声明。 - 用户标识
(Principal)
Principal 是被应用程序用来识别用户身份的任何具有唯一性的属性,用户标识可以是任何对系统而言“有意义”的东西,如用户名、手机号、用户 ID 等。 - 领域
(Realm)
Realm 是用户身份信息,权限,角色信息的提供者。Realm 从系统数据源(JDBC,File IO 或 LDAP 等)中获取数据,转交给 Shiro 以进行身份认证和权限管理,实际开发中一般不会直接实现 Realm 接口,而是基于其子类 AuthenticatingRealm 或 AuthorizingRealm 进行扩展,以减少工作量。 - 角色
(Role)
可以看成是一组权限的集合,如果一个用户的角色是 A,那就意味着他拥有角色 A 涵盖的所有权限。 - 会话
(Session)
会话是一个有状态的数据上下文,它与在一段时间内和应用程序交互的单个用户相关联。当用户登陆系统时,会话被创建,我们可以将数据保存到会话中,并在需要时从会话中读取这些数据,当用户主动登出系统或由于长时间不与系统交互而导致超时被动登出系统时,会话数据将被销毁。 - 主题
(Subject)
Subject 只是一个安全术语,一般表示系统用户的一个特定安全 “视图”,Subject 并不总是指代一个人,它也可以代表一个调用系统服务的外部进程,比如在一段时间内会间歇性执行某些任务的守护类系统账户(比如 cron 作业),基本上它是任何实体的代表,这些实体会与应用程序一起做一些事情。
3. Shiro的体系结构
-
如图:shiro的最上层主要有三个组件
-
Subject
:表示会与应用程序交互的任何东西。Subject
实例都强制绑定到SecurityManager
上,当我们调用Subject
的方法时,具体的操作最终都会转交给相关的SecurityManager
进行处理。 -
SecurityManager
:核心,负责协调与其他组件的关系一旦为应用程序配置了
SecurityManager
,通常就不需要再理会它,我们只需要和Subject
的 API 进行交互就可以了。 -
Realm
:与应用程序数据之间交换的“桥梁”, 当 Shiro 真正需要与安全性相关的数据(例如用户帐户,用户角色,权限等)进行交互以执行身份验证和授权时,Shiro 会从一个或多个为应用程序配置的Realm
中查找这些数据。可以简单地把
Realm
理解为特定于安全性的DAO
,它封装了对安全性相关数据的访问,在配置 Shiro 时,我们必须至少指定一个Realm
以用于身份验证(或授权)。
SecurityManager实际上并没有做什么实质的东西,因为shiro是高度模块化的,所以SecurityManager相当于一个容器,将几乎所有的行为委托给封装在内部的其它组件。
- shiro核心体系结构如下:
- 关于身份认证器(
Authenticator
):负责登录功能验证,Authenticator
知道如何与一个或多个Realm
进行协调,从这些Realm
中获取用户的身份信息,然后完成身份认证过程。 - 访问控制器(
Authorizer
) : 负责权限验证,Authorizer
也知道如何与多个Realm
协调,以获取角色和权限信息,Authorizer
使用这些信息来确定用户是否被允许执行某个动作。
02 | Shiro身份认证(Authentication)
-
主要功能特性:
-
一切基于当前用户:即都是基于Subject,所以在代码中任何地方都能很容易的获取到当前用户(
SecurityUtils.getSubject()
)。 -
仅一个方法调用就可以完成用户认证:即:
currentUser.login(token)
-
有快速定位的能力:shiro提供了异常定义,如:
- UnknownAccountException: 未知账号。
- IncorrectCredentialsException:密码错误。
- LockedAccountException:账号被冻结。
这几个异常类型可能会在登录失败时抛出,根据具体异常类型,我们能够快速定位问题。
-
用户身份信息来源可定制化:要对用户进行认证,那么就需要获取到用户的真实身份信息。在 Shiro 中,这部分数据的提供者为 Realm,Realm 充当着 Shiro Authentication 与系统数据源交互的重任,应用需要通过实现 Realm 接口的方式将从系统数据源(JDBC,File IO 或 LDAP 等)中获取到的用户身份信息转交给 Shiro,Shiro Authentication 才能进行身份认证。
-
-
与身份验证有关的概念术语:
- Principal:用户标识
- Credential:凭证
- Authentication Token:用户标识与凭证的载体,SecurityManager 的
login(token)
方法内部会直接将 AuthenticationToken 交给 Authenticator,由 Authenticator 最终执行身份验证/登录过程。在一般开发过程中我们使用最多的是 UsernamePasswordToken,它是 AuthenticationToken 的一个具体实现。
-
原生API实现登录功能:
-
创建一个maven项目,目录结构如下图所示
-
修改pom.xml文件
<dependencies> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.4.1</version> </dependency> <dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>1.2</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.0</version> <configuration> <source>1.6</source> <target>1.6</target> <encoding>UTF-8</encoding> </configuration> </plugin> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>exec-maven-plugin</artifactId> <version>1.1</version> <executions> <execution> <goals> <goal>java</goal> </goals> </execution> </executions> <configuration> <classpathScope>test</classpathScope> <mainClass>com.sample.App</mainClass> </configuration> </plugin> </plugins> </build>
-
resource下新建文件shiro.ini
[users] root = secret_pwd guest = guest_pwd
-
java包下新建类Demo
import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.*; import org.apache.shiro.config.IniSecurityManagerFactory; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.session.Session; import org.apache.shiro.subject.Subject; import org.apache.shiro.util.Factory; public class Demo { public static void main(String[] args) { IniSecurityManagerFactory factory = new IniSecurityManagerFactory("classpath:shiro.ini"); SecurityManager securityManager = (SecurityManager) factory.getInstance(); SecurityUtils.setSecurityManager((org.apache.shiro.mgt.SecurityManager) securityManager); // 获取当前用户 Subject currentUser = SecurityUtils.getSubject(); if (!currentUser.isAuthenticated()) { // 使用用户名和密码进行登录 UsernamePasswordToken token = new UsernamePasswordToken("guest", "guest_pwd"); token.setRememberMe(true); try { currentUser.login(token); System.out.println("登录成功"); } catch (UnknownAccountException uae) { System.out.println("用户名为 [" + token.getPrincipal() + "] 的用户不存在"); } catch (IncorrectCredentialsException ice) { System.out.println("密码错误"); } catch (LockedAccountException lae) { System.out.println("用户已被冻结"); } catch (AuthenticationException ae) { System.out.println("未知错误: " + ae); } } } }
-
运行,控制台打印出相关信息。
-
03 | Shiro授权 (Authrization)
-
主要功能特性:
- 一切基于当前用户:我们在代码的任何地方都能很容易的获取到当前用户(
SecurityUtils.getSubject()
),并对其身份信息和权限信息进行检索。 - 支持基于角色和基于权限的授权方式:在开发时,可选择一种或同时选择两种。
- 提供多种方式来实践权限控制:Subject对象、注解、AOP、以及JSP Taglibs
- 支持任何数据模型:shiro进行权限检查时只关心最终答案:是否有权限
- 一切基于当前用户:我们在代码的任何地方都能很容易的获取到当前用户(
-
关键概念与术语:
- 权限 (
Permission
) - 角色 (
Role
) - 用户 (
Users
) : Shiro中的用户指的是Subject,应用程序中用User来指代用户。
- 权限 (
-
基于用户角色的授权:通过检查用户是否拥有某一角色而确定其是否能够执行具体操作。
-
Subject 提供了检查用户角色的能力:
Subject currentUser = SecurityUtils.getSubject(); if (currentUser.hasRole("administrator")) { // 用户拥有 administrator 角色 } else { // 用户没有 administrator 角色 } 对用户角色进行检查的能力的类似方法: hasRoles(List<String> roleNames) hasAllRoles(Collection<String> roleNames) checkRole(String roleName)、checkRoles(Collection<String> roleNames) 和 checkRoles(String... roleNames) 三个方法也能检查用户角色,不同的是,这三个方法在内部如果检查出用户不具有相应角色的话会直接抛出异常(AuthorizationException)。
-
-
基于权限的授权 : (更优) 可通过
subject的isPermitted()
方法来实现-
isPermitted()
方法接受的参数有两类:字符串 或Permission
接口。-
通过实现 Permission 接口来承载权限信息
Permission 接口提供了对权限表述进行检查的能力,能够确保系统中使用的权限符合你对权限定义的一些限制。比如你希望一个权限的声明是严格对应到系统数据模型中的具体模型(或者说资源)的,对模型的操作只应该是模型支持的几种(比如 CRUD)。
-
通过字符串承载权限信息 ( 通配符权限 )
"printer:query"
:字符串分成两个部分,第一部分表示操作者(printer),第二部分表示允许执行的操作(query)。"printer:print,query"
:一个操作者有执行多个操作权限的方式,第一部分表示操作者(printer),第二部分表示允许执行的操作(query 和 print)。"printer:*"
:*
表示所有,即操作者(printer)拥有所有操作权限。"printer:print:epsoncolor"
:具体到实例的方式,表示操作者(printer)能够执行(print)操作,另外对操作的对象(epsoncolor)也进行了定义。
-
-
-
使用原生API实现权限检查 : 在原生API实现登录功能的基础上
-
修改shiro.ini
[users] root = secret_pwd, admin guest = guest_pwd, writer, reader [roles] admin = * writer = book:write reader = lib:view:java-in-action
-
在demo.java中增加语句
//1. 基于角色的权限检查,检查用户是否拥有 writer 角色。 if (currentUser.hasRole("writer")) { System.out.println("拥有「writer」角色"); } else { System.out.println("没有「writer」角色"); } //2. 基于权限(对具体资源的操作权限)的权限检查,检查用户是否有“写书”权限。 if (currentUser.isPermitted("book:write")) { System.out.println("拥有「写书」权限"); } else { System.out.println("没有「写书」权限"); } //3. 对具体资源实例的操作权限检查。 if (currentUser.isPermitted("lib:view:java-in-action")) { System.out.println("有权阅读 java-in-action"); } else { System.out.println("无权阅读 java-in-action"); }
-
在实际开发过程中往往不会直接通过 Subject 的
isPermitted*(arg)
和hasRole(arg)
等方法进行权限检查,更多的是使用注解(@RequiresRoles,@RequiresPermissions
等)进行权限检查,结合 Spring 和 AOP,使用注解的方式能够显著提高我们的开发效率
-
04 | Shiro会话管理 (Session Manager)
-
Session Listener : 监听会话中发生的生命周期事件,可以通过实现
SessionListener
接口,或继承SessionListenerAdapter
来接收这些通知。主要有如下三种通知:void onStart(Session session)
,会话开始。void onStop(Session session)
,会话结束,在主动调用Session.stop()
或Subject.logout()
时发生。void onExpiration(Session session)
,会话过期。
-
Session Storage : 对会话进行持久化(创建或更新时),当会话过期或不再使用时这些持久化数据需要被删除(释放占用空间),
SessionManager
实现这些CRUD
操作的具体组件是SessionDAO
。同样的,有必要的话我们也可以实现自己的SessionDAO
,来接管会话数据的持久化行为。 -
Session的相关API :
-
如下代码所示:
// 获取当前用户 Subject currentUser = SecurityUtils.getSubject(); Session session = currentUser.getSession(); session.setAttribute("username","guest"); if(session != null){ System.out.println("当前登陆用户为:" + session.getAttribute("username")); }else{ System.out.println("会话未创建"); } ===> 登录成功 当前登陆用户为:guest
-
完成。