rbac讲义

2020-05-11  本文已影响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重要对象

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)
上一篇下一篇

猜你喜欢

热点阅读