(八) SpringBoot起飞之路-整合Shiro详细教程(MyBatis、Thymeleaf)

2,306 阅读20分钟

兴趣的朋友可以去了解一下前几篇,你的赞就是对我最大的支持,感谢大家!

  • 文章目录

(一) SpringBoot起飞之路-HelloWorld

(二) SpringBoot起飞之路-入门原理分析

(三) SpringBoot起飞之路-YAML配置小结(入门必知必会)

(四) SpringBoot起飞之路-静态资源处理

(五) SpringBoot起飞之路-Thymeleaf模板引擎

(六) SpringBoot起飞之路-整合JdbcTemplate-Druid-MyBatis

(七) SpringBoot起飞之路-整合SpringSecurity(Mybatis、JDBC、内存)

(八) SpringBoot起飞之路-整合Shiro详细教程(MyBatis、Thymeleaf)

(九) SpringBoot起飞之路-Swagger 2 And 3

说明:

  • 这一篇的目的还是整合,也就是一个具体的实操体验,原理性的没涉及到,我本身也没有深入研究过,就不献丑了

  • SpringBoot 起飞之路 系列文章的源码,均同步上传到 github 了,有需要的小伙伴,随意去 down

  • 才疏学浅,就会点浅薄的知识,大家权当一篇工具文来看啦,不喜勿愤哈 ~

(一) 初识 Shiro

(1) 引言

权限以及安全问题,虽然并不是一个影响到程序、项目运行的必须条件,但是却是开发中的一项重要考虑因素,例如某些资源我们不想被访问到或者我们某些方法想要满足指定身份才可以访问,我们可以使用 AOP 或者过滤器来实现要求,但是实际上,如果代码涉及的逻辑比较多以后,代码是极其繁琐,冗余的,而有很多开发框架,例如 Spring Security,Shiro,已经为我们提供了这种功能,我们只需要知道如何正确配置以及使用它了

(2) 基本介绍

官网:http://shiro.apache.org/

Apache Shiro™ is a powerful and easy-to-use Java security framework that performs authentication, authorization, cryptography, and session management. With Shiro’s easy-to-understand API, you can quickly and easily secure any application – from the smallest mobile applications to the largest web and enterprise applications.

Apache Shiro™是一个功能强大且易于使用的Java安全框架,可执行身份验证、授权、加密和会话管理。通过Shiro易于理解的API,您可以快速、轻松地保护任何应用程序——从最小的移动应用程序到最大的web和企业应用程序。

简单梳理一下:

  • Shiro 和 Spring Security 性质是一样的,都是一款权限框架,用来保证应用的权限安全问题
  • Shiro 可执行身份验证、授权、加密和会话管理,Web集成,缓存等
  • Shiro 不仅可以应用到 JavaEE 环境下,甚至 JavaSE 也可以

(3) 基本功能

这部分的内容,说实话,刚入门简单扫两眼就行了,只有你真的敲过一次代码了,你才大概对其中某些部分能有个印象,再继续深入研究才可能有比较好的掌握

A:官方架构图

  • Authentication:用户认证就是指这个用户身份是否合法,一般我们的用户认证就是通过校验用户名密码,来判断用户身份的合法性,确定身份合法后,用户就可以访问该系统

  • Authorization:如果不同的用户需要有不同等级的权限,就涉及到用户授权,用户授权就是对用户能访问的资源,所能执行的操作进行控制,根据不同用户角色或者对应不同权限来划分不同的权限

  • SessionManager:Shior 官网说其提供了一个完整的会话管理解决方案, 它的所会话可以是普通的Java SE环境, 也可以是Web环境,不过我有点思维定式了,还是用习惯的方式,这块没怎么研究

  • Cryptography:加密明文密码, 保护数据安全

  • WebSupport:字面意思,其对Web的支持, 使得其可以非常容易的集成到Web环境;

  • Caching:缓存, 比如用户登录后, 其用户信息, 拥有的角色、权限不必每次去查,效率上会好一点

  • Concurrency:Shiro 支持多线程应用的并发验证,即,如在一个线程中开启另一个线程,能把权限自动传过去

  • Testing:没什么好说的,就是支持测试

  • Run As:允许一个用户假装为另一个用户(允许的条件下) 的身份进行访问资源请求

  • Remember Me:它也有,记住我这个功能

B:三大核心组件

