freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

Java 安全 | 从 Shiro 底层源码看 Shiro 漏洞 (上)
2024-10-24 12:59:43
所属地 海外

前言

Shiro 的漏洞已爆出很多年, 我们只关心到了它如何触发, 有时并没有想过这个框架是干嘛的, 甚至没有分析过该框架的底层运行逻辑, 那么本篇文章, 让大家从开发者的角度, 来观察 Shiro 漏洞.

从开发者为什么使用 Shiro, 到 Shiro 底层运行逻辑, 再到 Shiro 漏洞原理刨析.

并不会像某些文章一样, 只是把漏洞点点明一下, 用 Payload 打一下, 就说这个漏洞已经“研究明白”了.

本篇文章分为三个部分:

第一部分来介绍 Shiro 的基本使用, 清晰的感受到 Shiro 框架给程序员带来的便捷.

第二部分来介绍 Shiro 底层源码的分析, 感受该框架设计的精妙.

第三部分来介绍 Shiro 漏洞分析, 在这里我们会梳理底层源码分析的流程, 一步一步涉及到漏洞点的产生, 顺便记载一下 Shiro 的攻击姿势.

本篇文章目录如下:

image-20241019215104183.png

由于文章过长, 笔者将分为两篇进行发布.

权限管理

概念

image-20241006163451124.png

为了实现不同身份登录系统, 出现的功能模块不一样. 这个需求叫做权限管理.

  • 学生登录后, 出现的功能模块为: 选课, 成绩查询, 课程表

  • 老师登陆后, 出现的功能模块为: 学生管理, 成绩录入

那么有了这个基础的概念之后, 我们再看一个比较复杂的权限管理案例:

image-20241006165849875.png

具体实现

第一种方式

image-20241006171521654.png

这种方式适用于权限管理比较单一, 用户少, 每类用户权限固定的场景. 根据不同的页面来实现功能不一致的情况.

这种方式的缺点则是, 假设需要在销售人员主页增加新的功能时, 我们需要修改index1.html页面内容, 增加上新的功能, 需要后期慢慢维护.

这种基于页面的开发, 是不建议的.

第二种方式

image-20241006192212959.png

这一种设计是RBAC (基于角色的访问控制)的基本原型, 也不是最终版本, 看起来已经实现动态的显示功能效果了. 但是这里会存在一个问题.

假设我们新增了一个Heihu577用户, 那么我们就需要在用户权限表中增加很多权限, 那么假设Heihu577用户所需要的权限与liucheng用户权限一致, 那么用户权限表中又需要给Heihu577用户分配很多权限id, 直到与用户liucheng的权限id是一致的, 这样大大减少了灵活性.


那么这里我们需要引入角色的概念,角色是什么?我们不妨看下图进行理解.

image-20241006194328473.png

现在的权限分配, 是根据角色的, 我们只需要指明某个用户是某个角色, 即可得到该角色的具体权限. 而这么做的弊端则是, 假设A & B用户是同一角色, 而我们希望某一功能只给A用户而不给B用户, 这个时候怎么办呢?

image-20241006202455328.png

我们只需要增加一个用户权限表, 将额外的权限分配给具体用户即可, 当然随着业务逻辑的复杂, 我们的表也跟着复杂化了. 那么除了表结构的设计, 还需要我们程序设计的思想:

image-20241007135554675.png

而这里过滤器/拦截器部分我们也可以自己写流程, 判断当前是否是登录状态, 是否有SESSION等. 当我们考虑不全面时程序也可能出现BUG.

而这种情况我们也可以选择使用安全框架, 帮助我们在应用系统开发过程完成认证以及授权的工作. 而安全框架类似于一个保安的角色:

image-20241007140346688.png

这是一个演唱会的案例, 根据你的门票类别, 到达具体的座位, 当然这一切都需要你去告诉这个保安如何匹配规则, 换到程序里, 我们也仅仅做一个配置即可. 而这里我们就可以选择使用 Shiro, Shiro 就做了这些事情, 类似于 Shiro 的框架还有很多, Spring Security, OAuth2等.

Shiro

Shiro 的核心功能如下:

