SpringBoot集成认证授权之Shiro
in JavaDevelop with 0 comment

SpringBoot集成认证授权之Shiro

in JavaDevelop with 0 comment

介绍

Apache Shiro 是一个强大且易用的 Java 安全框架,提供了认证、授权、加密和会话管理等功能。Shiro 适用于任何有安全控制需要的 Java 应用,并且相对于其他安全框架,Shiro 无论从 API 使用,或是源码实现上都要简单清晰得多。

什么是 Shiro

Apache Shiro 是一个使用 Java 开发的,易于使用,功能强大且灵活的开源安全框架,它为开发人员提供了直观简洁的 API 来支持应用程序中的四个安全性基石:

  1. 身份验证(Authentication)。
  2. 授权(Authorization)。
  3. 会话管理(Session Management)。
  4. 加密(Cryptography)。

image-1658371277880

Shiro 提供了许多重要且实用的安全性相关的功能,其中被关注得最多的是身份验证和授权功能,这两者也是一个安全框架的核心功能,我们的课程将会把重心放在这两个部分,对于其它部分只进行简单的介绍。

想象有如下的动态安全模型:

管理员可以为应用系统创建不同的角色,角色可以绑定一组权限,之后管理员可以为不同用户分配一个或多个角色,并在应用正常提供服务期间在后台管理网页中随时更改所有这些功能。

通过使用 Shiro,我们将能够很容易的实现上述功能。

Shiro 关键概念和术语

Shiro 定义了一些术语和概念,它们之后会出现在课程的很多地方,这些术语在 Shiro 的 API 和官方文档中也随处可见,对这些术语和概念进行预先了解,对不同概念进行区分,这样能够提高我们之后的学习效率。

认证(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 作业),基本上它是任何实体的代表,这些实体会与应用程序一起做一些事情。

Shiro 框架的体系结构

Shiro 的设计目标是通过直观且易于使用的 API 来简化应用程序安全性相关代码的设计,应用程序安全控制功能的实现往往有着不同的需求场景,Shiro 的核心设计参考了这些用户在大多数情况下都会遇到的需求场景,在这些情况下来考虑 Shiro 的安全性设计。

在 Shiro 框架的最上层,主要有 3 个关键组件相互协作:

image-1658371300990

Subject

正如上面提到的那样,Subject 本质上是系统用户的一个特定安全 “视图”,可以表示会与应用程序交互的任何东西。

Subject 实例都强制绑定到 SecurityManager 上,当我们调用 Subject 的方法时,具体的操作最终都会转交给相关的 SecurityManager 进行处理。

SecurityManager

SecurityManager 是 Shiro 体系结构的核心,它协调着内部的其它安全组件,这些安全组件一起形成一种互相协作支持的关系,共同完成 Shiro 提供的所有功能。对于开发人员而言,一旦为应用程序配置了 SecurityManager,通常就不需要再理会它,我们只需要和 Subject 的 API 进行交互就可以了,这在上面的流程图中也有清晰的体现。

Realm

Realm 是 Shiro 与应用程序数据之间交换的“桥梁”,当 Shiro 真正需要与安全性相关的数据(例如用户帐户,用户角色,权限等)进行交互以执行身份验证和授权时,Shiro 会从一个或多个为应用程序配置的 Realm 中查找这些数据。

可以简单地把 Realm 理解为特定于安全性的 DAO,它封装了对安全性相关数据的访问,在配置 Shiro 时,我们必须至少指定一个 Realm 以用于身份验证(或授权)。

详细架构

下图展示了 Shiro 的核心体系结构:

image-1658371319026

上图是一个更加具体的 Shiro 详细架构图,图中清晰地描述了 SubjectSecurity ManagerRealm 三者各自在整个 Shiro 框架中所扮演的角色,以及担负的职责。

为了简化配置和实现灵活的可配置性(可插拔性),Shiro 的实现都是高度模块化的,高度模块化使得 SecurityManager 的实现实际上并没有做实质性的事情。相反,SecurityManager 的作用是作为一个轻量级的“容器”组件,将几乎所有的行为委托给封装在内部的其它组件,这是“包装器”设计模式的一种使用。

下面对身份认证器(Authenticator)和访问控制器(Authorizer)进行简单说明。

身份认证器(Authenticator)是负责执行和响应用户认证(登录)的组件。当用户尝试登录时,该逻辑由 Authenticator 执行,Authenticator 知道如何与一个或多个 Realm 进行协调,从这些 Realm 中获取用户的身份信息,然后完成身份认证过程。

访问控制器(Authorizer)是负责决定用户对应用中具体资源能否访问和操作的组件,是最终决定用户是否被允许做某事的机制,与 Authenticator 一样,Authorizer 也知道如何与多个 Realm 协调,以获取角色和权限信息,Authorizer 使用这些信息来确定用户是否被允许执行某个动作。

身份认证介绍

身份认证指的是认证系统用户身份的过程,其目的是为了控制系统只为可信用户提供服务,避免受到非法用户的损害,用户只有提供了系统信任的“凭据”,才能顺利通过身份认证,访问系统中的资源。

Shiro Authentication 的设计目的在于让身份认证过程直接,清晰和易扩展。接下来就让我们一起详细了解 Shiro Authentication 的功能特性以及关键概念,然后使用原生 API 实现简单的登录功能。

Shiro Authentication 主要功能特性

一切都是基于当前用户

几乎所有在 Shiro 中做的事情都是基于当前执行的用户,即 Subject,我们在代码的任何地方都能很容易的获取到当前用户(SecurityUtils.getSubject()),对于 Web App 来说,因为 Subject 就保存在线程上下文(ThreadContext)中。

仅一个方法调用就可以完成用户认证