Shiro框架中有三个核心组件:Subject ,SecurityManager和Realms

  1. Subject 是一个安全术语,代表认证主体,一般来说可以简单的理解为,当前操作的用户,不过用户这个概念实际上也不是很准确,因为 Subject 实际上不一定是人,也可以是一些例如第三方进程或者定时作业等等的事物,也就是理解为,当前同软件交互的事物。
    • 每一个Subject对象都必须被 SecurityManager 进行管理
  2. Subject 接受 SecurityManager 的管理,因为 SecurityManager 管理所有用户的安全操作,其内部引用了很多安全相关的组件,但是都不对外开放,开发人员更多的是使用 Subject
  3. Realms 这个概念也是重要的,其可以理解为 Shiro 与 数据之间的沟通器与中间桥梁认证授权时,就会去此部分找一些内容,从本质上 Realm 就是一个经过了大量封装的安全 Dao

(4) 用户|角色|权限的概念

既然 Shiro 是一个安全权限技术,简单来说,就是对程序中被访问的资源或者请求进行一定程度的控制,而如何划分就涉及到这三个概念:用户、角色、权限

用户(User):没啥好说的,代表当前 Subject 认证主体,例如某些内容必须用户登录后才可以访问

角色(Role):这代表用户担任的角色,身份,一个角色可以有多个权限,例如这一块只有管理员可以访问

权限(Permission):也就是操作资源的具体的权利,例如对数据进行添加、修改、删除、查看操作

补充:其实可以简单的理解,角色就是一些权限的集合组成的,正是这一堆权限已经将这个角色能做的事情限定死了,不用每次都说明这个角色可以做什么

(二) 静态页面导入 And 页面环境搭建

(1) 关于静态页面

A:页面介绍

页面是我自己临时弄得,有需要的朋友可以去我 GitHub:ideal-20 下载源码,简单说明一下这个页面

做一个静态页面如果嫌麻烦,也可以单纯的自己创建一些简单的页面,写几个标题文字,能体现出当前是哪个页面就好了

我代码中用的这些页面,就是拿开源的前端组件框架进行了一点的美化,然后方便讲解一些功能,页面模板主要是配合 Thymeleaf

1、目录结构

├── index.html                        // 首页
├── images                            // 首页图片,仅美观,无实际作用
├── css                               
├── js                                
├── views                             // 总子页面文件夹,权限验证的关键页面
│   ├── login.html					  // 登录页面
│   ├── success.html				  // 成功页面
│   ├── unauthorized.html			  // 未授权页面:此部分未授权的用户访问资源,跳转到此页面
│   ├── L-A							  // L-A 子页面文件夹,下含 a b c 三个子页面
│   │   ├── a.html
│   │   ├── b.html
│   │   ├── c.html
|	├── L-B							  // L-B 子页面文件夹,下含 a b c 三个子页面
│   │   ├── a.html
│   │   ├── b.html
│   │   ├── c.html
|	├── L-C							  // L-C 子页面文件夹,下含 a b c 三个子页面
│   │   ├── a.html
│   │   ├── b.html
│   │   ├── c.html

B:导入到项目

主要就是把基本一些链接,引入什么的先替换成 Thymeleaf 的标签格式,这里语法用的不是特别多,即使对于 Thymeleaf 不是很熟悉也是很容易看懂的,当然如果仍然感觉有点吃力,可以单纯的做成 html,将就一下,或者去看一下我以前的文章哈,里面有关于 Thymeleaf 入门的讲解

css、image、js 放到 resources --> static 下 ,views 和 index.html 放到 resources --> templates下

## (2) 环境搭建

A:引入依赖

这一部分引入也好,初始化项目的时候,勾选好自动生成也好,只要依赖正常导入了即可

  • 引入 Spring Security 模块
<dependency>
	<groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.5.3</version>
</dependency>

关键的依赖主要就是上面这个启动器,但是还有一些就是常规或者补充的了,例如 web、thymeleaf、devtools 等等,还有一些例如 Mybatis 等我都放进来了,下面的依赖基本已经全了,具体讲到某块,具体再说

thymeleaf-extras-shiro 这个后面讲解中会提到,是用来配合 Thymeleaf 整合 Shiro 的

<dependencies>
	<dependency>
        <groupId>com.github.theborakompanioni</groupId>
        <artifactId>thymeleaf-extras-shiro</artifactId>
        <version>2.0.0</version>
    </dependency>
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-spring</artifactId>
        <version>1.5.3</version>
    </dependency>

    <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.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.1.3</version>
    </dependency>

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>

    <dependency>
       <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
         <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>