image-20241007141857444.png

Authentication 认证: 验证用户是否有相应的身份 - 登录认证.

Authorization 授权: 对已经通过认证的用户, 检查是否具有某个权限, 或者角色, 从而控制是否能够进行某种操作.

Session Management 会话管理功能: 用户认证成功后创建会话, 在没有退出之前, 当前用户所有信息都保存至当前会话中 (具备 SESSION 功能), 可以在 Java SE 中使用.

Cryptography 密码管理: 对敏感信息进行加密.

支持的特性:

  • Web Support: Shiro 提供了过滤器, 可以通过过滤器拦截 Web 请求来处理 Web 应用的访问控制.

  • Caching 缓存支持: Shiro 可以缓存用户信息以及用户的角色权限信息, 可以提高执行效率.

  • Concurrency: Shiro 支持多线程应用.

  • Testing: 提供测试功能.

  • Run As: 允许一个用户以另一种身份去访问.

  • Remember Me: 记住密码功能.

Shiro 是一个安全框架, 不提供用户权限的维护. 用户的权限管理需要我们自己去设计.

Shiro 核心组件

Shiro 的运行流程为如下:

image-20241007150213823.png

这里 Subject 的创建是由 SecurityUtils 进行创建的, 后面我们代码会给出案例, 官方给出的图如下:

image-20241007150235043.png

基于 Java SE 基本使用

pom.xml文件中进行引入依赖:

<dependencies>
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-core</artifactId>
        <version>1.4.1</version>
    </dependency>
</dependencies>

因为本次的Realm从文件中获取数据, 所以这里创建/resources/shiro.ini文件, 内容如下:

[users] # 定义两个用户信息 用户名=密码,角色1,角色2...
heihuUser=heihuPass,seller # 账号名:heihuUser 密码:heihuPass 销售人员
hacker=123456,ckmgr # 账号名:hacker 密码:123456 仓管人员
admin=admin888,admin # 账号名: admin888 密码: admin888

[roles] # 定义角色与对应的权限 角色名=权限1,权限2,权限3...
seller=order-add,order-del,order-list # 销售人员的权限
ckmgr=ck-add,ck-del,ck-list # 仓管人员权限
admin=* # * 表示所有权限

这里的注释在实际运行代码时, 要将其删掉, 否则将报错!

随后我们创建测试程序:

Scanner scanner = new Scanner(System.in); // 接收外部传递来的账号密码
System.out.print("请输入用户名: ");
String username = scanner.nextLine();
System.out.print("请输入密码: ");
String password = scanner.nextLine();

DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager(); // 准备 SecurityManager
defaultSecurityManager.setRealm(new IniRealm("classpath:shiro.ini")); // 设置 Realm
SecurityUtils.setSecurityManager(defaultSecurityManager); // 将 SecurityManager 设置到 SecurityUtils 工具类中
Subject subject = SecurityUtils.getSubject(); // 通过 SecurityUtils 获取 subject 对象

System.out.println(subject.isAuthenticated()); // 判断 subject 是否通过认证, 这里是 false
subject.login(new UsernamePasswordToken(username, password)); // 通过 subject对象.login 进行认证, 通过账号密码进行认证, 认证失败则抛出异常
System.out.println(subject.isAuthenticated()); // 判断 subject 是否通过认证, 如果登录成功, 这里为 true, 如果登陆失败, 上一行代码已经抛出异常了.

由于登录失败会抛出异常, 所以我们这里可以使用try-catch进行捕获, 加入到我们的业务逻辑中:

Scanner scanner = new Scanner(System.in); // 接收外部传递来的账号密码
System.out.print("请输入用户名: ");
String username = scanner.nextLine();
System.out.print("请输入密码: ");
String password = scanner.nextLine();

DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager(); // 准备 SecurityManager
defaultSecurityManager.setRealm(new IniRealm("classpath:shiro.ini")); // 设置 Realm
SecurityUtils.setSecurityManager(defaultSecurityManager); // 将 SecurityManager 设置到 SecurityUtils 工具类中
Subject subject = SecurityUtils.getSubject(); // 通过 SecurityUtils 获取 subject 对象

