距离上一篇文章不知道已经过去多久了,两年还是三年还是更久?期间工作性质变化,从开发到实施到运维到支撑,如果不是有朋友让帮忙搞一份代码,估计这个系列就彻底太监了。
thymeleaf不知道算不算生不逢时,前后端分离已经走的太远,从头做到尾的工作已经过去了好久,所以这篇文章不知道还对人有帮助没有。可能还会有一些小公司人手有限,开发人员身兼多职,一如我当年刚踏进这个行业的时候吧。不定期更新一下这个系列,还是希望能善始善终。如果最后代码完成,可能会发到github。
上一篇只做了一件事情,就是把工作页面简单设计了一下,但是角色、权限、菜单这些东西并没有从数据库读取,所以我们要把这些内容完善起来。
数据模型
在第二篇文章的时候我们说过要使用用户-角色-权限的模式来设计,那么相应的应该要有角色表、用户角色表、权限表、角色权限表
CREATE TABLE `t_role` (
`id` varchar(36) NOT NULL,
`role_name` varchar(36) DEFAULT NULL COMMENT '角色名称',
`role_desc` varchar(255) DEFAULT NULL COMMENT '角色描述',
`status` bit(1) DEFAULT b'1' COMMENT '角色状态,1可用0不可用',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`modify_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统角色表'
CREATE TABLE `t_user_role` (
`id` varchar(36) NOT NULL,
`user_id` varchar(36) NOT NULL COMMENT '用户ID,来自用户表',
`role_id` varchar(36) NOT NULL COMMENT '角色ID,来自角色表',
`status` bit(1) NOT NULL DEFAULT b'1' COMMENT '状态,1表示可用,0表示不可用',
`create_time` datetime ,
`modify_time` datetime ,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户角色对应表';
create table t_authority(
id varchar(36) primary key,
parent_id varchar(36) comment '上级权限ID,顶级权限的上级ID为0',
authority_name varchar(36) comment '权限名称',
level varchar(10) comment '权限级别,1,2,3,4级,1级为顶级权限',
has_child bit(1) default b'0' comment '是否有下级权限,当为4时不具备下级权限',
url varchar(60) comment '菜单URL',
status bit(1) default b'1' comment '权限状态,1可用0不可用',
memo varchar(255) comment '备注',
orderby int(2) default 0 comment '菜单排序',
system_authority bit(1) default b'0' comment '是否系统菜单1是0否,系统菜单禁止删除',
creator varchar(36) comment '创建者ID',
create_time datetime comment '创建时间',
modify_time datetime comment '更新时间')default charset utf8mb4 comment '系统权限表'
create table t_role_authority(
id varchar(36) primary key,
role_id varchar(36) ,
authority_id varchar(36),
status bit(1) default b'1' comment '状态,1可用0不可用,默认可用',
create_time datetime,
modify_time datetime) default charset utf8mb4 comment '角色权限表'
大致规划四个顶级菜单,系统管理、我的信息、资源管理和审批管理。出于日志完整性、安全审计等因素考虑,在权限表中增加创建者字段,表示该数据是由何人创建的。由于目前是自用系统,所以只有一个超级管理员角色,顺序插入预设的数据,并根据已插入的数据
insert into t_role values (uuid(),'超级管理员','系统管理员,拥有一切操作权限',b'1',current_time,current_time);
-- 顶级菜单
insert into t_authority values(uuid(),'0','系统管理','1',b'1','',b'1','系统菜单禁止删除',0,b'1','ae051a0e-28db-11ec-ab0a-f8e43b04c9a6',current_time,current_time);
insert into t_authority values(uuid(),'0','我的信息','1',b'1','',b'1','系统菜单禁止删除',0,b'1','ae051a0e-28db-11ec-ab0a-f8e43b04c9a6',current_time,current_time);
insert into t_authority values(uuid(),'0','资源管理','1',b'1','',b'1','系统菜单禁止删除',0,b'1','ae051a0e-28db-11ec-ab0a-f8e43b04c9a6',current_time,current_time);
insert into t_authority values(uuid(),'0','审批管理','1',b'1','',b'1','系统菜单禁止删除',0,b'1','ae051a0e-28db-11ec-ab0a-f8e43b04c9a6',current_time,current_time);
select * from t_authority where authority_name = '系统管理';
-- 一级菜单
-- 系统管理
insert into t_authority values(uuid(),'a328e762-2b29-11ec-931b-e82a44fefbb2','用户管理','1',b'1','user/list',b'1','系统菜单禁止删除',0,b'1','ae051a0e-28db-11ec-ab0a-f8e43b04c9a6',current_time,current_time);
insert into t_authority values(uuid(),'a328e762-2b29-11ec-931b-e82a44fefbb2','角色管理','1',b'1','role/list',b'1','系统菜单禁止删除',1,b'1','ae051a0e-28db-11ec-ab0a-f8e43b04c9a6',current_time,current_time);
insert into t_authority values(uuid(),'a328e762-2b29-11ec-931b-e82a44fefbb2','权限管理','1',b'1','role/list',b'1','系统菜单禁止删除',2,b'1','ae051a0e-28db-11ec-ab0a-f8e43b04c9a6',current_time,current_time);
insert into t_authority values(uuid(),'a328e762-2b29-11ec-931b-e82a44fefbb2','用户管理','1',b'1','user/list',b'1','系统菜单禁止删除',0,b'1','ae051a0e-28db-11ec-ab0a-f8e43b04c9a6',current_time,current_time);
insert into t_authority values(uuid(),'a328e762-2b29-11ec-931b-e82a44fefbb2','角色管理','1',b'1','role/list',b'1','系统菜单禁止删除',1,b'1','ae051a0e-28db-11ec-ab0a-f8e43b04c9a6',current_time,current_time);
insert into t_authority values(uuid(),'a328e762-2b29-11ec-931b-e82a44fefbb2','字典表管理','1',b'1','dict/list',b'1','系统菜单禁止删除',2,b'1','ae051a0e-28db-11ec-ab0a-f8e43b04c9a6',current_time,current_time);
insert into t_authority values(uuid(),'a328e762-2b29-11ec-931b-e82a44fefbb2','日志管理','1',b'1','log/list',b'1','系统菜单禁止删除',3,b'1','ae051a0e-28db-11ec-ab0a-f8e43b04c9a6',current_time,current_time);
insert into t_authority values(uuid(),'a328e762-2b29-11ec-931b-e82a44fefbb2','审计管理','1',b'1','audit/list',b'1','系统菜单禁止删除',4,b'1','ae051a0e-28db-11ec-ab0a-f8e43b04c9a6',current_time,current_time);
insert into t_authority values(uuid(),'a328e762-2b29-11ec-931b-e82a44fefbb2','IP白名单管理','1',b'1','ipWhite/list',b'1','系统菜单禁止删除',5,b'1','ae051a0e-28db-11ec-ab0a-f8e43b04c9a6',current_time,current_time);
-- 我的信息
select * from t_authority where authority_name = '我的信息';
insert into t_authority values(uuid(),'d5847dd4-2b29-11ec-931b-e82a44fefbb2','我的资料','1',b'1','userInfo/list',b'1','系统菜单禁止删除',0,b'1','ae051a0e-28db-11ec-ab0a-f8e43b04c9a6',current_time,current_time);
insert into t_authority values(uuid(),'d5847dd4-2b29-11ec-931b-e82a44fefbb2','我的首页','1',b'1','myIndex/list',b'1','系统菜单禁止删除',1,b'1','ae051a0e-28db-11ec-ab0a-f8e43b04c9a6',current_time,current_time);
-- 资源管理
select * from t_authority where authority_name = '资源管理';
insert into t_authority values(uuid(),'eee51281-2b29-11ec-931b-e82a44fefbb2','网络设备','1',b'1','netDevice/list',b'1','系统菜单禁止删除',0,b'1','ae051a0e-28db-11ec-ab0a-f8e43b04c9a6',current_time,current_time);
insert into t_authority values(uuid(),'eee51281-2b29-11ec-931b-e82a44fefbb2','应用系统','1',b'1','webapp/list',b'1','系统菜单禁止删除',1,b'1','ae051a0e-28db-11ec-ab0a-f8e43b04c9a6',current_time,current_time);
insert into t_authority values(uuid(),'eee51281-2b29-11ec-931b-e82a44fefbb2','IP管理','1',b'1','ip/list',b'1','系统菜单禁止删除',2,b'1','ae051a0e-28db-11ec-ab0a-f8e43b04c9a6',current_time,current_time);
insert into t_authority values(uuid(),'eee51281-2b29-11ec-931b-e82a44fefbb2','接口资源','1',b'1','interface/list',b'1','系统菜单禁止删除',3,b'1','ae051a0e-28db-11ec-ab0a-f8e43b04c9a6',current_time,current_time);
-- 审批管理
select * from t_authority where authority_name = '审批管理';
insert into t_authority values(uuid(),'00b9721b-2b2a-11ec-931b-e82a44fefbb2','客户管理','1',b'1','customer/list',b'1','系统菜单禁止删除',0,b'1','ae051a0e-28db-11ec-ab0a-f8e43b04c9a6',current_time,current_time);
insert into t_authority values(uuid(),'00b9721b-2b2a-11ec-931b-e82a44fefbb2','客户资源权限','1',b'1','customerResource/list',b'1','系统菜单禁止删除',1,b'1','ae051a0e-28db-11ec-ab0a-f8e43b04c9a6',current_time,current_time);
insert into t_authority values(uuid(),'00b9721b-2b2a-11ec-931b-e82a44fefbb2','访问申请','1',b'1','access/list',b'1','系统菜单禁止删除',2,b'1','ae051a0e-28db-11ec-ab0a-f8e43b04c9a6',current_time,current_time);
insert into t_authority values(uuid(),'00b9721b-2b2a-11ec-931b-e82a44fefbb2','凭证管理','1',b'1','key/list',b'1','系统菜单禁止删除',3,b'1','ae051a0e-28db-11ec-ab0a-f8e43b04c9a6',current_time,current_time);
忽然发现复制粘贴导致所有菜单的级别都是1,赶紧更新一下
update t_authority set level = 2 where parent_id != '0';
基本上顶级菜单和左侧菜单的功能都有了,再把用户角色表和角色权限表数据补一下
insert into t_user_role select uuid(),u.id,r.id,b'1',current_time,current_time from t_user u,t_role r;
insert into t_role_authority select uuid(), r.id,a.id ,b'1' ,current_time,current_time from t_role r,t_authority a;
对应的entry创建好
业务逻辑
根据设计逻辑,登录成功后,要读取登录人的信息,首先要获得登录人拥有的菜单权限,由于页面分级展示,所以需要有不同的菜单获取功能,首先是要获取该用户有哪些顶级菜单功能,这样我们在top.html中来展现他的顶级菜单。然后用户再点击顶级菜单来选择下一级菜单,来刷新menu.html的页面,最后用户点击menu.html的菜单来刷新center.html的页面
正常来说,我们应该有个MenuAction来完成菜单权限的相关操作,但是用户登录成功时直接就要进行菜单展示,所以在LoginAction中就要做相关的操作了,用户在登录时也应该一次性获取用户自己的信息和权限。
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import com.bjca.base.BaseAction;
import com.bjca.bean.LoginModel;
import com.bjca.model.Authority;
import com.bjca.model.Role;
import com.bjca.model.User;
import com.bjca.service.LoginService;
import com.bjca.service.MenuService;
import com.bjca.service.RoleService;
@Controller
@RequestMapping(value="/")
public class LoginAction extends BaseAction{
@Autowired
private LoginService loginService;
@Autowired
private MenuService menuService;
@Autowired
private RoleService roleService;
@RequestMapping(value="/")
public String toLogin(@ModelAttribute(value="model") LoginModel loginModel,Model model){
model.addAttribute("j_username","");
model.addAttribute("j_password","");
return "index";
}
@RequestMapping(value="/index")
public String index(@ModelAttribute(value="model") LoginModel loginModel,Model model){
model.addAttribute("j_username", "");
model.addAttribute("j_password","");
return "index.html";
}
@RequestMapping(value="/login")
public String login(@ModelAttribute(value="model") LoginModel loginModel,Model model){
System.out.println("登录了登录了登录了");
//Map<String,Object> m = model.asMap();
//Set<String> key = m.keySet();
//System.out.println(SpringUtil.getApplicationContext()==null);
System.out.println(loginModel.getJ_username());
System.out.println(loginModel.getJ_password());
if(loginModel.getJ_username()==null){
return "index.html";
}
if(loginModel.getJ_password()==null){
return "index.html";
}
User user = new User();
user.setLoginName(loginModel.getJ_username());
user.setPassword(loginModel.getJ_password());
user.setPasswordBase64(loginModel.getJ_password());
loginModel = loginService.loginByPassword(user);
model.addAttribute("j_username", loginModel.getJ_username());
model.addAttribute("j_password",loginModel.getJ_password());
model.addAttribute("login_msg",loginModel.getLogin_msg());
model.addAttribute("login_status",loginModel.isLogin_status());
System.out.println(loginModel.getLogin_msg());
if(loginModel.isLogin_status()){
HttpSession session = request.getSession();
session.setAttribute("loginModel",loginModel);
String userId = loginModel.getUser().getId();
List<Role> roleList = roleService.getRoleListByUserId(userId);
List<Authority> topAuthorityList = menuService.getTopMenus(userId);
session.setAttribute("roleList",roleList);
session.setAttribute("topAuthorityList",topAuthorityList);
Map<String,List<Authority>> secondAuthorityList = menuService.getSecondMenusByUserId(userId);
session.setAttribute("secondAuthorityList", secondAuthorityList);
model.addAttribute("topMenus",topAuthorityList);
return "main";
}
return "index";
}
@RequestMapping(value="/logout")
public String logout(@ModelAttribute(value="model") LoginModel loginModel,Model model){
System.out.println("登出了登出了");
request.getSession().invalidate();
model.addAttribute("j_username","");
model.addAttribute("j_password","");
return "index.html";
}
}
上面的代码中,把用户拥有的顶级菜单权限获取到后作为页面返回到首页,把二级菜单按照顶级菜单的相关项读取后存放到session中,以供用户在切换顶级菜单时读取相应子菜单权限而不必再读取数据库。对应的工作代码如下
@RequestMapping("/main/choose_menu")
public String choose_menu(String id,Model model) {
//String id = request.getParameter("id");
System.out.println("菜单ID是:"+id);
Map<String,String> menus = new HashMap<String,String>();
Map<String,List<Authority>> secondAuthorityList = (Map<String,List<Authority>>) request.getSession().getAttribute("secondAuthorityList");
List<Authority> menuList = secondAuthorityList.get(id);
if(menuList==null){
menus.put("没有权限","unknow");
}else{
for(Authority bean:menuList){
menus.put(bean.getAuthorityName(), bean.getUrl());
}
}
model.addAttribute("menus", menus);
return "menu";
}
考虑到用户可能有顶级菜单权限却没有其子菜单的权限,从理论上来说不应该出现这种情况,这里作为一个简单的异常处理先放到这里。
由于大多数的Controller需要与页面交互,可能要注入HttpServlerRequest和HttpServlerRsesponse,所以注册一个公共的BaseAction,来抽象各个Controller的共同属性,让子类来继承BaseAction,随着应用的开发,也会有很多需要共用的内容会抽象到BaseAction中
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
public class BaseAction {
@Autowired
public HttpServletRequest request;
@Autowired
public HttpServletResponse response;
}
修改后的代码引入了RoleService和MenuService,相应的要哟RoleMapper和MenuMapper以及xml文件。AuthorityMapper.xml中的SQL语句
<select id="getTopMenus" parameterType="java.lang.String" resultMap="BaseResultMap">
select * from t_authority where
id in (
select authority_id from t_role_authority t,t_user_role r
where t.role_id = r.role_id and r.user_id = #{id,jdbcType=VARCHAR})
and parent_id = '0' and status = 1
</select>
<select id="getSecondMenusByUserId" parameterType="java.lang.String" resultMap="BaseResultMap">
select * from t_authority where
id in (
select authority_id from t_role_authority t,t_user_role r
where t.role_id = r.role_id and r.user_id = #{userId,jdbcType=VARCHAR})
and parent_id = #{id,jdbcType=VARCHAR} and status = 1
</select>
这里有一个mapper的接口方法带入了两个参数,要使用注解来说明参数调用情况
public interface AuthorityMapper {
List<Authority> getTopMenus(String id);
List<Authority> getSecondMenusByUserId(@Param("userId")String userId, @Param("id")String id);
}
对应的模板调整top.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<meta charset="UTF-8">
<div id="div1">
<p>
<span th:each="authority:${topMenus}">
<a href="#" th:data-id="${authority.id}" th:onclick="choose_menu(this.getAttribute('data-id'))">
<span th:text="${authority.authorityName}" />
</a>
</span>
</div>
<div id="div2">
<p>
<span><a href="#" onclick="logout()">登出</a></span>
</p>
</div>
</html>
menu.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<style type="text/css">
ul{ list-style: none outside none;}
</style>
</head>
<body>
<ul th:each="menu:${menus}">
<li><a href="#" th:data-value="${menu.value}" th:onclick="javascript:go_center(this.getAttribute('data-value'))"><span th:text="${menu.key}"></span></a></li>
</ul>
</body>
</html>
出于避免注入漏洞的考虑,thymeleaf调整了th:each的用法,所以页面也做相应的调整。
完成后的页面效果
可以展示,但是菜单的顺序和我们预想的不一致,预计是每个用户都拥有自己的"我的信息"菜单,但并不是每个用户都有系统管理、审批管理等权限,th:each中似乎没排序功能,所以在sql中按照orderby字段来进行一下排序看看效果
数据库记录本身顶级菜单没有排序,修改一下排序后再修改SQL
调整完以后,菜单顺序是按照orderby顺序来排了,但是二级菜单并没有按照排序来,这是由于做二次处理时把二级菜单提取出来存入到map中了,为了保持orderby的有效性,再调整一下menuAction的代码
@RequestMapping("/main/choose_menu")
public String choose_menu(String id,Model model) {
//String id = request.getParameter("id");
System.out.println("菜单ID是:"+id);
Map<String,List<Authority>> secondAuthorityList = (Map<String,List<Authority>>) request.getSession().getAttribute("secondAuthorityList");
List<Authority> menuList = secondAuthorityList.get(id);
//System.out.println(menuList.size());
if(menuList==null||menuList.size()==0){
model.addAttribute("hasAuthority",false);
}else{
model.addAttribute("hasAuthority",true);
model.addAttribute("menus", menuList);
}
return "menu";
}
页面代码
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<style type="text/css">
ul{ list-style: none outside none;}
</style>
</head>
<body>
<div th:switch="${hasAuthority}">
<div th:case="true">
<ul th:each="menu:${menus}">
<li><a href="#" th:data-value="${menu.url}" th:onclick="javascript:go_center(this.getAttribute('data-value'))"><span th:text="${menu.authorityName}"></span></a></li>
</ul>
</div>
<div th:case="*">
<ul>
<li>没有权限 </li>
</ul>
</div>
</div>
</body>
</html>
新增了一条顶级菜单用于测试效果,下面分别是点击系统管理和组织管理的效果