</dependencies>

B:页面跳转 Controller

因为我们用了模板,页面的跳转就需要交给 Controller 了,很简单,首先是首页的,当然关于页面这个就无所谓了,我随便跳转到了我的博客,接着还有登录页面、成功,未授权页面的跳转

有一个小 Tip 需要提一下,因为 L-A、L-B、L-C 文件夹下都有3个页面 a.html 、b.html 、c.html,所以可以利用 @PathVariable 写一个较为通用的跳转方法

@Controller
public class PageController {

    @RequestMapping({"/", "index"})
    public String index() {
        return "index";
    }

    @RequestMapping("/about")
    public String toAboutPage() {
        return "redirect:http://www.ideal-20.cn";
    }

    @RequestMapping("/toLoginPage")
    public String toLoginPage() {
        return "views/login";
    }

    @RequestMapping("/levelA/{name}")
    public String toLevelAPage(@PathVariable("name") String name) {
        return "views/L-A/" + name;
    }

    @RequestMapping("/levelB/{name}")
    public String toLevelBPage(@PathVariable("name") String name) {
        return "views/L-B/" + name;
    }

    @RequestMapping("/levelC/{name}")
    public String toLevelCPage(@PathVariable("name") String name) {
        return "views/L-C/" + name;
    }
    
    @RequestMapping("/unauthorized")
    public String toUnauthorizedPage() {
        return "views/unauthorized";
    }

    @RequestMapping("/success")
    public String toSuccessPage() {
        return "views/success";
    }
}

C:环境搭建最终效果

  • 为了贴图方便,我把页面拉窄了一点
  • 首页右上角应该为登录的链接,这里是因为,我运行的是已经写好的代码,不登录页面例如 L-A-a 等模块就显示不出来,所以拿一个定义好的管理员身份登陆了
  • 关于如何使其自动切换显示登陆还是登录后信息,在后面会讲解

1、首页

2、子页面

L-A、L-B、L-C 下的 a.html 、b.html 、c.html 都是一样的,只是文字有一点变化

3、登陆页面

4、成功及未授权页面

我截了个图,把两个页面拼接到一起了,没啥好说的,就是两个很普通的H5页面

(三) 创建数据库及实体

(1) 创建数据库以及表

-- ----------------------------
-- Table structure for role
-- ----------------------------
CREATE TABLE `role`  (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '角色表主键',
  `role_name` varchar(32) DEFAULT NULL COMMENT '角色名称',
  PRIMARY KEY (`id`)
);

-- ----------------------------
-- Records of role
-- ----------------------------
INSERT INTO `role` VALUES (1, 'SUPER_ADMIN');
INSERT INTO `role` VALUES (2, 'ADMIN');
INSERT INTO `role` VALUES (3, 'USER');

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用户主键',
  `username` varchar(32) NOT NULL COMMENT '用户名',
  `password` varchar(32) NOT NULL COMMENT '密码',
  `role_id` int(11) DEFAULT NULL COMMENT '与role角色表联系的外键',
  PRIMARY KEY (`id`),
  CONSTRAINT `user_role_on_role_id` FOREIGN KEY (`role_id`) REFERENCES `role` (`id`)
);

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, 'BWH_Steven', '666666', 1);
INSERT INTO `user` VALUES (2, 'admin', '666666', 2);
INSERT INTO `user` VALUES (3, 'zhangsan', '666666', 3);

-- ----------------------------
-- Table structure for permission
-- ----------------------------
CREATE TABLE `permission`  (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '权限表主键',
  `permission_name` varchar(50) NOT NULL COMMENT '权限名',
  `role_id` int(11) DEFAULT NULL COMMENT '与role角色表联系的外键',
  PRIMARY KEY (`id`),
  CONSTRAINT `permission_role_on_role_id` FOREIGN KEY (`role_id`) REFERENCES `role` (`id`)
);

-- ----------------------------
-- Records of permission
-- ----------------------------
INSERT INTO `permission` VALUES (1, 'user:*', 1);
INSERT INTO `permission` VALUES (2, 'user:*', 2);
INSERT INTO `permission` VALUES (3, 'user:queryAll', 3);

(2) 实体

在数据库中角色表,在用户表和权限表分别是有一个外键的概念,所以在实体中就写成了引用的形式

角色类

@Data
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public class Role {
    private int id;
    private String roleName;
}

