SpringBoot实战(二十五)集成 Shiro

目录

    • 一、Shiro 简介
      • 1.1 Shiro 定义
      • 1.2 Shiro 核心组件
      • 1.3 Shiro 认证过程
    • 二、SpringBoot集成
      • 2.1 集成思路
      • 2.2 Maven依赖
      • 2.3 自定义 Realm
      • 2.4 Shiro 配置类
      • 2.5 静态资源映射
      • 2.6 AuthController
      • 2.7 User 实体
      • 2.8 用户接口类
      • 2.9 用户接口实现类
      • 2.10 OrderController(鉴权测试)
    • 三、测试
      • 测试1:跳转登录页面
      • 测试2:注册用户
      • 测试3:登录主页
      • 测试4:权限校验
    • 四、注意

  • 官网地址: https://shiro.apache.org/
  • 中文文档: https://www.docs4dev.com/docs/zh/apache-shiro/1.5.3/reference/introduction.html
  • Spring集成Shiro官方文档: https://shiro.apache.org/spring-framework.html
  • GitHub: https://github.com/apache/shiro

一、Shiro 简介

1.1 Shiro 定义

Apache Shiro:是一款 Java 安全框架,不依赖任何容器,可以运行在 Java 项目中,它的主要作用是做身份认证、授权、会话管理和加密等操作。

其实不用 Shiro,我们使用原生 Java API 就可以实现安全管理,使用过滤器去拦截用户的各种请求,然后判断是否登录、是否拥有权限即可。但是对于一个大型的系统,分散去管理编写这些过滤器的逻辑会比较麻烦,不成体系,所以需要使用结构化、工程化、系统化的解决方案。

与 Spring Security 相比,shiro 属于轻量级框架,相对于 Spring Security 简单的多,也没有那么复杂。

1.2 Shiro 核心组件

Shiro 的运行机制如下图所示:

在这里插入图片描述

1)UsernamePasswordToken:封装用户登录信息,根据用户的登录信息创建令牌 token,用于验证令牌是否具有合法身份以及相关权限。

2)SecurityManager:核心部分,负责安全认证与授权。

3)Subject:一个抽象概念,包含了用户信息。

4)Realm:开发者自定义的模块,根据项目的需求,验证和授权的逻辑在 Realm 中实现。

5)AuthenticationInfo:用户的角色信息集合,认证时使用。

6)AuthorizationInfo:角色的权限信息集合,授权时使用。

7)DefaultWebSecurityManager:安全管理器,开发者自定义的 Realm 需要注入到 DefaultWebSecurityManager 中进行管理才能生效。

8)ShiroFilterFactoryBean:过滤器工厂,Shiro 的基本运行机制是开发者定制规则,Shiro 去执行,具体的执行操作就是由 ShiroFilterFactoryBean 创建一个个 Filter 对象来完成。

1.3 Shiro 认证过程

在这里插入图片描述


二、SpringBoot集成

2.1 集成思路

SpringBoot 集成 Shiro 思路图如下:

在这里插入图片描述

项目包结构如下:

在这里插入图片描述

2.2 Maven依赖

<!--Shiro-->
<dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-spring-boot-starter</artifactId><version>1.12.0</version>
</dependency>

2.3 自定义 Realm

自定义 Realm 主要实现了两大模块:

  • 认证:根据用户名,查询密码,然后封装返回 SimpleAuthenticationInfo 认证信息。
  • 授权:根据用户名,查询角色和权限,然后封装返回 SimpleAuthorizationInfo 授权信息。

CustomRealm.java

import com.demo.module.entity.User;
import com.demo.module.service.UserService;
import com.demo.util.SpringUtils;
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.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;/*** <p> @Title CustomRealm* <p> @Description 自定义Realm** @author ACGkaka* @date 2023/10/15 12:42*/
public class CustomRealm extends AuthorizingRealm {/*** 认证*/@Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {// 获取用户名String principal = (String) authenticationToken.getPrincipal();// 根据用户名查询数据库UserService userService = SpringUtils.getBean(UserService.class);User user = userService.findByUsername(principal);if (user != null) {return new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(),ByteSource.Util.bytes(user.getSalt()), this.getName());}return null;}/*** 授权*/@Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {// 获取用户名String principal = (String) principalCollection.getPrimaryPrincipal();if ("admin".equals(principal)) {// 管理员拥有所有权限SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();info.addRole("admin");info.addStringPermission("admin:*");info.addRole("user");info.addStringPermission("user:find:*");return info;}return null;}
}

2.4 Shiro 配置类

配置类中指定了如下内容:

  • 需要进行鉴权的资源路径;
  • 指定自定义 Realm;
  • 加密规则。

ShiroConfig.java

import com.demo.config.shiro.realm.CustomRealm;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import java.util.HashMap;
import java.util.Map;/*** <p> @Title ShiroConfig* <p> @Description Shiro配置类** @author ACGkaka* @date 2023/10/15 12:44*/
@Configuration
public class ShiroConfig {/*** ShiroFilter过滤所有请求*/@Beanpublic ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();// 给ShiroFilter配置安全管理器shiroFilterFactoryBean.setSecurityManager(securityManager);// 配置系统公共资源、系统受限资源(公共资源必须在受限资源上面,不然会造成死循环)Map<String, String> map = new HashMap<>();// 系统公共资源map.put("/login", "anon");map.put("/register", "anon");map.put("/static/**", "anon");// 受限资源map.put("/**", "authc");// 设置认证界面路径shiroFilterFactoryBean.setLoginUrl("/login");shiroFilterFactoryBean.setFilterChainDefinitionMap(map);return shiroFilterFactoryBean;}/*** 创建安全管理器*/@Beanpublic DefaultWebSecurityManager securityManager(Realm realm) {DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();securityManager.setRealm(realm);return securityManager;}/*** 创建自定义Realm*/@Beanpublic Realm realm() {CustomRealm realm = new CustomRealm();// 设置使用哈希凭证匹配HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();// 设置使用MD5加密算法credentialsMatcher.setHashAlgorithmName("MD5");// 设置散列次数:加密次数credentialsMatcher.setHashIterations(1024);realm.setCredentialsMatcher(credentialsMatcher);return realm;}
}

2.5 静态资源映射

静态资源映射主要将 cssjs 等静态文件夹映射到浏览器端,方便页面加载。

在这里插入图片描述

WebConfiguration.java

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.util.ResourceUtils;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;/*** SpringBoot静态路径配置** @author ACGkaka* @date 2019/11/27 15:38*/
@Configuration
@Primary
public class WebConfiguration implements WebMvcConfigurer {/*** 访问外部文件配置*/@Overridepublic void addResourceHandlers(ResourceHandlerRegistry registry) {registry.addResourceHandler("/static/css/**").addResourceLocations(ResourceUtils.CLASSPATH_URL_PREFIX + "/static/css/");registry.addResourceHandler("/static/js/**").addResourceLocations(ResourceUtils.CLASSPATH_URL_PREFIX + "/static/js/");WebMvcConfigurer.super.addResourceHandlers(registry);}
}

2.6 AuthController

主要用于进行 thymeleaf 模板引擎的页面跳转,以及登录、注册、退出登录等功能的实现。

AuthController.java

import com.demo.module.entity.User;
import com.demo.module.service.UserService;
import lombok.AllArgsConstructor;
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.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;import javax.annotation.Resource;/*** <p> @Title IndexController* <p> @Description 鉴权Controller** @author ACGkaka* @date 2019/10/23 20:23*/
@Controller
@AllArgsConstructor
public class AuthController {@Resourceprivate UserService userService;/*** 默认跳转主页*/@GetMapping("/")public String showIndex() {return "redirect:/index";}/*** 主页*/@GetMapping("/index")public String index() {return "index.html";}/*** 主页*/@GetMapping("/login")public String login() {return "login.html";}/*** 注册页*/@GetMapping("/register")public String register() {return "/register.html";}/*** 登录*/@PostMapping("/login")public String login(String username, String password) {// 获取主题对象Subject subject = SecurityUtils.getSubject();try {subject.login(new UsernamePasswordToken(username, password));System.out.println("登录成功!!!");return "redirect:/index";} catch (UnknownAccountException e) {System.out.println("用户错误!!!");} catch (IncorrectCredentialsException e) {System.out.println("密码错误!!!");}return "redirect:/login";}/*** 注册*/@PostMapping("/register")public String register(User user) {userService.register(user);return "redirect:/login.html";}/*** 退出登录*/@GetMapping("/logout")public String logout() {Subject subject = SecurityUtils.getSubject();subject.logout();return "redirect:/login.html";}
}

2.7 User 实体

封装了用户的基本属性。

User.java

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;import java.io.Serializable;
import java.time.LocalDateTime;/*** <p>* 用户表* </p>** @author ACGkaka* @since 2021-04-25*/
@Data
@TableName("t_user")
public class User implements Serializable {private static final long serialVersionUID = 1L;/*** 主键*/@TableId(value = "id", type = IdType.AUTO)private Long id;/*** 用户名*/private String username;/*** 密码*/private String password;/*** 随机盐*/private String salt;/*** 创建时间*/private LocalDateTime createTime;/*** 更新时间*/private LocalDateTime updateTime;
}

2.8 用户接口类

封装了注册(用户新增)和根据用户名查找接口。

UserService.java

import com.baomidou.mybatisplus.extension.service.IService;
import com.demo.module.entity.User;/*** 用户表 服务类*/
public interface UserService extends IService<User> {/*** 注册* @param user 用户*/void register(User user);/*** 根据用户名查询用户* @param principal 用户名* @return 用户*/User findByUsername(String principal);
}

2.9 用户接口实现类

实现了注册(用户新增)和根据用户名查找功能。

UserServiceImpl.java

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.demo.module.entity.User;
import com.demo.module.mapper.UserMapper;
import com.demo.module.service.UserService;
import com.demo.util.SaltUtils;
import org.apache.shiro.crypto.hash.Md5Hash;
import org.springframework.stereotype.Service;/*** <p>* 用户表 服务实现类* </p>** @author ACGkaka* @since 2021-04-25*/
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {@Overridepublic void register(User user) {// 注册用户checkRegisterUser(user);// 1.生成随机盐String salt = SaltUtils.getSalt(8);// 2.将随机盐保存到数据库user.setSalt(salt);// 3.明文密码进行MD5 + salt + hash散列次数Md5Hash md5Hash = new Md5Hash(user.getPassword(), salt, 1024);user.setPassword(md5Hash.toHex());// 4.保存用户this.save(user);}@Overridepublic User findByUsername(String principal) {// 根据用户名查询用户LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(User::getUsername, principal);return this.getOne(queryWrapper);}// ------------------------------------------------------------------------------------------// 内部方法// ------------------------------------------------------------------------------------------/*** 校验注册用户* @param user 用户*/private void checkRegisterUser(User user) {if (user == null) {throw new RuntimeException("用户信息不能为空");}if (user.getUsername() == null || "".equals(user.getUsername())) {throw new RuntimeException("用户名不能为空");}if (user.getPassword() == null || "".equals(user.getPassword())) {throw new RuntimeException("密码不能为空");}// 判断用户名是否已存在User existUser = this.getOne(new UpdateWrapper<User>().eq("username", user.getUsername()));if (existUser != null) {throw new RuntimeException("用户名已存在");}}
}

2.10 OrderController(鉴权测试)

在自定义 Realm 配置好用户权限后,用于测试对用户权限和角色权限的控制。

OrderController.java

import com.demo.common.Result;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.apache.shiro.subject.Subject;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;/*** <p> @Title OrderController* <p> @Description 订单控制器** @author ACGkaka* @date 2023/10/15 17:15*/
@RestController
@RequestMapping("/order")
public class OrderController {/*** 新增订单(代码实现权限判断)*/@GetMapping("/add")public Result<Object> add() {Subject subject = SecurityUtils.getSubject();if (subject.hasRole("admin")) {return Result.succeed().setData("新增订单成功");} else {return Result.failed().setData("操作失败,无权访问");}}/*** 编辑订单(注解实现权限判断)*/@RequiresRoles({"admin", "user"}) // 用来判断角色,同时拥有admin和user角色才能访问@RequiresPermissions("user:edit:01") // 用来判断权限,拥有user:edit:01权限才能访问@GetMapping("/edit")public String edit() {System.out.println("编辑订单");return "redirect:/index";}
}

三、测试

测试1:跳转登录页面

访问地址:http://localhost:8081,我们配置了根路径默认访问主页,由于用户没有登录,会默认跳转登录页面。

在这里插入图片描述

测试2:注册用户

访问地址:http://localhost:8081/register,输入用户名和密码后,系统会在数据库中新增用户信息,自动跳转登录页面,输入刚才的用户名和密码即可登录。

在这里插入图片描述

测试3:登录主页

输入正确的用户名密码,即可登录至主页。

在这里插入图片描述

测试4:权限校验

我们在主页中加入了 OrderController 中的接口,用于测试权限校验。

在这里插入图片描述

当使用非 admin 用户进行登录的时候,代码实现权限判断的 /add 接口,报错如下:

在这里插入图片描述

Shiro 注解实现权限判断的 /edit 接口,报错如下:

在这里插入图片描述

从提示信息可以看到,返回了状态码 500,报错信息为:当前用户的主体没有 admin 权限。

由此可见,两种鉴权结果均成功生效,具体使用哪一种由业务场景来定。

四、注意

由于 Shiro 核心是通过 Session 来实现用户登录的,所以有很多场景不支持,比如:

1)Session 默认存储在内存中,重启应用后所有用户登录状态会失效

2)默认配置不支持分布式,需要分布式部署的时候,可以通过 Redis数据库方式来同步 Session 会话。

整理完毕,完结撒花~ 🌻





参考地址:

1.SpringBoot之整合Shiro(最详细),https://blog.csdn.net/Yearingforthefuture/article/details/117384035

2.超详细 Spring Boot 整合 Shiro 教程!https://cloud.tencent.com/developer/article/1643122

3.springboot整合shiro(完整版),https://blog.csdn.net/w399038956/article/details/120434244

4.Shiro安全框架【快速入门】就这一篇!https://zhuanlan.zhihu.com/p/54176956

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/109573.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

SpringBoo整合WebSocket实战演练——Java入职十三天

前言 本文将介绍如何在Spring Boot应用程序中使用WebSocket实现服务端向客户端推送消息。Spring Boot和WebSocket的整合实现服务端向客户端推送消息,使得客户端能够实时接收并处理服务器发来的信息。WebSocket协议是一种双向通信的网络协议,使得客户端和服务器能够建立持久连…

List小练习,实现添加图书,并且有序遍历

SuppressWarnings({"all"})public static void main(String[] args) {List list new LinkedList(); // List list new Vector(); // List list new ArrayList();list.add(new Book1("红楼小梦",35.5,"曹雪芹"));list.add(new B…

Mysql数据库 1. SQL基础语法和操作

一、Mysql逻辑结构 一个数据库软件可以包含许多数据库 一个数据库包含许多表 一个表中包含许多字段&#xff08;列&#xff09; 数据库软件——>数据库——>数据表——>字段&#xff08;列&#xff09;、元组&#xff08;行&#xff09; 二、SQL语言基础语法 1.SQL…

ROS2的cv_bridge库opencv版本不匹配问题

1. 问题 /usr/bin/ld: warning: libopencv_imgcodecs.so.4.2, needed by /opt/ros/foxy/lib/libcv_bridge.so, may conflict with libopencv_imgcodecs.so.4.5/usr/bin/ld: warning: libopencv_core.so.4.2, needed by /opt/ros/foxy/lib/libcv_bridge.so, may conflict with …

git远程仓库、开发者使用流程、ssh连接

git远程仓库 https://www.cnblogs.com/liuqingzheng/p/15328319.html 远程仓库有&#xff1a; : github gitlab gitee 在gitee上创建了仓库 &#xff08;确保仓库是空的&#xff09;本地&#xff1a;git init本地&#xff1a;git commit -m 提交版本指定远程仓库地址 添加一…

滚雪球学Java(53):从入门到精通:SimpleDateFormat类高深用法,让你的代码更简洁!

咦咦咦&#xff0c;各位小可爱&#xff0c;我是你们的好伙伴——bug菌&#xff0c;今天又来给大家普及Java SE相关知识点了&#xff0c;别躲起来啊&#xff0c;听我讲干货还不快点赞&#xff0c;赞多了我就有动力讲得更嗨啦&#xff01;所以呀&#xff0c;养成先点赞后阅读的好…

STM32如何使用中断?

一&#xff1a;EXTI 简介 STM32F10x 外部中断/事件控制器&#xff08;EXTI&#xff09;包含多达 20 个用于产生事件/中断请求的边沿检测器。 EXTI 的每根输入线都可单独进行配置&#xff0c;以选择类型&#xff08;中断或事件&#xff09;和相应的触发事件&#xff08;上升 沿触…

易点易动设备管理系统:提升生产企业设备保养效率的利器

在现代生产企业中&#xff0c;设备保养是确保生产线稳定运行和产品质量的关键环节。然而&#xff0c;传统的设备保养方式往往面临效率低下、数据不准确等问题&#xff0c;影响了生产效率和竞争力。随着科技的进步&#xff0c;易点易动设备管理系统应运而生&#xff0c;以其智能…

【学一点儿前端】ajax、axios和fetch的概念、区别和易混淆点

省流读法 ajax是js异步技术的术语&#xff0c;早期相关的api是xhr&#xff0c;它是一个术语。 fetch是es6新增的用于网络请求标准api&#xff0c;它是一个api。 axios是用于网络请求的第三方库&#xff0c;它是一个库。 1.Ajax 它的全称是&#xff1a;Asynchronous JavaScri…

核酸管外观缺陷检测(一)

1.1 应用示例思路 (1) 对核酸管图像进行灰度化、阈值分割和连通域分析&#xff1b; (2) 筛选出待检测的区域&#xff0c;并对该区域进行变换校正&#xff1b; (3) 进一步获取待检测的ROI区域&#xff0c;并根据几何特征和阈值条件&#xff0c;来对核酸管外观进行检测&#x…

Windows10 Docker 安装教程

Docker Desktop是什么&#xff1f; Docker Desktop是适用于Windows的Docker桌面&#xff0c;是Docker设计用于在Windows 10上运行。它是一个本地 Windows 应用程序&#xff0c;为构建、交付和运行dockerized应用程序提供易于使用的开发环境。Docker Desktop for Windows 使用 …

Cron表达式每月20号晚18点执行

Cron表达式每月20号晚18点执行 0 0 18 20 * ?验证正确性

node.js的版本管理nvm

一、nvm是什么&#xff1f; nvm是一个node的版本管理工具&#xff0c;可以简单操作node版本的切换、安装、查看等等&#xff0c;与npm不同的是&#xff0c;npm是依赖包的管理工具。 二、nvm的安装 1.下载 2.安装 安装后的目录&#xff1a;C:\Users\admin\AppData\Roaming\nv…

Double 4 VR智能互动教学系统的教学应用

1. 激发学习兴趣 Double 4 VR智能互动教学系统通过虚拟现实技术为学生创造了一个身临其境的学习环境。学生可以通过戴上VR头盔&#xff0c;进入虚拟世界中与教学内容互动。这种沉浸式的学习方式能够激发学生的学习兴趣&#xff0c;使他们更加主动地参与到课堂中来。 2. 提供直…

DSP移相控制

//############################################################################################### // Description: // program for 4路PWM信号&#xff0c;每路以A为准&#xff0c;B与之互补&#xff0c;带死区。移相以第一路信号&#xff08;EPWM1A&#xff09;为基准…

Java10年技术架构演进

一、前言 又快到了1024&#xff0c;现代人都喜欢以日期的特殊含义来纪念属于自己的节日。虽然有点牵强&#xff0c;但是做件事情&#xff0c;中国人总喜欢找个节日来纪念&#xff0c;程序员也是一样。甚至连1111被定义成光棍节&#xff0c;这也算再无聊不过了。不过作为程序员…

报考阿里云acp认证,你得到的是什么?

放眼全球能够和亚马逊AWS、微软Azure竞争的&#xff0c;国内也就只有阿里云了。 阿里云目前稳居国内云计算市场第一&#xff0c;比排后面5名同行市场占有率的总和还要多&#xff0c;全球云计算市场&#xff0c;阿里云目前排名第3位。 阿里云的市场占有率说明市场对于阿里云产…

kafka、zookeeper、flink测试环境、docker

1、kafka环境单点 根据官网版本说明(3.6.0)发布&#xff0c;zookeeper依旧在使用状态&#xff0c;预期在4.0.0大版本的时候彻底抛弃zookeeper使用KRaft(Apache Kafka)官方并给出了zk迁移KR的文档 2、使用docker启动单点kafka 1、首先将kafka启动命令&#xff0c;存储为.servi…

小程序:uniapp解决主包体积过大的问题

已经分包但还是体积过大 运行时勾选“运行时是否压缩代码”进行压缩 在manifest.json配置&#xff08;开启分包优化&#xff09; "mp-weixin" : {"optimization" : {"subPackages" : true}//.... },在app.json配置&#xff08;设置组件按需注入…

ubuntu 22.04修改永久修改 mac 地址

使用 rc-local.service rc-local.service 是系统自带的一个开机自启服务&#xff0c;但是在 ubuntu 20.04 上&#xff0c;该服务默认没有开启。 在文件的最后面添加 [Install] 段的内容&#xff0c; cat >> /lib/systemd/system/rc-local.service << EOF # SPD…