try {
    subject.login(new UsernamePasswordToken(username, password)); // 通过 subject对象.login 进行认证, 通过账号密码进行认证, 认证失败则抛出异常
    System.out.println("登陆成功!");
} catch (IncorrectCredentialsException e) {
    System.out.println("登陆失败!");
} catch (UnknownAccountException e) {
    System.out.println("用户名不存在!");
}
// 如果看注释吃力, 可以根据 《Shiro 核心组件》中的流程进行理解

当然了, 登录成功后, 我们可以判断当前的角色是什么角色, 也可以判断当前的用户是否具备某个权限:

Scanner scanner = new Scanner(System.in); // 接收外部传递来的账号密码
System.out.print("请输入用户名: ");
String username = scanner.nextLine();
System.out.print("请输入密码: ");
String password = scanner.nextLine();

DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager(); // 准备 SecurityManager
defaultSecurityManager.setRealm(new IniRealm("classpath:shiro.ini")); // 设置 Realm
SecurityUtils.setSecurityManager(defaultSecurityManager); // 将 SecurityManager 设置到 SecurityUtils 工具类中
Subject subject = SecurityUtils.getSubject(); // 通过 SecurityUtils 获取 subject 对象

try {
    subject.login(new UsernamePasswordToken(username, password)); // 通过 subject对象.login 进行认证, 通过账号密码进行认证, 认证失败则抛出异常
    System.out.println("登陆成功!");
    System.out.println(subject.hasRole("seller")); // 判断角色: hacker 登录后返回 false, heihuUser | admin 登录后返回 true
    System.out.println(subject.isPermitted("order-del")); // 判断权限: 当前用户是否由 order-del 权限, heihuUser 登录返回 true, hacker 登录返回 false
} catch (IncorrectCredentialsException e) {
    System.out.println("登陆失败!");
} catch (UnknownAccountException e) {
    System.out.println("用户名不存在!");
}

其中流程如下:

image-20241007174301593.png

IniRealm只不过是实现了AuthorizingRealm接口,Shiro框架提供出来了罢了, 其中IniRealm实现了doGetAuthorizationInfo & doGetAuthenticationInfo方法, 这两个方法会根据传递过来的token类型来进入到具体的方法.

SpringBoot 整合 Shiro

IniRealm

如果我们想在SpringBoot中进行使用Shiro, 那么我们肯定是需要围绕如下环节进行研究.

创建 pom.xml:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.5.3</version>
</parent>

<dependencies>
    <dependency> <!-- 导入 shiro-spring, 会自动引入 shiro-core, shiro-web -->
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-spring</artifactId>
        <version>1.4.1</version>
    </dependency>
    <dependency> <!-- springboot 没有提供对 shiro 的自动配置, shiro 的自动配置需手动完成 -->
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency> <!-- 引入 thymeleaf 模板引擎 -->
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    <dependency> <!-- 引入 lombok -->
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
    <dependency> <!-- 引入 druid-spring-boot-starter, 自动配置 Druid -->
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.1.17</version>
    </dependency>
    <dependency> <!-- 会自动引入 mybatis, mybatis-spring, spring-boot-starter-jdbc -->
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.2.2</version>
    </dependency>
    <dependency> <!-- 引入 mysql 扩展 -->
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.28</version>
    </dependency>
    <dependency> <!-- 引入 SpringBoot 测试依赖 -->
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
    </dependency>
</dependencies>

创建/resources/application.yml:

server: # 设置启动端口
  port: 80