用户类,说明:由于我在其他模块下有一些同名的类,调用的时候经常会有一些误会,所以就稍微改了下名字 --> UserPojo,这里大家起 User 就 OK

@Data
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public class UserPojo {
    private int id;
    private String username;
    private String password;
    private Role role;
}

权限类

@Data
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public class Permission {
    private Integer id;
    private String permissionName;
    private Role role;
}

(四) 整合 MyBatis

今天要做的内容,实际上自己随便模拟两个数据也是可以的,不过为了贴近现实,还是引入了 Mybaits

(1) 引入依赖及进行配置

先引入 MyBatis 依赖,还有驱动依赖

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.3</version>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

连接池啥的就不折腾了,想自己换就自己配置一下哈

spring:
  datasource:
    username: root
    password: root99
    url: jdbc:mysql://localhost:3306/springboot_shiro_test?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8
    driver-class-name: com.mysql.cj.jdbc.Driver

mybatis:
  mapper-locations: classpath:mapper/*Mapper.xml
  type-aliases-package: cn.ideal.pojo

server:
  port: 8080

具体的 Mapper 这里还没写,讲解的过程中,按照流需要,再写上去

(2) 编写 Mapper

因为代码是在文章之前写好的,我们在后面会用到利用 username 进行查询用户和权限的方法,所以,我们就按这样写就好了

@Mapper
public interface UserMapper {
    UserPojo queryUserByUsername(@Param("username") String username);

    Permission queryPermissionByUsername(@Param("username") String username);
}

具体的 XML 配置 sql

这部分涉及到多表的一个稍复杂的查询,如果感觉有点吃力,可以去回顾一下前面的知识,或者干脆不管也可以,接着看后面的,纯了解 Shiro 也可以

<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="cn.ideal.mapper.UserMapper">

    <!-- 定义封装 User和 role 的 resultMap -->
    <resultMap id="userRoleMap" type="cn.ideal.pojo.UserPojo">
        <id property="id" column="id"/>
        <result property="username" column="username"></result>
        <result property="password" column="password"></result>
        <!-- 配置封装 UserPojo 的内容 -->
        <association property="role" javaType="cn.ideal.pojo.Role">
            <id property="id" column="id"></id>
            <result property="roleName" column="role_name"></result>
        </association>
    </resultMap>

    <!-- 定义封装 permission 和 role 的 resultMap -->
    <resultMap id="permissionRoleMap" type="cn.ideal.pojo.Permission">
        <id property="id" column="id"/>
        <result property="permissionName" column="permission_name"></result>
        <!-- 配置封装 Role 的内容 -->
        <association property="role" javaType="cn.ideal.pojo.Role">
            <id property="id" column="id"></id>
            <result property="roleName" column="role_name"></result>
        </association>
    </resultMap>

    <select id="queryUserByUsername" resultMap="userRoleMap">
        SELECT u.*,r.role_name FROM `user` u, `role` r
          WHERE username = #{username} AND u.role_id = r.id;
    </select>

    <select id="queryPermissionByUsername" resultMap="permissionRoleMap">
        SELECT p.* ,r.role_name FROM `user` u, `role` r, `permission` p
          WHERE username = #{username} AND u.role_id = r.id AND p.role_id = r.id;
    </select>

</mapper>

(3) 代码测试

@SpringBootTest
class Springboot13ShiroMybatisApplicationTests {

    @Autowired
    private UserMapper userMapper;
    
    @Test
    void contextLoads() {
        UserPojo admin = userMapper.queryUserByUsername("admin");
        System.out.println(admin.toString());
        Permission permission = userMapper.queryPermissionByUsername("admin");
        System.out.println(permission.toString());
    }
}

(五) Spring Boot 整合 Shiro

(1) 自定义认证和授权(Realm)

首先我们需要创建Shiro的配置类,在config包下创建一个名为 ShiroConfig 的配置类

@Configuration
public class ShiroConfig {
	// 1、ShiroFilterFactoryBean
	// 2、DefaultWebSecurityManager
	// 3、Realm 对象(自定义)
}

上面注释可以看出,我们需要在配置类中创建这样几个内容,由于他们几个之间存在关联,例如在 Manager 中关联自己创建的 Realm,在最上面的过滤器,又关联了中间这个 Manager,所以我们选择倒着写,先写后面的(也就是被引用最早的 Realm),这样就可以一层一层的在前面引用后面已经写好的,会更舒服一些

首先,在 ShiroConfig 配置类中编写一个方法用来获取 Realm ,直接返回一个实例化的 userRealm() 就可以了

/**
 * 创建 realm 对象,需要自己定义
 *
 * @return
 */