用户认证仅仅是一个单一的方法调用(currentUser.login(token)),在常规的系统中也是这么设计的,Shiro 保证扩展能力的同时也提供了干净的代码调用方式。

丰富的异常结构提供了快速定位问题的能力

Shiro 提供了丰富的异常定义,如下的几个异常类型可能会在登录失败时抛出,根据具体异常类型,我们能够快速定位问题。

用户身份信息来源可定制化

要对用户进行认证,那么就需要获取到用户的真实身份信息。在 Shiro 中,这部分数据的提供者为 Realm,Realm 充当着 Shiro Authentication 与系统数据源交互的重任,应用需要通过实现 Realm 接口的方式将从系统数据源(JDBC,File IO 或 LDAP 等)中获取到的用户身份信息转交给 Shiro,Shiro Authentication 才能进行身份认证。

Shiro Authentication 关键概念和术语

Principals

Principals 指的是可以识别一个用户的任何东西,比如用户名,电话号码等,它的值在系统中往往具有唯一性,能够唯一标识一个用户。

Credentials

Credentials 可以认为是一个用户证明自己是"本人"的凭证,通常应该是只有用户自己才知道的秘密值,最常见的是用户登录密码。此外,还有一些生物识别数据,如指纹、面容等。

AuthenticationToken

AuthenticationToken 是用户在进行身份验证时用户身份标识和凭证的载体,SecurityManager 的 login(token) 方法内部会直接将 AuthenticationToken 交给 Authenticator,由 Authenticator 最终执行身份验证/登录过程。在一般开发过程中我们使用最多的是 UsernamePasswordToken,它是 AuthenticationToken 的一个具体实现。

原生 API 实现登录功能

对于一个传统的用户登录实现,一般需要三个步骤:

  1. 获取用户提供的身份标识(principals),比如用户名,手机号码等,另一个是获取用户提供的身份凭证(credentials)。
  2. 将身份标识和身份凭证提交到系统。
  3. 成功提交到系统后,系统会根据身份标识获取到系统中保存的用户身份信息,将两者进行比对,如果用户提交的凭证匹配,那么就认为登录者是用户“本人”。

接下来我们开始使用 Shiro 原生 API 实现登录功能。示例将会是一个简单的命令行应用(command-line application),使用 maven 作为项目构建工具,应用开始运行后通过命令行输出日志信息,然后快速退出。

首先进行 maven 项目的创建,打开 WebIDE 的终端并执行下面的命令:

mvn archetype:generate -DgroupId=com.sample -DartifactId=sample -DarchetypeArtifactId=maven-archetype-quickstart

image-1658371520814

注意:期间遇到需要输入的地方直接回车即可。maven 下载依赖需要耗费一定时间,请耐心等待。

命令执行完成之后,展开 sample 目录,你可以看到如下图所示的目录结构:

image-1658371540904

接下来将需要用到的依赖添加到 pom.xml 文件中,shiro-core 中依赖了日志的相关功能,因此需要将 commons-logging 也添加到 pom 中,默认的 junit 依赖可以删除。

<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>

代码开发完成之后我们希望能够快速运行项目,然后通过观察控制台输出的日志验证结果,这里需要借助 exec-maven-plugin 插件,这样我们就可以通过 mvn compile exec:java 命令快速运行项目。

<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>

此时完整的 pom.xml 文件如下图所示:

image-1658371562825

Shiro 支持从系统初始化文件(.ini)中解析用户身份和权限信息,我们将使用 .ini 文件模拟系统中保存真实用户信息的数据源,这里首先需要在 main 文件夹下新建一个 resources 文件夹,然后在 resources 文件夹下创建 shiro.ini 文件,文件内容如下:

[users]
root = secret_pwd
guest = guest_pwd

上述内容表示系统中存在两个用户,其用户名分别为 root 和 guest,root 用户的密码为 secret_pwd,guest 用户的密码为 guest_pwd。将上述内容保存到 sample/src/main/resources/shiro.ini 文件中,方便在示例代码中读取。

image-1658371583032

准备工作都做好了,接下来就可以在 App.java 文件中进行代码编写。

  1. 首先我们需要做的是将 shiro.ini 文件配置到 SecurityManager 中,同时 SecurityManager 会负责对一些相关组件进行初始化。SecurityManager 实例的创建使用了工厂模式,通过调用 factory.getInstance() 方法就可以获取到具体实现的实例。
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
  1. 将用户身份信息保存到 UsernamePasswordToken,调用登录 API。
// 获取当前用户
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);
    }
}

正如你所看到的,Shiro 的 API 基于常见的工作流程进行设计。当登录方法被调用时,SecurityManager 将接收 AuthenticationToken(这里为 UsernamePasswordToken ),然后交给 Authenticator,之后 Authenticator 再将其调度到对应的 Realm 上,让 Realm 根据需要执行身份验证检查。如果登录尝试失败,那么可以通过运行时异常 AuthenticationException 来捕获到具体的出错原因。

  1. 最后,别忘了将依赖包通过 import 关键字进行导入。
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;

至此,我们的简单登录实现代码已经写完了,此时你的 App.java 文件看起来应该是这样的:

image-1658371608710

接下来通过执行 mvn compile exec:java (注意需在 /home/project/sample 目录下执行)命令来运行项目,控制台会输出许多 maven 相关的日志,大部分可以忽略,仔细查看,你可以在输出中看到 “登录成功” 的字样,这就说明 guest 用户提供了正确的凭据(“guest_pwd”),已经成功登录了。

注意:maven 下载依赖需要耗费一定时间,请耐心等待。

image-1658371626625

你可以尝试将 new UsernamePasswordToken(arg1,arg2) 的第一个或第二个参数修改成一个无意义的字符串,然后运行项目,看看会发生什么。