spring:
  thymeleaf: # 设置 thymeleaf 模板存放位置
    prefix: "classpath:/templates/"
    suffix: ".html"
  datasource:
    druid: # 设置数据库连接
      url: jdbc:mysql://localhost:3306/shiro?useSSL=true&characterEncoding=utf-8&useUnicode=true
      username: root
      password: root
      driver-class-name: com.mysql.cj.jdbc.Driver
  mvc:
    static-path-pattern: /static/** # 设置静态资源访问路径
  web:
    resources:
      static-locations: classpath:/static/** # 设置静态资源保存目录

mybatis:
  mapper-locations: classpath:mappers/*.xml # 设置 mybatis mapper文件存放位置, 用于扫描
  type-aliases-package: com.heihu577.bean # 设置 JavaBean 存放位置

定义com.heihu577.MainApp类:

@SpringBootApplication
public class MainApp {
    public static void main(String[] args) {
        ConfigurableApplicationContext ioc = SpringApplication.run(MainApp.class, args);
    }
}

因为Shiro需要我们手动配置, 所以我们定义com.heihu577.config.ShiroAutoConfiguration类如下:

@Configuration
public class ShiroAutoConfiguration {
    @Bean
    public IniRealm getIniRealm() { // 先使用 IniReal 做演示
        IniRealm iniRealm = new IniRealm("classpath:shiro.ini");
        /* classpath:shiro.ini 文件内容如下:
            [users]
            heihuUser=heihuPass,seller
            hacker=123456,ckmgr
            admin=admin888,admin

            [roles]
            seller=order-add,order-del,order-list
            ckmgr=ck-add,ck-del,ck-list
            admin=*
        */
        return iniRealm;
    }

    @Bean
    public SecurityManager getSecurityManager(IniRealm iniRealm) {
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        defaultWebSecurityManager.setRealm(iniRealm); // 要想完成校验, 需要 Realm
        // SecurityUtils.setSecurityManager(defaultWebSecurityManager); // 设置 SecurityUtils 下的 SecurityManager
        return defaultWebSecurityManager;
    }

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); // 准备一个拦截器, 用于拦截用户请求
        shiroFilterFactoryBean.setSecurityManager(securityManager); // 进行数据校验的核心是 SecurityManager, 所以这里需要配置 SecurityManager
        // 配置拦截规则...
        HashMap<String,String> filterMap = new HashMap();
        filterMap.put("/", "anon"); // anon 匿名用户可访问
        filterMap.put("/login", "anon"); // 对 login.html 不拦截
        filterMap.put("/register", "anon"); // 对 register.html 不拦截
        filterMap.put("/user/login", "anon");
        filterMap.put("/**", "authc"); // authc 认证用户可访问
        filterMap.put("/static/**", "anon"); // 对 /static/** 都不拦截
        // user: 使用 RememberMe 用户可访问
        // perms: 对应权限可访问
        // role: 对应角色可访问
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap); // 将规则设置进来
        shiroFilterFactoryBean.setLoginUrl("/login"); // 设置默认的登录界面
        shiroFilterFactoryBean.setUnauthorizedUrl("/"); // 设置未授权访问的跳转URL
        return shiroFilterFactoryBean;
    }
}

随后定义com.heihu577.controller.PageController && com.heihu577.controller.UserController控制器如下:

@Controller
public class PageController {
    @RequestMapping(value = {"/"})
    public String index() {
        return "index"; // 跳转 thymeleaf 下的 index.html 模板引擎
    }

    @RequestMapping("/login")
    public String login() {
        return "login";
    }
}
@Controller
@RequestMapping("/user")
public class UserController {
    @Resource
    private UserServiceImpl userService; // 自动注入 userService

    @PostMapping("/login")
    public String login(String username, String password) {
        try {
            userService.login(username, password);
            System.out.println("登陆成功!");
            return "index"; // 登录成功去 index 页面.
        } catch (Exception e) { // 使用 try-catch 进行判断登录状态
            System.out.println("登录失败!");
            return "login"; // 登录失败去 login 页面, 若想要注入数据, 使用 Model 即可
        }
    }
}

定义两个模板如下:

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>主页</title>
</head>
<body>
    <h1>Hello World!</h1>
</body>
</html>
<!-- login.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录表单</title>
</head>
<body>
    <form action="/user/login" method="post">
        u: <input type="text" name="username"/><br>




        p: <input type="password" name="password"/><br>




        <input type="submit">
    </form>
</body>
</html>

定义com.heihu577.service.UserServiceImpl如下:

@Service
public class UserServiceImpl {
    public void login(String username, String password)  throws Exception { // 登陆失败会抛出异常, 登陆成功没返回值
        UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(username, password);
        Subject subject = SecurityUtils.getSubject();
        subject.login(usernamePasswordToken); // 发送登录请求.
    }
}