@Bean
public UserRealm userRealm() {
    return new UserRealm();
}

具体内容,我们需要创建一个新的类来定义

我们自定义了一个 UserRealm类,同时继承 AuthorizingRealm 类,接着就需要实现两个方法:

  • doGetAuthenticationInfo() 认证方法:查看用户是否能通过认证,可简单理解为登录是否成功

  • doGetAuthorizationInfo() 授权方法:给当前已经登录成功的用户划分权限以及分配角色

根据上面的介绍也很好理解,肯定是认证先行,接着才会执行授权方法,所以我们先来编写认证的代码

A:认证

认证首先就要先获取到我们前台传来的数据,这块很显然,交给 Controller 来做,我们先来完成这个内容,再回来编写认证

说明:获取前台的数据就是下面的 login 方法,同时在其中调用了认证的方法,其他几个方法,只是为了后期演示的时候使用,一块给出来了,同时下面登录方法中我捕获了所有异常,大家可以自己更细致的划分,同时由于为了演示重点,我前台没有做太多的处理,例如session中传入一些登录失败等的字符串,完全不写也是可以的哈

@Controller
public class UserController {
    @RequestMapping("/user/queryAll")
    @ResponseBody
    public String queryAll() {
        return "这是 user/queryAll 方法";
    }

    @RequestMapping("/user/admin/add")
    @ResponseBody
    public String adminAdd() {
        return "这是 user/adminAdd 方法";
    }

    @RequestMapping("/login")
    public String login(String username, String password, HttpServletRequest request) {
        // 由于是根据name参数获取的,我这里封装了一下
        UserPojo user = new UserPojo();
        user.setUsername(username);
        user.setPassword(password);
        // 创建出一个 Token 内容本质基于前台的用户名和密码(不一定正确)
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        // 获取 subject 认证主体(这里也就是现在登录的用户)
        Subject subject = SecurityUtils.getSubject();
        try{
            // 认证开始,这里会跳转到自定义的 UserRealm 中
            subject.login(token);
            // 可以存储到 session 中
            request.getSession().setAttribute("user", user);
            return "views/success";
        }catch(Exception e){
            // 捕获异常
            e.printStackTrace();
            request.getSession().setAttribute("user", user);
            request.setAttribute("errorMsg", "兄弟,用户名或密码错误");
            return "views/login";
        }
    }
}

UserRealm 下的认证方法:

说明:通过方法参数中的 token 就可以获取到我们刚才的那个 token信息,最方便的方法就是下面,直接通过 getPrincipal() 获取到用户名(Object 转 String),还有一种方法就是,将 Token 强转了 UsernamePasswordToken 类型,接着需要用户名或者密码等信息都可以通过 getxxx 的方法获取到

可以看到,我们只需要将数据库中查询到的数据交给 Shiro 去做认证就可以了,具体细节都被封装了

补充:userService.queryUserByUsername(username) 方法只是调用返回了 UserMapper 中根据用户名查询用户信息的方法,只是为了结构完整,没涉及任何业务,如果不清楚,可以去 GitHub 看一下源码

/**
 * 认证
 *
 * @param authenticationToken
 * @return
 * @throws AuthenticationException
 */
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
	// 根据在接受前台数据创建的 Token 获取用户名
    String username = (String) authenticationToken.getPrincipal();
    //  UsernamePasswordToken userToken = (UsernamePasswordToken) authenticationToken;
    //  System.out.println(userToken.getPrincipal());
    //  System.out.println(userToken.getUsername());
    //  System.out.println(userToken.getPassword());
    
    // 通过用户名查询相关的用户信息(实体)
    UserPojo user = userService.queryUserByUsername(username);
    if (user != null) {
        // 存入 Session,可选
        SecurityUtils.getSubject().getSession().setAttribute("user", user);
        // 密码认证的工作,Shiro 来做
        AuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), "userRealm");
        return authenticationInfo;
     } else {
        // 返回 null 即会抛异常
        return null;
     }
}

B:授权

授权,也就是在用户认证后,来设置用户的权限或者角色信息,这里主要是获取到用户名以后,通过 service 中调用 mapper 接着根据用户名查询用户或者权限,由于返回的是用户或者权限实体对象,所以配合 getxxx等方法就可以获取到需要的值了

