在真正的项目中, 我们不会使用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</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.1.17</version></dependency><dependency> <!-- 引入 shiro 标签依赖 --><groupId>com.github.theborakompanioni</groupId><artifactId>thymeleaf-extras-shiro</artifactId><version>2.1.0</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>
application.yml
:
server:port: 80mybatis:type-aliases-package: com.heihu577.beanmapper-locations: classpath:mappers/*.xmlspring:datasource:druid:url: jdbc:mysql://localhost:3306/shiro2?useSSL=true&useUnicode=true&characterEncoding=utf-8username: rootpassword: rootdriver-class-name: com.mysql.cj.jdbc.Driver
定义Bean
:
package com.heihu577.bean;@Data
public class User {private Integer userId;private String username;private String password;private String passwordSalt;
}
MainApp
:
@SpringBootApplication
@MapperScan("com.heihu577.mapper")
public class MainApp {public static void main(String[] args) {ConfigurableApplicationContext ioc = SpringApplication.run(MainApp.class, args);}
}
UserMapper 根据用户名查询用户信息
定义Mapper接口
:
public interface UserMapper {// 根据用户名, 查询用户信息public User queryUserByUserName(@Param("username") String username);
}
随后我们创建/resources/mappers/UserMapper.xml:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.heihu577.mapper.UserMapper"><resultMap id="user" type="User"><id property="userId" column="user_id"/><result property="username" column="username"/><result property="password" column="password"/><result property="passwordSalt" column="password_salt"/></resultMap><select id="queryUserByUserName" resultMap="user" parameterType="String">SELECT * FROM `tb_users` WHERE `username` = #{username}</select>
</mapper>
RoleMapper 根据用户名查询角色信息
public interface RoleMapper {// 根据用户名, 查询出角色名称public Set<String> queryRoleByUserName(@RequestParam("username") String username);/** 涉及到联表查询* SELECT * FROM tb_users INNER JOIN tb_urs ON tb_users.user_id = tb_urs.uid INNER JOIN tb_roles ON tb_urs.rid = tb_roles.role_id;* +---------+----------+----------+---------------+-----+-----+---------+-----------+* | user_id | username | password | password_salt | uid | rid | role_id | role_name |* +---------+----------+----------+---------------+-----+-----+---------+-----------+* | 1 | zhangsan | 123456 | NULL | 1 | 1 | 1 | admin |* | 2 | lisi | 123456 | NULL | 2 | 2 | 2 | cmanager |* | 3 | wangwu | 123456 | NULL | 3 | 3 | 3 | xmanager |* | 4 | zhaoliu | 123456 | NULL | 4 | 4 | 4 | kmanager |* | 5 | chenqi | 123456 | NULL | 5 | 5 | 5 | zmanager |* +---------+----------+----------+---------------+-----+-----+---------+-----------+**/
}
定义Mapper文件
:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.heihu577.mapper.RoleMapper"><select id="queryRoleByUserName" resultType="String" parameterType="String">SELECT role_name FROM tb_usersINNER JOIN tb_urs ON tb_users.user_id = tb_urs.uidINNER JOIN tb_roles ON tb_urs.rid = tb_roles.role_idWHERE username = #{username};</select>
</mapper>
PermissionMapper 根据用户名查询权限信息
public interface PermissionMapper {public Set<String> queryPermissionByUserName(@RequestParam("username") String username);
}
定义Mapper文件
:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.heihu577.mapper.PermissionMapper"><select id="queryPermissionByUserName" resultType="String" parameterType="String">SELECT permission_code FROM tb_usersINNER JOIN tb_urs ON tb_users.user_id = tb_urs.uidINNER JOIN tb_roles ON tb_urs.rid = tb_roles.role_idINNER JOIN tb_rps ON tb_rps.rid = tb_roles.role_idINNER JOIN tb_permissions ON tb_permissions.permission_id = tb_rps.pidWHERE username = #{username};</select>
</mapper>
自定义 Realm 设计
定义如下Realm
:
public class MyRealm extends AuthorizingRealm { // 自定义 Realm 通常继承 AuthorizingRealm@Resourceprivate UserMapper userMapper;@Resourceprivate RoleMapper roleMapper;@Resourceprivate PermissionMapper permissionMapper;/*** @return 当前 Realm 名称, 可自定义名称*/@Overridepublic String getName() {return "MyRealm";}/*** 准备好授权数据, 授权数据就是当前用户的角色, 当前用户的权限信息, 所以我们只需要准备这些数据, 返回给 SecurityManager 即可.*/@Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {String username = (String) principals.iterator().next(); // 得到已经登录成功的用户名, 实际上获取到的内容是 doGetAuthenticationInfo 方法中 new SimpleAuthenticationInfo(用户名, 用户密码, 当前Realm名称) 中的第一个参数Set<String> roles = roleMapper.queryRoleByUserName(username); // 通过用户名得到角色名称Set<String> permissions = permissionMapper.queryPermissionByUserName(username); // 通过用户名得到权限信息SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();info.setRoles(roles); // 将数据库查询出来的信息封装到 AuthorizationInfo 中info.setStringPermissions(permissions);return info;}/*** 准备好认证数据, 我们无需操心比对, 比对最终交给 SecurityManager, 我们只需要提供数据就可以了.* 而认证数据, 我们只需要提供 账号,密码 即可.*/@Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {// subject.login(token) 会调用到这里UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token; // 认证时, 强制转换String username = usernamePasswordToken.getUsername(); // 得到用户名User user = userMapper.queryUserByUserName(username); // 从数据库中查询该用户名, 得到该用户信息if (user != null) {// 成功从数据库中查询到用户, 我们就将用户的信息封装到 AuthenticationInfo 中, SimpleAuthenticationInfo 是 AuthenticationInfo 的子类return new SimpleAuthenticationInfo(username, user.getPassword(), this.getName());// new SimpleAuthenticationInfo(用户名, 用户密码, 当前Realm名称)}return null;}
}
我们只需要重写doGetAuthenticationInfo方法, 并且返回AuthenticationInfo类型的数据即可,AuthenticationInfo类型的数据中封装的就是用户的账号与密码信息.
重写doGetAuthorizationInfo方法, 并且返回AuthorizationInfo类型的信息,AuthorizationInfo中包含了用户的角色, 权限信息.
重写getName方法, 为我们的自定义Realm增加一个名称.
从上面可以看到的是, 我们自定义Realm成功参与了自己的数据库查询逻辑在里面, 我们使用了MyBatis从数据库中取数据, 将数据放入到返回对象中. 因为比对工作是由SecurityManager完成的, 所以我们这里只需提供数据即可, 无需加入自己的业务逻辑判断. 当然, 为了验证方便, 我们依然使用之前的login.html, index.html, UserServiceImpl, PageController, UserController进行做测试即可.
最终运行效果:
不同的用户, 不同的角色, 具有不同的权限.
Layui 优化界面
去 https://www.layuicdn.com/docs/v2/demo/admin.html
中拷贝代码, 并且下载 Layui 所需要的 css 与 js.
并且将 Layui 中的 CSS 与 JS 放入到/resource/static目录下, 定义/resources/templates/index.html文件内容如下:
<!DOCTYPE html>
<html xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head><base href="/"> <!-- 一定要加入这一行代码, 避免 CSS, JS 引用出错. --><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>layout 管理系统大布局 - Layui</title><link rel="stylesheet" href="./layui/css/layui.css">
</head>
<body>
<div class="layui-layout layui-layout-admin"><div class="layui-header"><div class="layui-logo layui-hide-xs layui-bg-black">layout demo</div><!-- 头部区域(可配合layui 已有的水平导航) --><ul class="layui-nav layui-layout-left"><!-- 移动端显示 --><li class="layui-nav-item layui-show-xs-inline-block layui-hide-sm" lay-header-event="menuLeft"><i class="layui-icon layui-icon-spread-left"></i></li><li class="layui-nav-item layui-hide-xs"><a href="">nav 1</a></li><li class="layui-nav-item layui-hide-xs"><a href="">nav 2</a></li><li class="layui-nav-item layui-hide-xs"><a href="">nav 3</a></li><li class="layui-nav-item"><a href="javascript:;">nav groups</a><dl class="layui-nav-child"><dd><a href="">menu 11</a></dd><dd><a href="">menu 22</a></dd><dd><a href="">menu 33</a></dd></dl></li></ul><shiro:user><ul class="layui-nav layui-layout-right"><li class="layui-nav-item layui-hide layui-show-md-inline-block"><a href="javascript:;"><img src="x" alt="图片显示错误" class="layui-nav-img"><shiro:principal/></a><dl class="layui-nav-child"><dd><a href="">Your Profile</a></dd><dd><a href="">Settings</a></dd><dd><a href="">Sign out</a></dd></dl></li><li class="layui-nav-item" lay-header-event="menuRight" lay-unselect><a href="javascript:;"><i class="layui-icon layui-icon-more-vertical"></i></a></li></ul><shiro:user></div><div class="layui-side layui-bg-black"><div class="layui-side-scroll"><!-- 左侧导航区域(可配合layui已有的垂直导航) --><ul class="layui-nav layui-nav-tree" lay-filter="test"><li class="layui-nav-item layui-nav-itemed"><a class="" href="javascript:;">仓库管理</a><dl class="layui-nav-child"><shiro:hasPermission name="sys:c:save"><dd><a href="javascript:;">入库</a></dd></shiro:hasPermission><shiro:hasPermission name="sys:c:delete"><dd><a href="javascript:;">出库</a></dd></shiro:hasPermission><shiro:hasPermission name="sys:c:delete"><dd><a href="javascript:;">更新仓库</a></dd></shiro:hasPermission><shiro:hasPermission name="sys:c:delete"><dd><a href="">查找仓库</a></dd></shiro:hasPermission></dl></li><li class="layui-nav-item layui-nav-itemed"><a class="" href="javascript:;">销售管理</a><dl class="layui-nav-child"><shiro:hasPermission name="sys:x:save"><dd><a href="javascript:;">保存订单</a></dd></shiro:hasPermission><shiro:hasPermission name="sys:x:delete"><dd><a href="javascript:;">删除订单</a></dd></shiro:hasPermission><shiro:hasPermission name="sys:x:delete"><dd><a href="javascript:;">更新订单</a></dd></shiro:hasPermission><shiro:hasPermission name="sys:x:delete"><dd><a href="">查询订单</a></dd></shiro:hasPermission></dl></li><li class="layui-nav-item layui-nav-itemed"><a class="" href="javascript:;">客户管理</a><dl class="layui-nav-child"><shiro:hasPermission name="sys:k:save"><dd><a href="javascript:;">新增客户</a></dd></shiro:hasPermission><shiro:hasPermission name="sys:k:delete"><dd><a href="javascript:;">删除客户</a></dd></shiro:hasPermission><shiro:hasPermission name="sys:k:update"><dd><a href="javascript:;">修改客户</a></dd></shiro:hasPermission><shiro:hasPermission name="sys:k:find"><dd><a href="">查询客户</a></dd></shiro:hasPermission></dl></li></ul></div></div><div class="layui-body"><!-- 内容主体区域 --><div style="padding: 15px;">内容主体区域。记得修改 layui.css 和 js 的路径</div></div><div class="layui-footer"><!-- 底部固定区域 -->底部固定区域</div>
</div>
<script src="./layui/layui.js"></script>
<script>//JSlayui.use(['element', 'layer', 'util'], function () {var element = layui.element, layer = layui.layer, util = layui.util, $ = layui.$;//头部事件util.event('lay-header-event', {//左侧菜单事件menuLeft: function (othis) {layer.msg('展开左侧菜单的操作', {icon: 0});}, menuRight: function () {layer.open({type: 1, content: '<div style="padding: 15px;">处理右侧面板的操作</div>', area: ['260px', '100%'], offset: 'rt' //右上角, anim: 5, shadeClose: true});}});});
</script>
</body>
</html>
最终运行结果:
项目打包
在pom.xml
文件中增加如下内容:
<build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins>
</build>
随后用maven打包即可:
如果在别的机器进行部署, 这里数据库链接等信息一定要配置好, 否则项目启动不来.