最终可以实现用户登录, 判断账号密码是否正确. 当前为了学习方便, 使用了 IniRealm 进行演示, 其中思路如下.

image-20241008121529770.png

而如果我们想要使用JdbcRealm, 那么我们则需要配置相应的Realm.

JdbcRealm

其中JdbcRealm需要创建如下表结构:

CREATE TABLE `users`(
	id int primary key auto_increment,
    username varchar(60) not null unique,
    password varchar(60) not null,
    password_salt varchar(20)
);
-- 创建五个用户如下
INSERT INTO `users`(username, password) VALUES('zhangsan', '123456');
INSERT INTO `users`(username, password) VALUES('lisi', '123456');
INSERT INTO `users`(username, password) VALUES('wangwu', '123456');
INSERT INTO `users`(username, password) VALUES('zhaoliu', '123456');
INSERT INTO `users`(username, password) VALUES('chenqi', '123456');

CREATE TABLE `user_roles`(
	id int primary key auto_increment,
    username varchar(60) not null,
    role_name varchar(100) not null
);
-- 给这五个用户分别增加如下角色
-- admin 系统管理人员
-- cmanager 库管人员
-- xmanager 销售人员
-- kmanager 客服人员
-- zmanager 行政人员
INSERT INTO `user_roles`(username, role_name) VALUES('zhangsan', 'admin');
INSERT INTO `user_roles`(username, role_name) VALUES('lisi', 'cmanager');
INSERT INTO `user_roles`(username, role_name) VALUES('wangwu', 'xmanager');
INSERT INTO `user_roles`(username, role_name) VALUES('zhaoliu', 'kmanager');
INSERT INTO `user_roles`(username, role_name) VALUES('chenqi', 'zmanager');

CREATE TABLE `roles_permissions`(
	id int primary key auto_increment,
    role_name varchar(100) not null,
    permission varchar(100) not null
);
-- admin 具备所有权限
INSERT INTO `roles_permissions`(`role_name`,`permission`) VALUES("admin", "*");
-- 库管人员具备的权限
INSERT INTO `roles_permissions`(`role_name`,`permission`) VALUES("cmanager", "sys:c:save");
INSERT INTO `roles_permissions`(`role_name`,`permission`) VALUES("cmanager", "sys:c:delete");
INSERT INTO `roles_permissions`(`role_name`,`permission`) VALUES("cmanager", "sys:c:update");
INSERT INTO `roles_permissions`(`role_name`,`permission`) VALUES("cmanager", "sys:c:find");
-- 销售人员具备的权限
INSERT INTO `roles_permissions`(`role_name`,`permission`) VALUES("xmanager", "sys:c:find");

INSERT INTO `roles_permissions`(`role_name`,`permission`) VALUES("xmanager", "sys:x:save");
INSERT INTO `roles_permissions`(`role_name`,`permission`) VALUES("xmanager", "sys:x:delete");
INSERT INTO `roles_permissions`(`role_name`,`permission`) VALUES("xmanager", "sys:x:update");
INSERT INTO `roles_permissions`(`role_name`,`permission`) VALUES("xmanager", "sys:x:find");

INSERT INTO `roles_permissions`(`role_name`,`permission`) VALUES("xmanager", "sys:k:save");
INSERT INTO `roles_permissions`(`role_name`,`permission`) VALUES("xmanager", "sys:k:delete");
INSERT INTO `roles_permissions`(`role_name`,`permission`) VALUES("xmanager", "sys:k:update");
INSERT INTO `roles_permissions`(`role_name`,`permission`) VALUES("xmanager", "sys:k:find");
-- 客服人员所具备的权限
INSERT INTO `roles_permissions`(`role_name`,`permission`) VALUES("kmanager", "sys:k:update");
INSERT INTO `roles_permissions`(`role_name`,`permission`) VALUES("kmanager", "sys:k:find");
-- 行政人员
INSERT INTO `roles_permissions`(`role_name`,`permission`) VALUES("zmanager", "sys:*:find");

当前表结构如果想要查询具体用户是否具有某种权限的话, 关系如下:

image-20241008154258677.png

那么此时我们修改ShiroAutoConfiguration类代码如下:

@Configuration
public class ShiroAutoConfiguration {
//    @Bean
//    public IniRealm getIniRealm() { // 先使用 IniReal 做演示
//        IniRealm iniRealm = new IniRealm("classpath:shiro.ini");
//        return iniRealm;
//    }

    @Bean
    public JdbcRealm getJdbcRealm(DataSource dataSource) {
        JdbcRealm jdbcRealm = new JdbcRealm();
        jdbcRealm.setDataSource(dataSource); // druid-spring-boot-starter 已经配置好了, 直接引用即可
        jdbcRealm.setPermissionsLookupEnabled(true); // 默认开启认证功能, 需手动开启授权功能
        return jdbcRealm;
    }


    @Bean
    public SecurityManager getSecurityManager(JdbcRealm ream) {
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        defaultWebSecurityManager.setRealm(ream); // 要想完成校验, 需要 Realm
        // SecurityUtils.setSecurityManager(defaultWebSecurityManager); // 设置 SecurityUtils 下的 SecurityManager
        return defaultWebSecurityManager;
    }

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        HashMap<String,String> filterMap = new HashMap();
        filterMap.put("/", "anon");
        filterMap.put("/login", "anon");
        filterMap.put("/register", "anon");
        filterMap.put("/user/login", "anon");
        filterMap.put("/**", "authc");
        filterMap.put("/static/**", "anon");

        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap); // 将规则设置进来
        shiroFilterFactoryBean.setLoginUrl("/login"); // 设置默认的登录界面
        shiroFilterFactoryBean.setUnauthorizedUrl("/"); // 设置未授权访问的跳转URL
        return shiroFilterFactoryBean;
    }
}

修改完具体Realm后, 我们的功能也成功生效了.

Shiro 标签使用

当我们登录成功后, 需要显示当前用户信息, 以及对应的权限功能入口, Shiro 提供了一套标签, 可以应用于 Thymeleaf, jsp 中。

当前我们的环境是 Thymeleaf, 所以我们必须在pom.xml中导入依赖:

<dependency> <!-- 引入 shiro 标签依赖 -->
    <groupId>com.github.theborakompanioni</groupId>
    <artifactId>thymeleaf-extras-shiro</artifactId>
    <version>2.1.0</version>
</dependency>

随后我们需要在我们的配置类中定义一个Bean:

@Bean
public ShiroDialect getShiroDialect() {
    return new ShiroDialect();
}

随后我们在具体模板中进行声明引用即可:

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

修改/resources/templates/index.html文件内容如下:

<!DOCTYPE html>
<html lang="en" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head>
    <meta charset="UTF-8">
    <title>主页</title>
</head>
<body>
<shiro:guest>
    欢迎游客访问 | <a href="/login">登录</a> <!-- 当用户没有登录时会显示该内容 -->
</shiro:guest>
<shiro:user>
    欢迎用户名为: <shiro:principal/> 的用户访问! <br>



 <!-- 当前已经登陆成功的状态, 会显示该内容 <shiro:principal/> 获取当前用户名 -->
    <hr>
    当前用户角色为:
    <shiro:hasRole name="admin"> 超级管理员</shiro:hasRole> <!-- 判断当前的角色 -->
    <shiro:hasRole name="cmanager"> 库管人员</shiro:hasRole>
    <shiro:hasRole name="xmanager"> 销售人员</shiro:hasRole>
    <shiro:hasRole name="kmanager"> 客服人员</shiro:hasRole>
    <shiro:hasRole name="zmanager"> 行政人员</shiro:hasRole>
    <hr>
    <br>