当然了,最主要的还是根据自己 mapper 以及表的返回情况设置,这里只要能获取到角色以及权限信息(这里是 String 类型)就可以了,如果是多个角色,就要使用 setRoles() 方法了,具体需要可以看参数和返回值,或者查阅文档,这里演示都是单个的

/**
 * 授权
 *
 * @param principalCollection
 * @return
 */
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    // 获取用户名信息
    String username = (String) principalCollection.getPrimaryPrincipal();
    // 创建一个简单授权验证信息
    SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
    // 给这个用户设置从 role 表获取到的角色信息
    authorizationInfo.addRole(userService.queryUserByUsername(username).getRole().getRoleName());
    //给这个用户设置从 permission 表获取的权限信息
    authorizationInfo.addStringPermission(userService.queryPermissionByUsername(username).getPermissionName());
    return authorizationInfo;
}

(2) Shiro 配置

授权和配置就写好了,也就是说 Realm 完事了,一个大头内容完成了,我们接着就可以回到 Shiro 的配置中去了,继续倒着写,开始写关于第二点 Manager 的内容

@Configuration
public class ShiroConfig {
	// 1、ShiroFilterFactoryBean
	// 2、DefaultWebSecurityManager
    
	// 3、Realm 对象(自定义)
	@Bean
	public UserRealm userRealm() {
    	return new UserRealm();
	}
}

A:配置安全管理器

接着就来配置安全管理器(SecurityManager),这里就需要将刚才写好的 Realm 引入进来,这样 Shiro 就可以访问 Realm 了,然后接着返回

/**
 * 配置安全管理器 SecurityManager
 *
 * @return
 */
 @Bean
 public DefaultWebSecurityManager securityManager() {
    // 将自定义 Realm 加进来
    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    // 关联 Realm
    securityManager.setRealm(userRealm());
    return securityManager;
}

如果,setRealm 的时候直接调用下面的 userRealm() 出现了问题,那么可以考虑在方法参数中配合 @Qualifier 使用,它会自动去找下面 public UserRealm userRealm() 方法的方法名 userRealm,userRealm 中的注解不指定name也行,这里只是为了让大家看得更明白

@Bean
public DefaultWebSecurityManager securityManager(@Qualifier("userRealm") UserRealm userRealm) {
    // 将自定义 Realm 加进来
    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    // 关联 Realm
    securityManager.setRealm(userRealm);
    return securityManager;
}

@Bean(name="userRealm")
public UserRealm userRealm() {
    return new UserRealm();
}

B:配置过滤器

这又是一个关键的地方,首先创建一个 ShiroFilterFactoryBean 肯定是毋庸置疑的,最后毕竟要返回这个对象,首先就是将刚才的 securityManager 关联进来了,也就是说层层调用,最终把 Realm 关联过来了,接着要写的就是重头戏了,我们接着需要设置一些自己定义的内容

  • 自定义登录页面
  • 成功页面
  • 未授权界面
  • 一个自定义的 Map 用来存储需要放行或者拦截的请求
  • 注销页面

重点说一下拦截放行(Map)这块:通过 map 键值对的形式存储,key 存储 URL ,value 存储对应的一些权限或者角色等等,其实 key 这块还是很好理解的,例如 :/css/** /user/admin/** 分别代表 css 文件夹下的所有文件,以及请求路径前缀为 /user/admin/ URL,而对应的 value 就有一定的规范了

关键:

  • anon:无需认证,即可访问,也就是游客也可以访问
  • authc:必须认证,才能访问,也就是例如需要登录后
  • roles[xxx] :比如拥有某种角色身份才能访问 ,注:xxx为角色参数
  • perms[xxx]:必须拥有对某个请求、资源的相关权限才能访问,注:xxx为权限参数

补充:

  • user:必须使用【记住我】这个功能才能访问
  • logout:注销,执行后跳转到设置好的登录页面去
/**
 * 配置 Shiro 过滤器
 *
 * @param securityManager
 * @return
 */
