前言
Shiro 的漏洞已爆出很多年, 我们只关心到了它如何触发, 有时并没有想过这个框架是干嘛的, 甚至没有分析过该框架的底层运行逻辑, 那么本篇文章, 让大家从开发者的角度, 来观察 Shiro 漏洞.
从开发者为什么使用 Shiro, 到 Shiro 底层运行逻辑, 再到 Shiro 漏洞原理刨析.
并不会像某些文章一样, 只是把漏洞点点明一下, 用 Payload 打一下, 就说这个漏洞已经“研究明白”了.
本篇文章分为三个部分:
第一部分来介绍 Shiro 的基本使用, 清晰的感受到 Shiro 框架给程序员带来的便捷.
第二部分来介绍 Shiro 底层源码的分析, 感受该框架设计的精妙.
第三部分来介绍 Shiro 漏洞分析, 在这里我们会梳理底层源码分析的流程, 一步一步涉及到漏洞点的产生, 顺便记载一下 Shiro 的攻击姿势.
本篇文章目录如下:
由于文章过长, 笔者将分为两篇进行发布.
权限管理
概念
为了实现不同身份登录系统, 出现的功能模块不一样. 这个需求叫做权限管理
.
学生登录后, 出现的功能模块为: 选课, 成绩查询, 课程表
老师登陆后, 出现的功能模块为: 学生管理, 成绩录入
那么有了这个基础的概念之后, 我们再看一个比较复杂的权限管理案例:
具体实现
第一种方式
这种方式适用于权限管理比较单一, 用户少, 每类用户权限固定的场景. 根据不同的页面来实现功能不一致的情况.
这种方式的缺点则是, 假设需要在销售人员主页
增加新的功能时, 我们需要修改index1.html
页面内容, 增加上新的功能, 需要后期慢慢维护.
这种基于页面的开发, 是不建议的.
第二种方式
这一种设计是RBAC (基于角色的访问控制)
的基本原型, 也不是最终版本, 看起来已经实现动态的显示功能效果了. 但是这里会存在一个问题.
假设我们新增了一个Heihu577
用户, 那么我们就需要在用户权限表
中增加很多权限, 那么假设Heihu577
用户所需要的权限与liucheng
用户权限一致, 那么用户权限表中又需要给Heihu577
用户分配很多权限id, 直到与用户liucheng
的权限id是一致的, 这样大大减少了灵活性.
那么这里我们需要引入角色
的概念,角色
是什么?我们不妨看下图进行理解.
现在的权限分配, 是根据角色的, 我们只需要指明某个用户是某个角色, 即可得到该角色的具体权限. 而这么做的弊端则是, 假设A & B
用户是同一角色, 而我们希望某一功能只给A用户
而不给B用户
, 这个时候怎么办呢?
我们只需要增加一个用户权限表
, 将额外的权限分配给具体用户即可, 当然随着业务逻辑的复杂, 我们的表也跟着复杂化了. 那么除了表结构的设计, 还需要我们程序设计的思想:
而这里过滤器/拦截器
部分我们也可以自己写流程, 判断当前是否是登录状态, 是否有SESSION等. 当我们考虑不全面时程序也可能出现BUG.
而这种情况我们也可以选择使用安全框架, 帮助我们在应用系统开发过程完成认证以及授权的工作. 而安全框架类似于一个保安的角色:
这是一个演唱会的案例, 根据你的门票类别, 到达具体的座位, 当然这一切都需要你去告诉这个保安如何匹配规则, 换到程序里, 我们也仅仅做一个配置即可. 而这里我们就可以选择使用 Shiro, Shiro 就做了这些事情, 类似于 Shiro 的框架还有很多, Spring Security, OAuth2等.
Shiro
Shiro 的核心功能如下:
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 的运行流程为如下:
这里 Subject 的创建是由 SecurityUtils 进行创建的, 后面我们代码会给出案例, 官方给出的图如下:
基于 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("用户名不存在!");
}
其中流程如下:
而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 进行演示, 其中思路如下.
而如果我们想要使用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");
当前表结构如果想要查询具体用户是否具有某种权限的话, 关系如下:
那么此时我们修改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>
不同用户登录效果:
自定义 Realm
在真正的项目中, 我们不会使用Shiro
提供的JdbcRealm
, 而是使用自定义Realm
, 配合我们的MyBatis
, 以及自定义表结构进行联合使用.
表结构定义
那么下面我们来定义这些表:
-- 用户信息表
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