当前所拥有的权限为: <br>




    <hr>
    仓库管理
    <ul>
        <shiro:hasPermission name="sys:c:save"><li><a href="#">入库</a></li></shiro:hasPermission>
        <shiro:hasPermission name="sys:c:delete"><li><a href="#">出库</a></li></shiro:hasPermission>
        <shiro:hasPermission name="sys:c:update"><li><a href="#">更新仓库</a></li></shiro:hasPermission>
        <shiro:hasPermission name="sys:c:find"><li><a href="#">查找仓库</a></li></shiro:hasPermission>
    </ul>
    <hr>
    销售管理
    <ul>
        <shiro:hasPermission name="sys:x:save"><li><a href="#">保存订单</a></li></shiro:hasPermission>
        <shiro:hasPermission name="sys:x:delete"><li><a href="#">删除订单</a></li></shiro:hasPermission>
        <shiro:hasPermission name="sys:x:update"><li><a href="#">更新订单</a></li></shiro:hasPermission>
        <shiro:hasPermission name="sys:x:find"><li><a href="#">查询订单</a></li></shiro:hasPermission>
    </ul>
    客户管理
    <ul>
        <shiro:hasPermission name="sys:k:save"><li><a href="#">新增客户</a></li></shiro:hasPermission>
        <shiro:hasPermission name="sys:k:delete"><li><a href="#">删除客户</a></li></shiro:hasPermission>
        <shiro:hasPermission name="sys:k:update"><li><a href="#">修改客户</a></li></shiro:hasPermission>
        <shiro:hasPermission name="sys:k:find"><li><a href="#">查询客户</a></li></shiro:hasPermission>
    </ul>
</shiro:user>
</body>
</html>

不同用户登录效果:

image-20241008180754620.png

自定义 Realm

在真正的项目中, 我们不会使用Shiro提供的JdbcRealm, 而是使用自定义Realm, 配合我们的MyBatis, 以及自定义表结构进行联合使用.

image-20241008184622995.png

表结构定义

那么下面我们来定义这些表:

-- 用户信息表
CREATE TABLE `tb_users`(
	user_id int unsigned primary key auto_increment,
    username varchar(60) not null unique,
    password varchar(60) not null,
    password_salt varchar(60)
);
INSERT INTO `tb_users`(username, password) VALUES('zhangsan', '123456');
INSERT INTO `tb_users`(username, password) VALUES('lisi', '123456');
INSERT INTO `tb_users`(username, password) VALUES('wangwu', '123456');
INSERT INTO `tb_users`(username, password) VALUES('zhaoliu', '123456');
INSERT INTO `tb_users`(username, password) VALUES('chenqi', '123456');

-- 角色信息表
CREATE TABLE `tb_roles`(
	role_id int unsigned primary key auto_increment,
    role_name varchar(60) not null
);
INSERT INTO `tb_roles`(role_name) VALUES('admin'); -- 系统管理员
INSERT INTO `tb_roles`(role_name) VALUES('cmanager'); -- 仓管
INSERT INTO `tb_roles`(role_name) VALUES('xmanager'); -- 销售
INSERT INTO `tb_roles`(role_name) VALUES('kmanager'); -- 客服
INSERT INTO `tb_roles`(role_name) VALUES('zmanager'); -- 行政

-- 权限信息表
CREATE TABLE `tb_permissions`(
	permission_id int primary key auto_increment,
    permission_code varchar(60) not null,
    permission_name varchar(60)
);
INSERT INTO `tb_permissions`(`permission_code`,`permission_name`) VALUES("sys:c:save", "入库");
INSERT INTO `tb_permissions`(`permission_code`,`permission_name`) VALUES("sys:c:delete", "出库");
INSERT INTO `tb_permissions`(`permission_code`,`permission_name`) VALUES("sys:c:update", "修改");
INSERT INTO `tb_permissions`(`permission_code`,`permission_name`) VALUES("sys:c:find", "查询");

INSERT INTO `tb_permissions`(`permission_code`,`permission_name`) VALUES("sys:x:save", "新增订单");
INSERT INTO `tb_permissions`(`permission_code`,`permission_name`) VALUES("sys:x:delete", "删除订单");
INSERT INTO `tb_permissions`(`permission_code`,`permission_name`) VALUES("sys:x:update", "修改订单");
INSERT INTO `tb_permissions`(`permission_code`,`permission_name`) VALUES("sys:x:find", "查询订单");