@Bean
public ShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager securityManager) {
    // 定义 shiroFactoryBean
    ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

    // 关联 securityManager
    shiroFilterFactoryBean.setSecurityManager(securityManager);

    // 自定义登录页面,如果登录的时候,就会执行这个请求,即跳转到登录页
    shiroFilterFactoryBean.setLoginUrl("/toLoginPage");
    // 指定成功页面
     shiroFilterFactoryBean.setSuccessUrl("/success");
    // 指定未授权界面
    shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");

    // LinkedHashMap 是有序的,进行顺序拦截器配置
    Map<String, String> filterChainMap = new LinkedHashMap<>();

    // 配置可以匿名访问的地址,可以根据实际情况自己添加,放行一些静态资源等,anon 表示放行
    filterChainMap.put("/css/**", "anon");
    filterChainMap.put("/img/**", "anon");
    filterChainMap.put("/js/**", "anon");
    // 指定页面放行,例如登录页面允许所有人登录
    filterChainMap.put("/toLoginPage", "anon");

    // 以“/user/admin” 开头的用户需要身份认证,authc 表示要进行身份认证
    filterChainMap.put("/user/admin/**", "authc");

    filterChainMap.put("/levelA/**", "roles[USER]");
    filterChainMap.put("/levelB/**", "roles[ADMIN]");
    filterChainMap.put("/levelC/**", "roles[SUPER_ADMIN]");

    // /user/admin/ 下的所有请求都要经过权限认证,只有权限为 user:[*] 的可以访问,也可以具体设置到 user:xxx
    filterChainMap.put("/user/admin/**", "perms[user:*]");

    // 配置注销过滤器
    filterChainMap.put("/logout", "logout");

    // 将Map 存入过滤器
    shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainMap);
    return shiroFilterFactoryBean;
}

C:解决多身份问题

其实上面的内容已经基本健全了,但是还有一个很棘手的问题,那就是,例如我主页中的三个模块,超级管理员A、B、C都可以访问,管理员能访问 A 和 B,而登录后的普通用户只能访问 A,如何写呢?是不是像下面这样呢?

filterChainMap.put("/levelA/**", "roles[USER,ADMIN,SUPER_ADMIN]");
filterChainMap.put("/levelB/**", "roles[ADMIN,SUPER_ADMIN]");
filterChainMap.put("/levelC/**", "roles[SUPER_ADMIN]");

但是你一用,肯定会发现问题,我们来看一下关于 Role相关的过滤器代码,很显然关于 Role 的验证竟然是通过 hasAllRoles 实现的,也就是说,我们要满足所有的身份才能访问,不能达到,任选其一即可的效果

/**
 * Filter that allows access if the current user has the roles specified by the mapped value, or denies access
 * if the user does not have all of the roles specified.
 *
 * @since 0.9
 */
public class RolesAuthorizationFilter extends AuthorizationFilter {

    //TODO - complete JavaDoc

    @SuppressWarnings({"unchecked"})
    public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws IOException {

        Subject subject = getSubject(request, response);
        String[] rolesArray = (String[]) mappedValue;

        if (rolesArray == null || rolesArray.length == 0) {
            //no roles specified, so nothing to check - allow access.
            return true;
        }

        Set<String> roles = CollectionUtils.asSet(rolesArray);
        return subject.hasAllRoles(roles);
    }

}

自定义一个 Fileter,重新定义关于 Role 的验证方式,改成 hasRole 的方式

public class MyRolesAuthorizationFilter extends AuthorizationFilter {

    @SuppressWarnings({"unchecked"})
    public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws IOException {

        Subject subject = getSubject(request, response);
        String[] rolesArray = (String[]) mappedValue;

        if (rolesArray == null || rolesArray.length == 0) {
            return false;
        }

        List<String> roles = CollectionUtils.asList(rolesArray);
        boolean[] hasRoles = subject.hasRoles(roles);
        for (boolean hasRole : hasRoles) {
            if (hasRole) {
                return true;
            }
        }
        return false;
    }
}

有了这个重新修改了规则的角色过滤器,我们就可以继续回到配置中去,通过下面三行代码就可以讲这个新的规则的过滤器设置进去

// 设置自定义 filter
Map<String, Filter> filterMap = new LinkedHashMap<>();
filterMap.put("anyRoleFilter", new MyRolesAuthorizationFilter());
shiroFilterFactoryBean.setFilters(filterMap);

自然,原来相应的Map定义就要变化了,配合自定义过滤器,改成多个角色的的形式

// 页面 -用户需要角色认证
filterChainMap.put("/levelA/**", "anyRoleFilter[USER,ADMIN,SUPER_ADMIN]");
filterChainMap.put("/levelB/**", "anyRoleFilter[ADMIN,SUPER_ADMIN]");
filterChainMap.put("/levelC/**", "anyRoleFilter[SUPER_ADMIN]");

(六) Shiro 整合 Thymeleaf

