开发基于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语句配置 在resourcemappers文件夹下添加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运行时异常然后进行单元用例测试

右键类名,点击codegnerateTest

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的格式 绝大部分控制层要做的三件事:

  1. 接收用户输入
  2. 调用业务逻辑
  3. 返回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方法目的完全为了便于我们进行测试用例

首先选中函数名

然后菜单栏生成测试用例:codegernerateTest

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在线加密 (hwcha.com)

  1. 即使拿到加密后的字符串,也不会得到原密码,操作不可逆
  2. 原密码不修改加密多少次都不会改变md5,修改原密码一两位数字生成的md5没有规律,强绑定
  3. 无论加密前字符串多长,加密后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解密加密 (cmd5.com)

仍旧就不安全,不是不可逆吗,为何还能反推? 因为此网站使用穷举法,由于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();表示清空登陆数据,然后跳回登陆页面