美文网首页
rbac讲义

rbac讲义

作者: 建国同学 | 来源:发表于2020-05-11 10:16 被阅读0次

    Bootstrap前端框架

    1.概述

    Bootstrap 是美国 Twitter 公司的设计师 Mark Otto 和 Jacob Thornton 合作基于 HTML、CSS、JavaScript 开发的简洁、直观、强悍的前端开发框架,使得 Web 开发更加快捷。 Bootstrap 提供了优雅的 HTML 和 CSS 规范,它即是由动态 CSS 语言 Less 写成。(重点是响应式,能适应各种各种设备)

    学习方法

    参考文档,拷贝案例,修改调整

    2.HelloWorld

    1、项目中添加 Bootstrap文件目录

    2、html页面引入相关的样式和 JS

    <link rel="stylesheet" href="/js/bootstrap/css/bootstrap.css">
    <script src="/js/jquery/jquery.min.js"></script>
    <script src="/js/bootstrap/js/bootstrap.js"></script>
    

    3、添加文档案例查看效果

    3.常用组件

    面板,栅格系统,列表,表格,按钮,表单,字体图标,模态框

    RBAC权限管理系统

    1.课程目标

    了解权限管理在系统中的作用

    熟练使用SSM项目开发环境

    熟练使用jQuery完成前台页面的基本功能

    完成RBAC权限管理系统的开发

    通过该系统熟悉WEB应用的基本开发流程

    2.权限管理系统

    权限管理,一般指根据系统设置的安全规则或者安全策略,限制用户可以访问而且只能访问自己被授权的资源,从而保障数据资源在合法范围内得以有效使用和管理。权限管理几乎出现在任何系统里面,只要有用户和密码的系统。

    在权限管理中使用最多的还是基于角色访问控制(RBAC: Role Based Access Control)

    ![role.png](https://img.haomeiwen.com/i11635091/e9915db50ff5a889.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

    3.RBAC概述

    基于角色的访问控制,这种模型的基本概念是把权限(Permission)与角色(Role)联系在一起,用户通过充当合适角色的成员而获得该角色的权限。

    在实际的组织中,为了完成组织的业务工作,需要在组织内部设置不同的职位,职位既表示一种业务分工,又表示一种责任与权利。根据业务分工的需要,职位被划分给不同群体,各个群体的人根据其工作任务的需要被赋予不同的职责和权利,每个人有权了解与使用与自己任务相关的信息与资源,对于那些不应该被知道的信息则应该限制他们访问。这就产生了访问控制的需求。

    例如,在一间公司中,有老板、经理、行政、人事、财务等不同的职位,在通常情况下,职位所赋予的权利是不变的,但在某个职位上工作的人可以根据需要调整。RBAC 模型对组织内部的这些关系与访问控制要求给出了非常恰当的描述。

    RBAC重要对象

    • 用户(User):角色施加的主体;用户通过拥有某个或多个角色以得到对应的权限。

    • 角色(Role):表示一组权限的集合。

    • 权限(Permission):用于限定能够访问的一个资源。

    4.需求分析

    我们现在的RBAC权限管理系统中包括部门管理,员工管理,权限管理,角色管理 4个模块。

    管理流程通常是使用下面的方式完成

    1、在角色管理界面,为角色分配权限

    2、在员工管理界面,为员工分配角色

    在员工通过登录认证后,发起对系统资源的访问时,检查当前员工是否拥有访问对应资源的权限,如果没有,阻止访问,给出提示"您没有权限进行该操作!",反之,放行即可。

    每个模块基本都是一些简单的CRUD的需求,所以难度并不大,相对而言,下面几个点可能存在一定的难度:员工与角色的关系管理 , 权限加载 , 角色和权限的关系管理 , 登录会话 , 权限检查

    这几个点相对来说存在一些难度,大家一定要细心去分析清楚其中的思路流程。

    5.环境搭建

    1、 数据库表结构理解, 部门表,员工表,角色表,权限表

    2、 创建Maven项目, 项目添加依赖, 以及相关配置文件

    3、 重命名包,删除一些之前引入的样式文件与JS 文件

    6.部门管理

    1、在数据库新建 department 表

    2、美化部门页面

    拷贝资料中提供的 jsp 和静态资源到指定目录

    webapp
    --css
    --js
    --WEB-INF
        --views
    

    3、实现分页功能

    分析前端twbs-pagination分页插件的使用

    1.引入插件

    <script src="/js/jquery/jquery.min.js"></script>
    <script src="/js/bootstrap/js/bootstrap.js"></script>
    <script src="/js/plugins/twbsPagination/jquery.twbsPagination.min.js"></script>
    

    2.页面中添加ul元素

    <div style="text-align: center;">
        <ul id="pagination" class="pagination"></ul>
    </div>
    

    3.在表单中添加隐藏的input当前页

    <input type="hidden" name="currentPage" id="currentPage" value="1">               
    

    4.编写分页js代码

     $("#pagination").twbsPagination({
                totalPages: ${result.totalPage},
                startPage: ${result.currentPage},
                visiblePages:5,
                first:"首页",
                prev:"上页",
                next:"下页",
                last:"尾页",
                onPageClick:function(event,page){ //点击页码的时候会执行的方法
                     $("#currentPage").val(page);
                     $("#searchForm").submit();
                }
     });
    

    7.员工管理

    1.参考部门代码实现员工分页查询

    2.关键字以及部门下拉框高级查询

    3.新增员工功能

    4.编辑员工功能

    隐藏密码输入框

    <c:if test="${empty employee}">    </c:if>
    

    部门回显

    <div class="form-group">
        <label for="dept" class="col-sm-2 control-label">部门:</label>
        <div class="col-sm-6">
            <select class="form-control" id="dept" name="dept.id">
                <c:forEach items="${depts}" var="d">
                    <option value="${d.id}">${d.name}</option>
                </c:forEach>
            </select>
            <script>
                $("#dept").val(${employee.dept.id})
            </script>
        </div>
    </div>
    
    

    8.角色管理

    1.创建角色表, 员工角色关系管理中间表

    2.实现角色页面的crud功能

    3.员工页面选择角色功能

    角色多选框列表显示

    model.addAttribute("roles",roleService.listAll());
    
    <select multiple class="form-control allRoles" size="15">
        <c:forEach items="${roles}" var="r" >
            <option value="${r.id}">${r.name}</option>
        </c:forEach>
    </select>
    

    角色左移右移效果

    function moveSelected(src, target) {
          $("."+target).append($("."+src+" option:selected"));
    }
    
    function moveAll(src, target) {
          $("."+target).append($("."+src+" option"));
    }
    

    选择超管后隐藏角色区域

    var roleDiv;
    $("#admin").click(function () {
        //判断是否是勾选状态
        var checked = $(this).prop('checked');
        if(checked){
            //删除角色的div
            roleDiv = $("#role").detach(); 
        }else{
            //恢复角色div,加到超管的后面
            $("#adminDiv").after(roleDiv);
        }
    })
    

    编辑时若员工是超管也需要隐藏角色区域

    var checked = $("#admin").prop('checked');
    if(checked){
        //删除角色的div
        roleDiv = $("#role").detach(); 
    }
    

    4.保存员工时处理角色

    注意:下拉框中选中的数据的才会提交,若没有选中的数据是不会提交的。

    解决: 修改按钮为普通按钮,绑定点击事件处理函数,在函数中把右边的select 元素中的 option 设置为选中的,再提交表单。

     $("#submitBtn").click(function () {
         //把右边的下拉框的option全部选择
         $(".selfRoles option").prop("selected",true);
         //提交表单
         $("#editForm").submit();
     })
    

    新增时后台需要处理中间表

    if(ids!=null&&ids.length>0){
        for (Long roleId : ids) {
            employeeMapper.insertRelation(employee.getId(),roleId);
        }
    }
    

    编辑时先删除关系再处理中间表

    employeeMapper.deleteRelation(employee.getId());
    

    sql处理

    <delete id="deleteRelation">
         delete from employee_role where employee_id = #{eid}
    </delete>
      
    <insert id="insertRelation">
         insert into employee_role (employee_id,role_id) VALUES (#{eid},#{rid})
    </insert>
    

    5.角色回显

    员工类中添加roles集合

    private List<Role> roles = new ArrayList<>();
    

    使用额外sql查询员工的角色信息

    <collection property="roles" select="cn.wolfcode.rbac.mapper.RoleMapper.selectByEmpId" column="id" ></collection>
    
     <select id="selectByEmpId" resultMap="BaseResultMap">
        select r.id,r.name,r.sn
        from role r JOIN employee_role er
        ON r.id = er.role_id
        where er.employee_id = #{eid}
     </select>
    

    页面循环获取

    <select multiple class="form-control selfRoles"  name="ids">
        <c:forEach items="${employee.roles}" var="r" >
        <option value="${r.id}">${r.name}</option>
        </c:forEach>
    </select>
    

    6.角色去重

    注意:角色回显的时候,发现左右两边的角色有重复,应该是有右边有的角色,不应该在左边出现。

    解决:页面加载完,拿左边两边的option 对比,遍历左边每个角色,若已经在右边列表内,则需要删除。

    //1.把已有的角色id放入一个数组中(右边)
    var ids = [];
    $(".selfRoles option").each(function (i, ele) {
        ids.push( $(ele).val());
    })
    //2.遍历所有的角色(左边)
    $(".allRoles option").each(function (i, ele) {
        //3.判断是否存在ids数组中,如果是就删除掉自己
        var id = $(ele).val();
        if($.inArray(id,ids)!=-1){
            $(ele).remove();
        }
    })
    

    7.员工删除

    删除员工数据,还要从中间表employee_role 中删除与此员工相关的数据。

    employeeMapper.deleteRelation(employee.getId());
    

    9.权限管理

    权限表要包含什么字段?

    权限名称是给分配权限的人看的,用中文,见名知意。

    权限表达式要唯一这样才能区分用户访问的到底是什么资源。要做访问的资源的权限限制,其实就是对系统中的动态资源或者说控制器中的处理方法做限制,因为处理方法包含对数据库的CRUD 操作。换句话说,控制器中的处理方法就是一个一个权限,即数据库中权限表的数据来源于所有控制器的一个一个处理方法。权限表达式的值需要具有唯一性, 那么我们就约定权限表达式组成:控制器类名首字母小写除去Controller:方法名(department:list),这样就可以唯一了。

    控制器中每个处理方法怎么转化成权限表中的数据?

    一条条手动添加太麻烦,需要用代码来批量添加

    我们可以自定义一个注解 , 在处理方法上贴该注解,注解的值使用中文名称即可,表明是什么样的权限 , 再利用贴了注解的方法 , 生成唯一的权限表达式。

    贴注解了除了上面的好处,还可以区分一个处理方法是否要做权限限制,贴了代表要限制,反之不要。

    实现权限管理页面

    1.权限管理页面的列表显示

    2.权限加载实现步骤

    为权限页面的加载权限按钮绑定点击事件 , 点击按钮后发送ajax到后台

    $(".btn_reload").click(function () {
        $.get('/permission/reload.do',function (data) {
            if(data.success){
                 window.location.reload(); //重新加载当前页面
            }else{
                  alert(data.msg)
            }
        })
    })
    

    编写controller处理方法

    @RequestMapping("/reload")
    @ResponseBody
    public JsonResult reload() {
        try {
            permissionService.reload();
            return new JsonResult();
        } catch (Exception e) {
            e.printStackTrace();
            return new JsonResult(false,"加载失败");
        }
    }
    

    自定义权限注解

    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface RequiredPermission {
        String value();
        String expression();
    }
    

    在控制器方法上贴权限注解

    @RequiredPermission(value="部门列表",expression="department:list")
    @RequestMapping("/list")
    public String list(Model model, @ModelAttribute("qo") QueryObject qo){
        PageResult<Department> pageResult = departmentService.query(qo);
        model.addAttribute("pageResult", pageResult);
        return "department/list"; 
    }
    

    实现加载权限的逻辑

    实现步骤: 首先获取容器对象,再从容器获取所有贴有 @Controller 注解的 bean,再获取里面贴有自己自定义的注解的方法,获取权限名称和权限表达式(可以手动拼接,亦可使用注解定义获取),判断该方法是否贴有我们自定义注解,还要判断这个方法拼接权限表达式是否存放在数据库中,若贴有注解且该权限表达式不存在数据库, 则创建 Permission对象,封装数据并存入数据库中。

    @Autowired
    private ApplicationContext ctx;
    
    public void reload() {
        //获取所有的权限表达式
        List<String> permissions = permissionMapper.selectAllExpression();
        //把所有的controller中贴了权限注解的方法,转换成权限对象,并保存到数据库中
        //利用spring上下文,获取带有controller注解的所有bean
        Map<String, Object> map = ctx.getBeansWithAnnotation(Controller.class);
        Collection<Object> values = map.values();
        //遍历每个controller,获取字节码对象,再获取该字节码中所有的方法
        for (Object controller : values) {
            Class<?> clazz = controller.getClass(); //字节码对象
            Method[] methods = clazz.getDeclaredMethods(); //该字节码中所有的方法
            //遍历每个方法
            for (Method method : methods) {
                //判断方法上是否贴有权限注解
                RequiredPermission annotation = method.getAnnotation(RequiredPermission.class);
                //判断注解是否为空
                if(annotation!=null){
                    //获取注解中的权限名称name
                    String name = annotation.value();
                    //获取注解中的权限表达式
                    String expression = annotation.expression();
                    //或者通过反射获取权限表达式
                    //String simpleName = clazz.getSimpleName().replace("Controller","");
                    //uncapitalize方法可以把首字母变为小写
                   // String expression = StringUtils.uncapitalize(simpleName)+":"+method.getName();
                    //如果没有存在数据库,就插入进入
                    if(!permissions.contains(expression)) {
                        //封装成权限对象
                        Permission permission = new Permission();
                        permission.setName(name);
                        permission.setExpression(expression);
                        //把权限对象保存到数据库
                        permissionMapper.insert(permission);
                    }
                }
            }
        }
    }
    

    5.新增/编辑/删除角色时处理权限数据

    参考员工管理角色时的实现步骤

    10.用户登录

    1.使用ajax方式的提交登录

    用户体验更好,保留用户刚输入的用户名和密码,失败之后不需要跳转页面可马上提示错误信息。

     $(".submitBtn").click(function () { 
         //serialize方法可以把表单的所有参数都获取出来,使用&拼接
         $.post('/login.do',$("#loginForm").serialize(),function (data) {
             if(data.success){
                 window.location.href = "/employee/list.do";
             }else{
                 alert(data.msg)
             }
         })
    })
    

    后台处理登录逻辑

    @RequestMapping("/login")
    @ResponseBody
    public JsonResult login(String username, String password) {
        JsonResult json = new JsonResult();
        try {
            employeeService.login(username, password);
            return new JsonResult();
        } catch (Exception e) {
            e.printStackTrace();
            return new JsonResult(false,"用户名或密码错误");
        }
    }
    

    业务层

    public void login(String username, String password) {
        //查询数据库中是否有能匹配上的数据
        Employee employee = employeeMapper.selectByUsernameAndPassword(username, password);
        if (employee == null) { //登录失败
            //通知调用者这里失败了
            throw new RuntimeException("账号和密码不匹配");
        }
        //登录成功,把当前登录成功的对象存入session中,供后期登录校验;
        session.setAttribute("EMPLOYEE_IN_SESSION", employee);
    }
    

    登录后页面可显示用户昵称

    <span class="hidden-xs">${EMPLOYEE_IN_SESSION.name}</span>
    

    登录拦截器

    public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler)
                throws Exception {
        //获取当前登录的用户
        Employee employee = session.getAttribute("EMPLOYEE_IN_SESSION");
        if (employee == null) { //没有登录,不放行
            resp.sendRedirect("/login.html"); //回到登录界面
            return false;
        }
        return true; //已经登录,放行
    }
    

    把拦截器配置到mvc.xml文件中

    <!-- 注册拦截器 -->
    <mvc:interceptors>
        <!-- 配置登录拦截器 -->
        <mvc:interceptor>
            <!-- 对哪些资源起拦截作用 -->
            <mvc:mapping path="/**"/>
            <!-- 对哪些资源不起拦截作用 -->
            <mvc:exclude-mapping path="/login.do"/>
            <!-- 哪个Bean是拦截器 -->
            <bean class="cn.wolfcode.rbac.web.interceptor.CheckLoginInterceptor"/>
        </mvc:interceptor>
    </mvc:interceptors>
    

    11.权限控制

    权限拦截流程

    1.添加权限拦截器

    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //如果登录用户是管理员,直接放行
        Employee employee = session.getAttribute("EMPLOYEE_IN_SESSION");
        if(employee.isAdmin()){
            return true;
        }
        //如果不是管理员,获取当前要执行的控制器的处理方法
        HandlerMethod handlerMethod = (HandlerMethod)handler;
        //判断method中是否贴了权限注解@RequiredPermission
        RequiredPermission annotation = handlerMethod.getMethodAnnotation(RequiredPermission.class);
        if(annotation==null){
            //如果没有贴,直接放行
            return true;
        }
        //通过权限注解获取权限表达式
        String expression = annotation.expression();
        //通过反射来获取权限表达式
        ///String simpleName = handlerMethod.getBean().getClass().getSimpleName().replace("Controller","");
       // String expression = StringUtils.uncapitalize(simpleName)+":"+handlerMethod.getMethod().getName();
        //获取当前登录用户拥有的权限List<String>
        List<String> permissions = permissionService.selectByEmpId(employee.getId());
        if(permissions.contains(expression)){
            return true;//放行
        }
        //跳转到没权限的提示页面
        request.getRequestDispatcher("/WEB-INF/views/common/nopermission.jsp").forward(request,response);
        return false; //不放行
    }
    

    2.提供查询权限的方法

    <select id="selectByEmpId" resultType="String">
        select distinct p.expression from permission p
          join role_permission rp on p.id = rp.permission_id
          join employee_role er on rp.role_id = er.role_id
        where er.employee_id = #{id}
    </select>
    

    3.把拦截器配置到mvc.xml文件中

    <!-- 注册拦截器 -->
    <mvc:interceptors>
        <!-- 配置登录拦截器 -->
        <mvc:interceptor>
            <!-- 对哪些资源起拦截作用 -->
            <mvc:mapping path="/**"/>
            <!-- 对哪些资源不起拦截作用 -->
            <mvc:exclude-mapping path="/login.do"/>
            <!-- 哪个Bean是拦截器 -->
            <bean class="cn.wolfcode.rbac.web.interceptor.CheckPermissionInterceptor"/>
        </mvc:interceptor>
    </mvc:interceptors>
    

    4.每次判断权限都要查询数据库 , 效率太低

    权限数据不会经常变动 , 不用每次去数据库查询权限数据, 可以在登录时就把用户的权限存入session中

    if(!employee.isAdmin()){ //如果不是管理员,就需要查询权限信息
        List<String> permissions = permissionService.selectByEmpId(employee.getId());
        session.setAttribute("EXPRESSION_IN_SESSION", permissions);
    }
    

    5.抽取UserContext工具 , 方便获取数据

    可以在代码任意的地方获取请求对象,或者 HttpSessison 对象,也可以避免操作 session 时,key 的名称过长易导致写错。

    public abstract class UserContext {
        
        public static final String EMPLOYEE_IN_SESSION = "EMPLOYEE_IN_SESSION";
        public static final String EXPRESSION_IN_SESSION = "EXPRESSION_IN_SESSION";
        
        //往session存入登录用户
        public static void setCurrentUser(Employee emp) {
            getSession().setAttribute(EMPLOYEE_IN_SESSION, emp);
        }
        //从session获取登录用户
        public static Employee getCurrentUser() {
             return (Employee) getSession().getAttribute(EMPLOYEE_IN_SESSION);
        }
        //获取session对象
        public static HttpSession getSession() {
            ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            return attrs.getRequest().getSession();
        }
        //往session存入登录用户的权限信息
        public static void setExpression(List<String> permissions) {
            getSession().setAttribute(EXPRESSION_IN_SESSION,permissions);
        }
        //从session获取登录用户的权限信息
        public static List<String> getExpression() {
            return (List<String>) getSession().getAttribute(EXPRESSION_IN_SESSION);
        }
    }
    
    
    ![rbac.png](https://img.haomeiwen.com/i11635091/f4365a0158a740b3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

    相关文章

      网友评论

          本文标题:rbac讲义

          本文链接:https://www.haomeiwen.com/subject/ckbynhtx.html