开发基于RBAC的权限控制模块
◆基于角色权限控制(RBAC)是面向企业安全策略的访问控制方式 ◆RBAC核心思想是将控制访问的资源与角色(Role)进行绑定 ◆系统的用户(User)与角色(Role)再进行绑定,用户便拥有对应权限
RBAC底层设计
打开navicat数据库,,选择imooc_oa数据库,点击查询,新建查询,将sql文件夹下的sql文件源代码复制粘贴,sql表就创建成功了
为了统一规范,防止主键外键类型不统一关联查询出现问题,所以牺牲空间,设置为bigint
分析各张表:
初识Element Plus
网站快速成型工具 Elment Plus,一套为开发者、设计师和产品经理准备的基于Vue3.0的桌面端组件库 一个 Vue 3 UI 框架 | Element Plus (gitee.io)
后端工程师推荐使用CDN方式进行安装,详细请查询官方文档 Element Plus依托于Vue组件,所以需要导入Vue插件 在webapp目录下新建assets文件夹,里面存放css、js、html等前端代码进行统一管理
首先新建vue文件夹,将学习vue3.0时的vue.global.js文件直接粘贴放入 还有Element Plus的资源目录也直接粘贴放入,还有axios.js文件
实现登陆功能
在webapp目录下新建login.html文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>MK网OA办公系统</title>
<!-- 引入样式 -->
<link rel="stylesheet" type="text/css" href="assets/element-plus/index.css">
<!-- 引入组件库 -->
<script src="/assets/vue/vue.global.js"></script>
<script src="/assets/element-plus/index.full.js"></script>
<script src="/assets/axios/axios.js"></script>
<style>
.login-box {
border: 1px solid #DCDFE6;
width: 350px;
margin: 180px auto;
padding: 35px 35px 15px 35px;
border-radius: 5px;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
box-shadow: 0 0 25px #909399;
}
.login-title{
text-align: center;
margin: 0 auto 40px auto;
color: #303133;
}
</style>
</head>
<body>
<div id="app">
<el-form ref="loginForm" label-width="80px" :rules="rules" :model="form" class="login-box">
<h2 class="login-title">MK网OA办公系统</h2>
<el-form-item label="账号" prop="username">
<el-input type="text" placeholder="请输入账号" v-model="form.username"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input type="password" placeholder="请输入密码" v-model="form.password"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" v-on:click="onSubmit('loginForm')" style="width:200px">登录</el-button>
</el-form-item>
</el-form>
</div>
<script>
const Main = {
data() {
return {
form: {
username: ''
,password: ''
}
,rules:{
username: [
{required: true,message : '账号不能为空' , trigger:'blur'}
],
password:[
{required: true,message : '密码不能为空' , trigger:'blur'}
]
}
}
}
,methods : {
onSubmit(formName){
const form = this.$refs[formName];
form.validate((valid) => {
if(valid){
console.info("表单校验成功,准备提交数据);
}
})
}
}
};
//初始化Vue,绑定Main中的数据,利用ElementPlus对#app容器进行重新渲染
const app = Vue.createApp(Main);
app.use(ElementPlus);
app.mount("#app");
</script>
</body>
</html>
el-form等标签是Element Plus组件里的特定标签 逗号写在前面便于注释和维护,不用再删上一个后面的逗号
Form 表单 | Element Plus (gitee.io)
输入localhost/login.html
<el-input type="password" placeholder="请输入密码" v-model="form.password"></el-input>
中把 type="text"
改为 type="password"
就可以密码输入时变成*号
实现用户登陆model层
为了便于测试,数据库添加test用户
记得提交事务,才保存成功
创建子包com.imooc.oa.entity,用于存储数据库的一个个对象 新建User.java类
package com.imooc.oa.entity;
public class User {
private Long userId; //user_id
private String username;
private String password;
private Long employeeId;
private Integer salt;
@Override
public String toString() {
return "User{" +
"userId=" + userId +
", username='" + username + '\'' +
", password='" + password + '\'' +
", employeeId=" + employeeId +
", salt=" + salt +
'}';
}
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Long getEmployeeId() {
return employeeId;
}
public void setEmployeeId(Long employeeId) {
this.employeeId = employeeId;
}
public Integer getSalt() {
return salt;
}
public void setSalt(Integer salt) {
this.salt = salt;
}
}
属性与数据库字段一一对应,已经开启驼峰命名转换 快速生成get、set以及toString方法 接下来为其添加sql语句配置 在resource→mappers文件夹下添加user.xml文件
<?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="usermapper">
<select id="selectByUsername" parameterType="String" resultType="com.imooc.oa.entity.User">
select * from sys_user where username = #{value}
</select>
</mapper>
*获取的数据库字段会自动驼峰命名转换向User对象进行赋值 写完后一定要记得在mabatis-config.xml进行注册配置让其知道User.xml文件的存在
我们在讲解到MVC模式的时候需要两个类,一个是service类,一个是mapper类 新建这两个包 其中mapper下新建UserMapper.java,用于处理数据库的增删改查
package com.imooc.oa.mapper;
import com.imooc.oa.entity.User;
import com.imooc.oa.utils.MybatisUtils;
public class UserMapper {
public User selectByUsername(String username){
User user = (User)MybatisUtils.executeQuery(sqlSession -> sqlSession.selectOne("usermapper.selectByUsername", username));
return user;
}
}
usermapper.selectByUsername
对应上面user.xml里的命名空间+id的访问方式
我们发现mapper不实现业务逻辑,只是纯粹用于数据库增删改查,具体实现要追溯到上一级service文件夹
新建UserService.java文件
package com.imooc.oa.service;
import com.imooc.oa.entity.User;
import com.imooc.oa.mapper.UserMapper;
import com.imooc.oa.service.exception.LoginException;
public class UserService {
private UserMapper userMapper = new UserMapper();
/**
* 根据前台输入进行登录校验
* @param username 前台输入的用户名
* @param password 前台输入的密码
* @return 校验通过后,包含对应用户数据的User实体类
* @throws LoginException 用户登录异常
*/
public User checkLogin(String username , String password){
User user = userMapper.selectByUsername(username);
if(user == null){
throw new LoginException("用户名不存在");
}
if(!password.equals(user.getPassword())){
throw new LoginException("密码错误");
}
return user;
}
}
java没有提供登录失败异常,需要我们自己创建 在service下创建子包exception,名为LoginException.java
package com.imooc.oa.service.exception;
public class LoginException extends RuntimeException{
public LoginException(String message){
super(message);
}
}
需要继承RuntimeException
运行时异常然后进行单元用例测试
右键类名,点击code→gnerate→Test
package com.imooc.oa.service;
import com.imooc.oa.entity.User;
import org.junit.Test;
import static org.junit.Assert.*;
public class UserServiceTest {
private UserService userService = new UserService();
@Test
public void checkLogin1() {
User user = userService.checkLogin("m8", "test");
System.out.println(user);
}
@Test
public void checkLogin2() {
User user = userService.checkLogin("test", "test");
System.out.println(user);
}
@Test
public void checkLogin3() {
User user = userService.checkLogin("m8", "test1");
System.out.println(user);
}
}
实现用户登陆Controller层
如何在web应用中对外暴露数据?不再是业务逻辑负责范畴,需要Controller层承上启下,主要实现的组件是Servlet oa包下创建controller控制层包,新建LoginServlet.java
package com.imooc.oa.controller;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.imooc.oa.entity.User;
import com.imooc.oa.service.UserService;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;
@WebServlet("/api/login")
public class LoginServlet extends HttpServlet {
private UserService userService = new UserService();
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=utf-8");
//接收用户输入
String username = request.getParameter("username");
String password = request.getParameter("password");
//调用业务逻辑
Map result = new LinkedHashMap<>();
try {
User user = userService.checkLogin(username, password);
user.setPassword(null);
user.setSalt(null);
//处理结果编码,0代表处理成功,非0代表处理失败
result.put ("code","0");
result.put ("message","success");
}catch (Exception e){
e.printStackTrace();
result.put("code",e.getClass().getSimpleName());
result.put ("message",e.getMessage());
}
//返回JSON结果
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setSerializationInclusion (JsonInclude.Include.NON_NULL);
String json = objectMapper.writeValueAsString(result);
response.getWriter().println(json);
}
}
使用@webServlet()绑定url,为了统一工程接口,使用/api/的方式,application/json
前台向servlet发起请求,返回的是json格式文件。所以没用text的格式
绝大部分控制层要做的三件事:
- 接收用户输入
- 调用业务逻辑
- 返回JSON结果
返回json结果需要导入依赖
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.12.3</version>
</dependency>
tomcat发布时,默认不会将我们导入的依赖发布到webapp下,所以可能会报错误,需要我们在项目结构在工件下选中所有jar包,右键添加到lib目录下 既然服务器能返回错误信息,如何在错误信息中返回登陆用户名 在try语句块中添加data数据项
Map data = new LinkedHashMap();
data.put ("user",user);
result.put ("data",data);
如果删掉password,会报密码错误
同理username报用户名不存在
实现用户登录View层
login.html是view层,主要作用是提交数据,如何和后台进行交互,需要通过axios组件使用ajax通过异步方式进行通信 继续添加
,methods : {
onSubmit(formName){
const form = this.$refs[formName];
form.validate((valid) => {
if(valid){
console.info("表单校验成功,准备提交数据");
const form = this.form;
const $message = this.$message;
const params = new URLSearchParams();
params.append("username", form.username);
params.append("password", form.password);
axios.post("/api/login", params, {}).then(function (response) {
console.info(response);
const json = response.data;
if(json.code=="0"){
sessionStorage.uid=json.data.user.userId;
sessionStorage.eid=json.data.user.employeeId;
window.location.href = "/index.html";
}else{
$message.error({message:json.message, offset: 100});
}
});
}
})
}
}
json.code=="0"
表示处理成功,进入后台首页
const $message = this.$message;
可以帮我们生成好看的错误提示等对话框
message:json.message
是从json获取到的错误数据
如输错密码:
输错用户名:
封装ResponseUtils工具
对一些小细节进行优化,比如(“code”,0)这些标准化数据,可以封装,减少后续重复处理 在utils包下创建ResponseUtils.java类
package com.imooc.oa.utils;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.LinkedHashMap;
import java.util.Map;
public class ResponseUtils {
private String code;
private String message;
private Map data = new LinkedHashMap<>();
public ResponseUtils(){
this.code = "0";
this.message = "success";
}
public ResponseUtils(String code , String message){
this.code = code;
this.message = message;
}
public ResponseUtils put(String key , Object value){
this.data.put(key, value);
return this;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public Map getData() {
return data;
}
public void setData(Map data) {
this.data = data;
}
public String toJsonString(){
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
try {
String json = objectMapper.writeValueAsString(this);
return json;
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
}
其中添加put方法目的完全为了便于我们进行测试用例
首先选中函数名
然后菜单栏生成测试用例:code→gernerate→Test
package com.imooc.oa.utils;
import org.junit.Test;
import static org.junit.Assert.*;
public class ResponseUtilsTest {
@Test
public void put1() {
ResponseUtils resp = new ResponseUtils("LoginException", "密码错误").put("class", "XXXClass").put("name", "imooc");
String json = resp.toJsonString();
System.out.println(json);
}
@Test
public void put2() {
System.out.println(new ResponseUtils("LoginException", "密码错误").put("class", "XXXClass").put("name", "imooc").toJsonString());
}
}
想要设置等多个附加data数据只需继续.put即可
toJsonString()
方法转为json对象
put2()方法是对put1()的简写
然后右键函数名进行运行
这种情况请先点击maven生命周期的clean命令,即可重新运行成功
那么LoginServlet.java如何修改呢
//接收用户输入
String username = request.getParameter("username");
String password = request.getParameter("password");
//调用业务逻辑
ResponseUtils resp = null;
try {
User user = userService.checkLogin(username, password);
user.setPassword(null);
user.setSalt(null);
//处理结果编码,0代表处理成功,非0代表处理失败
resp = new ResponseUtils().put("user", user);
}catch (Exception e){
e.printStackTrace();
resp = new ResponseUtils(e.getClass().getSimpleName(), e.getMessage());
}
//返回JSON结果
response.getWriter().println(resp.toJsonString());
我们只在后台进行了json逻辑调整,没有对这个页面进行任何修改,只关心返回数据和提交地址,服务端做了什么变换对于这个界面没有任何影响,这就是mvc一大优势,使前端实现和后端逻辑进行了解耦
封装Md5Utils加密工具类
密码通过原文保存,为了防止泄露,使用Md5加密
MD5摘要算法
◆MD5信息摘要算法是广泛使用的密码散列函数 ◆MD5可以产生出一个128位的散列值用于唯一标识源数据 ◆项目中通常使用MD5作为敏感数据的加密算法
- 即使拿到加密后的字符串,也不会得到原密码,操作不可逆
- 原密码不修改加密多少次都不会改变md5,修改原密码一两位数字生成的md5没有规律,强绑定
- 无论加密前字符串多长,加密后md5长度稳定
Apache Commons Codec
◆Commons-Codec是Apache提供的编码/解码组件 ◆通过Commons-Codec可轻易生成源数据的MD5摘要 ◆MD5摘要方法:String md5=DigestUtils.md5Hex(源数据) Codec – Home (apache.org) 这是阿帕奇提供的开源md5加密解密项目 pom.xml增加依赖:
<!--Apache 加密/解密组件-->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.15</version>
</dependency>
记住,重新加载后,还要再项目结构→工件,将依赖右键添加到WEB-INF的lib目录下,才能在发布环境使用它
新增工具类Md5Utils.java
package com.imooc.oa.utils;
import org.apache.commons.codec.digest.DigestUtils;
public class Md5Utils {
/**
* 对源数据生成MD5摘要
* @param source 源数据
* @return MD5摘要
*/
public static String md5Digest(String source){
return DigestUtils.md5Hex(source);
}
/**
* 对源数据加盐混淆后生成MD5摘要
* @param source 源数据
* @param salt 盐值
* @return MD5摘要
*/
public static String md5Digest(String source,Integer salt){
char[] chars = source.toCharArray();
for (int i= 0 ; i< chars.length ; i++){
chars[i] = (char) (chars[i] + salt);
}
String target = new String(chars);
//System.out.println(target);
String md5 = DigestUtils.md5Hex(target);
return md5;
}
}
进行测试
package com.imooc.oa.utils;
import org.junit.Test;
import static org.junit.Assert.*;
public class Md5UtilsTest {
@Test
public void md5Digest1() {
String md5 = Md5Utils.md5Digest("123456");
System.out.println(md5);
}
@Test
public void md5Digest2() {
String md5 = Md5Utils.md5Digest("123456",888);
System.out.println(md5);
}
}
仍旧就不安全,不是不可逆吗,为何还能反推?
因为此网站使用穷举法,由于md5特殊性,原密码不修改加密多少次都不会改变md5,所以将常见密码和对应的md5加密保存到数据库,只是做了反向查询而已
所以我们只需要在原密码上动手脚,比如变复杂,搞特殊符号,就不会被反向查询到了,在加密过程称为加盐
/**
* 对源数据加盐混淆后生成MD5摘要
* @param source 源数据
* @param salt 盐值
* @return MD5摘要
*/
public static String md5Digest(String source,Integer salt){
char[] chars = source.toCharArray();
for (int i= 0 ; i< chars.length ; i++){
chars[i] = (char) (chars[i] + salt);
}
String target = new String(chars);
//System.out.println(target);
String md5 = DigestUtils.md5Hex(target);
return md5;
}
完整登陆功能实现
查看我们数据库数据,password部分是通过md5+盐值的方式进行保存
然后在UserService.java下修改
public User checkLogin(String username , String password){
User user = userMapper.selectByUsername(username);
if(user == null){
throw new LoginException("用户名不存在");
}
String md5 = Md5Utils.md5Digest(password, user.getSalt());
if(!md5.equals(user.getPassword())){
throw new LoginException("密码错误");
}
return user;
}
在UserServiceTest.java进行测试,测试成功(m8用户名对应密码是test)
package com.imooc.oa.service;
import com.imooc.oa.entity.User;
import org.junit.Test;
import static org.junit.Assert.*;
public class UserServiceTest {
private final UserService userService = new UserService();
@Test
public void checkLogin1() {
User user = userService.checkLogin("m8", "test");
System.out.println(user);
}
@Test
public void checkLogin2() {
User user = userService.checkLogin("test", "test");
System.out.println(user);
}
@Test
public void checkLogin3() {
User user = userService.checkLogin("m8", "test1");
System.out.println(user);
}
}
如果运行checkLogin3()会报密码错误
调整好UserService后,LoginServlet.java不用调整,包裹前端页面Login.html也不需要调整
重新运行输入用户名m8,密码test,就能成功跳转到index.html首页了 此时还有个问题,当黑客进行抓包时可以获取到链接,导致md5和盐值salt泄露,所以我们需要将其隐藏
在LoginServlet.java文件try语句块添加语句块
user.setPassword(null);
user.setSalt(null);
将favvicon.ico文件直接复制粘贴到webapp目录下,可以直接为网页导航栏添加图片
到此登陆界面算是开发完毕了
绘制后台首页UI布局
在webapp下新建index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>MK网办公OA系统</title>
<!-- 引入样式 -->
<link rel="stylesheet" type="text/css" href="assets/element-plus/index.css">
<!-- 引入组件库 -->
<script src="/assets/vue/vue.global.js"></script>
<script src="/assets/element-plus/index.full.js"></script>
<script src="/assets/axios/axios.js"></script>
<style>
.el-header {
background-color: rgb(238, 241, 246);
color: #333;
line-height: 60px;
}
html,body,#app,.el-container {
padding: 0px;
margin: 0px;
height: 100%;
max-height: 100%;
}
</style>
</head>
<body>
<div id="app">
<el-container style="height:100%;border:1px solid #eee">
<el-header>
<el-row>
<el-col :span="12">
<span style="font-size: 18px;color:darkcyan">MK网办公OA系统</span>
</el-col>
</el-row>
</el-header>
<el-container>
<el-aside width="200px" style="max-height:100%;background-color: rgb(238, 241, 246)">
我是功能区
</el-aside>
<el-main>
<iframe id="main" name="main" src="https://www.bilibili.com/" style="width:100%;height:95%;border: 0px"></iframe>
</el-main>
</el-container>
</el-container>
</div>
<script>
const Main = {
data(){
return {
}
}
};
const app = Vue.createApp(Main);
app.use(ElementPlus);
app.mount("#app");
</script>
</body>
</html>
查看常用布局页面
Container 布局容器 | Element Plus (gitee.io) 注意需要设置下面代码段,才能全屏显示不同区域
html,body,#app,.el-container {
padding: 0px;
margin: 0px;
height: 100%;
max-height: 100%;
}
iframe标签中src可以设置网页,包括自己的URI网页,调用自己书写的,如果设置为height:100%,右侧会出现小滚动条,不美观,所以设置为95%
开发RBACModel层
首先先来开发功能区 首先分析数据库 sys_user(user_id)→sys_role_user(role_id)→sys_role_node(node_id)→sys_node(node_name)
从登陆角色表中(sys_user)获取到user_id,然后根据角色权限功能表中(sys_node)获得功能node_name
使用多表查询功能 新建查询
select DISTINCT n.*
from sys_role_user ru,sys_role_node rn,sys_node n
where
ru.role_id = rn.role_id and rn.node_id = n.node_id
and ru.user_id = 1
ORDER BY n.node_code
看出1号员工对应功能为这四项
改变ru.user_id = 1
中数值,数值从角色登陆表获得,就可以得到不同权限下的功能
我们使用sql传参方式进行选择user_id
在resources资源目录下的mappers新建rbac.xml文件,进行sql语句的配置书写
<?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="rbacmapper">
<select id="selectNodeByUserId" parameterType="Long" resultType="com.imooc.oa.entity.Node">
select DISTINCT n.*
from sys_role_user ru , sys_role_node rn , sys_node n
where
ru.role_id = rn.role_id and rn.node_id = n.node_id
and ru.user_id = #{value}
order by n.node_code
</select>
</mapper>
在com.imooc.oa.entity下创建实体类Node.java对sql执行结果进行保存接收
package com.imooc.oa.entity;
public class Node {
private Long nodeId;
private Integer nodeType;//节点类型 1-模块 2-功能
private String nodeName;//节点名
private String url;//页面URL
private Integer nodeCode;//节点编码
private Long parentId;//上级编号
public Long getNodeId() {
return nodeId;
}
public void setNodeId(Long nodeId) {
this.nodeId = nodeId;
}
public Integer getNodeType() {
return nodeType;
}
public void setNodeType(Integer nodeType) {
this.nodeType = nodeType;
}
public String getNodeName() {
return nodeName;
}
public void setNodeName(String nodeName) {
this.nodeName = nodeName;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public Integer getNodeCode() {
return nodeCode;
}
public void setNodeCode(Integer nodeCode) {
this.nodeCode = nodeCode;
}
public Long getParentId() {
return parentId;
}
public void setParentId(Long parentId) {
this.parentId = parentId;
}
}
写好rbac后不要忘记在mybatis-config文件下进行注册
<mapper resource="mappers/rbac.xml"/>
然后在com.imooc.oa.mapper下新建RbacMapper.java,用于执行sql语句
package com.imooc.oa.mapper;
import com.imooc.oa.entity.Node;
import com.imooc.oa.utils.MybatisUtils;
import java.util.List;
public class RbacMapper {
public List<Node> selectNodeByUserId(Long userId){
List list = (List)MybatisUtils.executeQuery(sqlSession -> sqlSession.selectList("rbacmapper.selectNodeByUserId", userId));
return list;
}
}
向上推演,在com.imooc.service下新建RbacService.java用于处理用户权限
package com.imooc.oa.service;
import com.imooc.oa.entity.Node;
import com.imooc.oa.mapper.RbacMapper;
import java.util.List;
public class RbacService {
private RbacMapper rbacMapper = new RbacMapper();
public List<Node> selectNodeByUserId(Long userId){
return rbacMapper.selectNodeByUserId(userId);
}
}
然后我们进行测试
package com.imooc.oa.service;
import com.imooc.oa.entity.Node;
import org.junit.Test;
import java.util.List;
import static org.junit.Assert.*;
public class RbacServiceTest {
private RbacService rbacService = new RbacService();
@Test
public void selectNodeByUserId() {
List<Node> nodes = rbacService.selectNodeByUserId(3l);
for(Node n:nodes){
System.out.println(n.getNodeName());
}
}
}
注意3后面加l表示长整型,我们parameterType="Long"
为长整型
行政审批下对应这两个功能
开发RBAC Contronller层
我们模块下分有很多功能,上面行政审批下对应两个功能,我们要像右边一样有层次展现
node_type表示等级,1表示模块名,2表示功能名
在contronller下新建UserInfoServlet.java,用来获取与用户相关信息,其中包括用户所拥有相关功能
package com.imooc.oa.controller;
import com.imooc.oa.entity.Node;
import com.imooc.oa.service.RbacService;
import com.imooc.oa.utils.ResponseUtils;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@WebServlet("/api/user_info")
public class UserInfoServlet extends HttpServlet {
private RbacService rbacService = new RbacService();
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String uid = request.getParameter("uid");
List<Node> nodes = rbacService.selectNodeByUserId(Long.parseLong(uid));
List<Map> treeList = new ArrayList<>();
Map module = null;
for(Node node : nodes){
if(node.getNodeType() == 1){
module = new LinkedHashMap();
module.put("node", node);
//多个功能是个集合
module.put("children", new ArrayList());
treeList.add(module);
}else if(node.getNodeType() == 2){
List children = (List)module.get("children");
children.add(node);
}
}
String json = new ResponseUtils()
.put("nodeList", treeList).toJsonString();
response.setContentType("application/json;charset=utf-8");
response.getWriter().println(json);
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}
}
重启tomcat,访问localhost/api/user_info?uid=3
开发RBAC View层
我们请求的uid如何与前台进行交互? 在login.html中,在登陆前将关键信息保存,比如用户编号,才能进行后续处理查看系统功能有哪些 login.html中
if(json.code=="0"){
sessionStorage.uid=json.data.user.userId;
sessionStorage.eid=json.data.user.employeeId;
window.location.href = "/index.html";
}else{...}
重新运行,会话成功存储,如果关闭页面窗口,就会销毁,会话存储与页面窗口绑定,而本地存储不会,我们根据需求进行选择存储,现在登陆界面只需要短期存储
修改index里的script标签块
<script>
const Main = {
data(){
return {
nodeList:[],
employee:{}
}
}
,methods:{
showPage(url){
document.getElementById("main").src = url;
}
,logout(){
sessionStorage.clear();
window.location.href = "/login.html";
}
}
,mounted(){
const objApp = this;
const uid = sessionStorage.uid;
axios.get("/api/user_info?uid=" + uid)
.then(function(response){
const json = response.data;
json.data.nodeList.forEach(function (item){
objApp.nodeList.push(item);
})
console.info(objApp.nodeList);
})
}
};
const app = Vue.createApp(Main);
app.use(ElementPlus);
app.mount("#app");
</script>
通过ajax向nodeList:[]异步发送数据 访问http://localhost/index.html
可以看出成功打印出json信息,那该如何显示在功能区呢?
element plus早已为我们准备好了 Menu 菜单 | Element Plus (gitee.io)
在index.html文件下的e-aside标签对里添加代码
<!--默认展开第一个模块功能-->
<el-menu :default-openeds="['0']">
<template v-for="(n,idx) in nodeList">
<el-submenu :index="idx.toString()">
<template #title><i class="el-icon-s-tools"></i>{{n.node.nodeName}}</template>
<template v-for="func in n.children">
<el-menu-item :index="func.nodeId.toString()" v-on:click="showPage(func.url)">{{func.nodeName}}</el-menu-item>
</template>
</el-submenu>
</template>
</el-menu>
为了点击跳转url,使用showpage方法,在method里添加
,methods:{
showPage(url){
document.getElementById("main").src = url;
}
}
回顾Mapper接口开发过程
修改页面header部分
<el-header>
<el-row>
<el-col :span="12">
<span style="font-size: 18px;color:darkcyan">MK网办公OA系统</span>
</el-col>
<el-col :span="12" style="text-align:right">
<el-dropdown>
<i class="el-icon-s-check" style="font-size:18px;margin-right: 15px">
<span style="margin-right: 15px">张三[研发工程师]</span>
</i>
<!--下拉菜单 #dropdown-->
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-on:click="logout">注销</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-col>
</el-row>
</el-header>
用户名和职位我们都是写死的,如何动态进行修改?我们重新回顾下开发过程
sql文件夹添加有employee数据文件
复制下代码到那navicat进行使用,新建查询并运行
老规矩,先创建实体类,在entity下,新建Employee.java
package com.imooc.oa.entity;
public class Employee {
private Long employeeId; //员工编号
private String name; //姓名
private Long departmentId; //部门编号
private String title; //头衔/职务
private Integer level; //岗位级别
@Override
public String toString() {
return "Employee{" +
"employeeId=" + employeeId +
", name='" + name + '\'' +
", departmentId=" + departmentId +
", title='" + title + '\'' +
", level=" + level +
'}';
}
public Long getEmployeeId() {
return employeeId;
}
public void setEmployeeId(Long employeeId) {
this.employeeId = employeeId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Long getDepartmentId() {
return departmentId;
}
public void setDepartmentId(Long departmentId) {
this.departmentId = departmentId;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public Integer getLevel() {
return level;
}
public void setLevel(Integer level) {
this.level = level;
}
}
接下来在资源目录mappers文件夹下书写eployee.xml
此时命名空间不能随便写了,需要使用接com.imooc.oa.mapper.EmployeeMapper
EmployeeMapper等会写
<?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="com.imooc.oa.mapper.EmployeeMapper">
<select id="selectById" parameterType="Long" resultType="com.imooc.oa.entity.Employee">
select * from adm_employee where employee_id = #{value}
</select>
<select id="selectByParams" parameterType="java.util.Map" resultType="com.imooc.oa.entity.Employee">
select * from adm_employee
where
1=1
<if test="level != null">
and level = #{level}
</if>
<if test="departmentId != null">
and department_id = #{departmentId}
</if>
<if test="title != null">
and title = #{title}
</if>
</select>
</mapper>
在java下创建接口com.imooc.oa.mapper.EmployeeMapper
package com.imooc.oa.mapper;
import com.imooc.oa.entity.Employee;
import java.util.List;
import java.util.Map;
public interface EmployeeMapper {
public Employee selectById(Long employeeId);
public List<Employee> selectByParams(Map params);
}
selectById
名字要和eployee.xml下的id属性对应
不要忘记在mybatis-config.xml注册
<mapper resource="mappers/employee.xml"/>
然后进行测试用例
package com.imooc.oa.mapper;
import com.imooc.oa.entity.Employee;
import com.imooc.oa.utils.MybatisUtils;
import org.junit.Test;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.*;
public class EmployeeMapperTest {
@Test
public void selectById() {
Employee emp = (Employee)MybatisUtils.executeQuery(sqlSession -> {
EmployeeMapper employeeMapper = sqlSession.getMapper(EmployeeMapper.class);
Employee employee = employeeMapper.selectById(4l);
System.out.println(employee);
return employee;
});
}
@Test
public void selectByParams1() {
Map params = new HashMap<>();
params.put("level", 7);
params.put("departmentId", 2);
MybatisUtils.executeQuery(sqlSession -> {
EmployeeMapper employeeMapper = sqlSession.getMapper(EmployeeMapper.class);
List<Employee> employees = employeeMapper.selectByParams(params);
System.out.println(employees);
return employees;
});
}
@Test
public void selectByParams2() {
Map params = new HashMap<>();
params.put("level", 8);
MybatisUtils.executeQuery(sqlSession -> {
EmployeeMapper employeeMapper = sqlSession.getMapper(EmployeeMapper.class);
List<Employee> employees = employeeMapper.selectByParams(params);
System.out.println(employees);
return employees;
});
}
}
上面就是基于Mapper接口进行数据查询
接下来就是将其继续开发,完成header部分的动态处理
实现Header显示和注销功能
在service下创建EployeeService.java
package com.imooc.oa.service;
import com.imooc.oa.entity.Employee;
import com.imooc.oa.mapper.EmployeeMapper;
import com.imooc.oa.utils.MybatisUtils;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class EmployeeService {
public Employee selectById(Long employeeId){
Employee employee = (Employee)MybatisUtils.executeQuery(sqlSession -> {
EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);
return mapper.selectById(employeeId);
});
return employee;
}
public Employee selectLeader(Long employeeId){
Employee l = (Employee)MybatisUtils.executeQuery(sqlSession -> {
EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);
Employee employee = mapper.selectById(employeeId);
Map params = new HashMap<>();
Employee leader = null;
if(employee.getLevel() < 7 ){
//查询部门经理
params.put("level", 7);
params.put("departmentId", employee.getDepartmentId());
List<Employee> employees = mapper.selectByParams(params);
leader = employees.get(0);
}else if(employee.getLevel() == 7 ){
//查询总经理
params.put("level", 8);
List<Employee> employees = mapper.selectByParams(params);
leader = employees.get(0);
}else if(employee.getLevel() == 8){
//返回自己
leader = employee;
}
return leader;
});
return l;
}
}
写好service后,根据MVC的反顺序,向上推进找到controller层
记住,我们编程逻辑顺序从先获取数据库的底层反着来,根据MVC的四个步骤进行
在UserInfoServlet.java中,用于获取与用户相关信息,其中包含员工数据 先实例化employee
private EmployeeService employeeService = new EmployeeService();
再按员工编号获取数据,doGet方法里添加获取员工编号
String eid = request.getParameter("eid");
然后带到selectById()方法中
Employee employee = employeeService.selectById(Long.parseLong(eid));
得到了员工数据,通过ResponseUtils返回给客户端,只需额外增加put方法即可
String json = new ResponseUtils()
.put("nodeList", treeList).put("employee",employee).toJsonString();
这样当前登陆员工获取到了 验证一下localhost/api/user_info?uid=1&eid=1
数据获取成功,如何和界面进行绑定?
打开login.html登录页
if(json.code=="0"){
sessionStorage.uid=json.data.user.userId;
sessionStorage.eid=json.data.user.employeeId;
window.location.href = "/index.html";
}
这里存放了eid,我们在index.html进行提取 在.mount(){}进行事件绑定
,mounted(){
const objApp = this;
const eid = sessionStorage.eid;
const uid = sessionStorage.uid;
axios.get("/api/user_info?uid=" + uid + "&eid=" + eid)
.then(function(response){
const json = response.data;
json.data.nodeList.forEach(function (item){
objApp.nodeList.push(item);
})
console.info(objApp.nodeList);
objApp.employee = json.data.employee;
})
}
在哪用?要和当前数据进行绑定,在data(){}数据块中
data(){
return {
nodeList:[],
employee:{}
}
}
这样employee:{}里就有数据了 后面进行双向绑定 将张三[研发工程师]进行修改,动态实现绑定
<span style="margin-right: 15px">张三[研发工程师]</span>
修改为
<span style="margin-right: 15px">{{employee.name}}[{{employee.title}}]</span>
更新tomcat的类和资源
然后登陆账号m8,密码test,进入成功
查看数据库
通过employ_id对应上m8
接下来设置注销逻辑,先绑定注销事件
<el-dropdown-item v-on:click="logout">注销</el-dropdown-item>
处理注销方法
,methods:{
showPage(url){
document.getElementById("main").src = url;
}
,logout(){
sessionStorage.clear();
window.location.href = "/login.html";
}
}
sessionStorage.clear();
表示清空登陆数据,然后跳回登陆页面