INSERT INTO `tb_permissions`(`permission_code`,`permission_name`) VALUES("sys:k:save", "新增客户");
INSERT INTO `tb_permissions`(`permission_code`,`permission_name`) VALUES("sys:k:delete", "删除客户");
INSERT INTO `tb_permissions`(`permission_code`,`permission_name`) VALUES("sys:k:update", "修改客户");
INSERT INTO `tb_permissions`(`permission_code`,`permission_name`) VALUES("sys:k:find", "查询客户");

-- 用户角色表
CREATE TABLE `tb_urs`(
	uid int not null,
    rid int not null
);
INSERT INTO `tb_urs` VALUES(1,1); -- 第1个用户是第1个角色 (zhangsan 是 admin 角色)
INSERT INTO `tb_urs` VALUES(2,2);
INSERT INTO `tb_urs` VALUES(3,3);
INSERT INTO `tb_urs` VALUES(4,4);
INSERT INTO `tb_urs` VALUES(5,5);

-- 角色权限表
CREATE TABLE `tb_rps`(
	rid int not null,
    pid int not null
);
INSERT INTO `tb_rps` VALUES(2,1); -- 仓库管理员拥有四个权限
INSERT INTO `tb_rps` VALUES(2,2);
INSERT INTO `tb_rps` VALUES(2,3);
INSERT INTO `tb_rps` VALUES(2,4);
INSERT INTO `tb_rps` VALUES(3,5); -- 销售人员具有九个权限, 包含客服人员的权限, 以及仓库查询权限
INSERT INTO `tb_rps` VALUES(3,4);
INSERT INTO `tb_rps` VALUES(3,6);
INSERT INTO `tb_rps` VALUES(3,7);
INSERT INTO `tb_rps` VALUES(3,8);
INSERT INTO `tb_rps` VALUES(3,9);
INSERT INTO `tb_rps` VALUES(3,10);
INSERT INTO `tb_rps` VALUES(3,11);
INSERT INTO `tb_rps` VALUES(3,12);
INSERT INTO `tb_rps` VALUES(4,11); -- 客服人员具有两个权限, 查询和修改
INSERT INTO `tb_rps` VALUES(4,12);
INSERT INTO `tb_rps` VALUES(5,12); -- 行政人员具备所有查询功能
INSERT INTO `tb_rps` VALUES(5,8);
INSERT INTO `tb_rps` VALUES(5,4);

由于是自定义Realm, 所以查询数据的操作应该由我们自己手动完成, 所以这里我们应该配合我们的MyBatis进行查询数据信息.

DAO 设计

因为我们需要从数据库中拿数据, 那么我们这里可以参考一下JdbcRealm做了什么:

public class JdbcRealm extends AuthorizingRealm {
   protected static final String DEFAULT_AUTHENTICATION_QUERY = "select password from users where username = ?";
    protected static final String DEFAULT_SALTED_AUTHENTICATION_QUERY = "select password, password_salt from users where username = ?";
    // ↑ 根据用户名查询用户信息
    protected static final String DEFAULT_USER_ROLES_QUERY = "select role_name from user_roles where username = ?";
    // ↑ 查询具体用户名的角色名称
    protected static final String DEFAULT_PERMISSIONS_QUERY = "select permission from roles_permissions where role_name = ?";
    // ↑ 根据角色名查询权限列表
}

当然如果我们想要自定义Realm, 我们也需要制定这些业务场景的查询语句. 为了使用我们的 MyBatis 联动 Realm, 这里我们重新建立一个干净的项目.

引入依赖:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId> <!-- 引入 parent -->
    <version>2.5.3</version>
</parent>
<dependencies>
    <dependency> <!-- 导入 shiro-spring, 会自动引入 shiro-core, shiro-web -->
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-spring</artifactId>
        <version>1.4.1</version>
    </dependency>
    <dependency> <!-- springboot 没有提供对 shiro 的自动配置, shiro 的自动配置需手动完成 -->
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency> <!-- 引入 thymeleaf 模板引擎 -->
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    <dependency> <!-- 引入 lombok -->
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
    <dependency> <!-- 引入 druid-spring-boot-starter, 自动配置 Druid -->
        <groupId>com.alibaba</gr
# 代码审计 # Java代码审计
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录