主要内容已经结束了,不过因为在前面 Spring Security 中,讲过如何搭配 Thymeleaf 使用,所以接着补充一点关于如何用 Shiro 配合 Thymeleaf 的方法

A:引入

首先引入两者整合的依赖:

<dependency>
    <groupId>com.github.theborakompanioni</groupId>
    <artifactId>thymeleaf-extras-shiro</artifactId>
    <version>2.0.0</version>
</dependency>

这个版本已经是最新的了(还是很旧)2016年,具体可以去 maven repository 官网中查一下

注:这个依赖需要 thymeleaf 是 3.0 的版本,我们的 Springboot 是用的最新的启动器,自然是 3.0 不过还是提一下

接着在 Shiro 的主配置 ShiroConfig 类中加入这样的代码,这样,我们就可以在 thymeleaf 中使用 Shiro 的自定义标签

/**
 * 整合 thymeleaf
 * @return
 */
@Bean(name = "shiroDialect")
public ShiroDialect shiroDialect(){
    return new ShiroDialect();
}

B:修改页面

操作结束后,我们就可以开始修改页面了,首先引入头部约束 xmlns:shiro="http://www.pollix.at/thymeleaf/shiro“

<html lang="zh_CN" xmlns:th="http://www.thymeleaf.org"
      xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">

这里解决的问题,主要是登录前后,顶部导航栏的一个显示问题,例如登录前就应该显示登陆,登录后,就显示用户名和注销,如果需要更多的信息,我就建议存到 session ,这里我是直接使用 shiro:principal 标签获取的用户名

<div>
   <!-- 这里代表别的代码,下面只是节选 -->
    
   <!--登录注销-->
    <div class="right menu">
      <!--如果未登录-->
      <!--<div shiro:authorize="!isAuthenticated()">-->
      <div shiro:notAuthenticated="">
        <a class="item" th:href="@{/toLoginPage}">
          <i class="address card icon"></i> 登录
        </a>
      </div>

      <!--如果已登录-->
      <div shiro:authenticated="">
        <a class="item">
          <i class="address card icon"></i>
          用户名:<span shiro:principal></span>
          <!--角色:<span sec:authentication="principal.authorities"></span>-->
        </a>
      </div>

      <div shiro:authenticated="">
        <a class="item" th:href="@{/logout}">
          <i class="address card icon"></i> 注销
        </a>
      </div>
    </div> 
</div>	

下面就是用来只显示对应模块的,例如用户登录就只有 A可以访问,所以 B 和 C模块 就不给他显示了,反正这个模块他也不能访问

<div class="ui stackable three column grid">
    <div class="column" shiro:hasAnyRoles="USER,ADMIN,SUPER_ADMIN">
      <div class="ui raised segments">
        <div class="ui segment">
          <a th:href="@{/levelA/a}">L-A-a</a>
        </div>
        <div class="ui segment">
          <a th:href="@{/levelA/b}">L-A-b</a>
        </div>
        <div class="ui segment">
          <a th:href="@{/levelA/c}">L-A-c</a>
        </div>
      </div>
    </div>
    <div class="column" shiro:hasAnyRoles="ADMIN,SUPER_ADMIN">
      <div class="ui raised segments">
        <div class="ui segment">
          <a th:href="@{/levelB/a}">L-B-a</a>
        </div>
        <div class="ui segment">
          <a th:href="@{/levelB/b}">L-B-b</a>
        </div>
        <div class="ui segment">
          <a th:href="@{/levelB/c}">L-B-c</a>
        </div>
      </div>
    </div>
    <div class="column" shiro:hasRole="SUPER_ADMIN">
      <div class="ui raised segments">
        <div class="ui segment">
          <a th:href="@{/levelC/a}">L-C-a</a>
        </div>
        <div class="ui segment">
          <a th:href="@{/levelC/b}">L-C-b</a>
        </div>
        <div class="ui segment">
          <a th:href="@{/levelC/c}">L-C-c</a>
        </div>
      </div>
    </div>
  </div>

C:看一下效果

普通管理员登录后,显示账号和注销,同时只有超级管理员才能访问的 C模块 就不给予显示

(七) 结尾

如果文章中有什么不足,欢迎大家留言交流,感谢朋友们的支持!

如果能帮到你的话,那就来关注我吧!如果您更喜欢微信文章的阅读方式,可以关注我的公众号

在这里的我们素不相识,却都在为了自己的梦而努力 ❤

一个坚持推送原创开发技术文章的公众号:理想二旬不止