RBAC 权限控制模型

  • 如果没有权限控制,系统的功能完全不设防,全部暴露在所有用户面前,用户登录以后可以使用系统中的所有功能,这是实际运行中不能接受的。
  • 所以权限控制系统的目标就是:管理用户行为,保护系统功能。
  • 那么如何进行权限控制呢?
    • 定义资源
    • 创建权限
    • 创建角色
    • 管理用户
    • 建立关联关系

一、如何进行权限控制呢?

  1. 定义资源:资源就是系统中需要保护起来的功能。具体形式很多:URL 地址、handler方法、service 方法、页面元素等等都可以定义为资源使用权限控制系统保护起来。
  2. 创建权限:一个功能复杂的项目会包含很多具体资源,成千上万都有可能。这么多资源逐个进行操作太麻烦了。为了简化操作,可以将相关的几个资源封装到一起,打包成一个“权限”同时分配给有需要的人。
  3. 创建角色:对于一个庞大系统来说,一方面需要保护的资源非常多,另一方面操作系统的人也非常多。把资源打包为权限是对操作的简化,同样把用户划分为不同角色也是对操作的简化。否则直接针对一个个用户进行管理就会很繁琐。所以角色就是用户的分组、分类。先给角色分配权限,然后再把角色分配给用户,用户以这个角色的身份操作系统就享有角色对应的权限了。
  4. 管理用户:系统中的用户其实是人操作系统时用来登录系统的账号、密码。
  5. 建立关联关系:
    • 权限→资源:单向多对多
      • Java 类之间单向:从权限实体类可以获取到资源对象的集合,但是通过资源获取不到权限
      • 数据库表之间多对多:一个权限可以包含多个资源,一个资源可以被分配给多个不同权限
    • 角色→权限:单向多对多
      • Java 类之间单向:从角色实体类可以获取到权限对象的集合,但是通过权限获取不到角色
      • 数据库表之间多对多:一个角色可以包含多个权限,一个权限可以被分配给多个不同角色
    • 用户→角色:双向多对多
      • Java 类之间双向:可以通过用户获取它具备的角色,也可以看一个角色下包含哪些用户
      • 数据库表之间多对多:一个角色可以包含多个用户,一个用户可以身兼数职

二、 多对多关联关系在数据库中的表示

2.1.没有中间表的情况

如果只能在一个外键列上存储关联关系数据,那么现在这情况无法使用 SQL 语句进行关联查询。

2.2. 有中间表

select t_studet.id,t_student.name from t_student left join t_inner on t_studen.id = t_inner.stuent_id left join t_subject on t_inner.subject_id=t_subject.id where t_subjct.id=1

2.3.中间表主键生成的方式

方式一:另外设置字段作为主键

方式二:使用联合主键(组合起来不能重复即可!)

 三、RBAC 权限模型

鉴于权限控制的核心是用户通过角色与权限进行关联,所以前面描述的权限控制系统可以提炼为一个模型:RBAC(Role-Based Access Control ,基于角色的访问控制)。在 RBAC 模型中,一个用户可以对应多个角色,一个角色拥有多个权限,权限具体定义用户可以做哪些事情

3.1 RBAC0~RBAC3:4种权限模型介绍

  1. RBAC0:最基本的RBAC模型,RBAC模型的核心部分,后面三种升级版 RBAC 模型也都是建立在 RBAC0的基础上。
  2. RBAC1:在 RBAC0 的基础上增加了角色之间的继承关系。角色 A 继承角色 B 之后将具备 B 的权限再增加自己独有的其他权限。比如:付费会员角色继承普通会员角色,那么付费会员除了普通会员的权限外还具备浏览付费内容的权限。
  3.  RBAC2:在 RBAC0 的基础上进一步增加了角色责任分离关系。责任分离关系包含静态责任分离和动态责任分离两部分。
    1. 静态责任分离:给用户分配角色时生效
      1. 互斥角色:权限上相互制约的两个或多个角色就是互斥角色。用户只能被分配到一组互斥角色中的一个角色。例如:一个用户不能既有会计师角色又有审计师角色。
      2. 基数约束:
        一个角色对应的访问权限数量应该是受限的;一个角色中用户的数量应该是受限的 ;一个用户拥有的角色数量应该是受限的
      3. 先决条件角色:用户想拥有A角色就必须先拥有B角色,从而保证用户拥有 X 权限的前提是拥有 Y 权限。例如:“金牌会员”角色只能授予拥有“银牌会员”角色的用户,不能直接授予普通用户 
    2. 动态责任分离:用户 登录系统时生效
      • 一个用户身兼数职,在特定场景下激活特定角色:马云在阿里巴巴内部激活创始人角色;马云在某企业级论坛上激活演讲嘉宾角色 
  4. RBAC3:RBAC3 是在 RBAC0 的基础上同时添加 RBAC2 和 RBAC3 的约束,最全面、最复杂。

3.2 模型图解

        3.2.1基本RBAC模型

        3.2.2扩展RBAC模型

四、后台管理系统的角色分配

1.权限控制

2.给admin分配角色role

2.1目标:通过页面操作把 Admin 和 Role 之间的 关联关系保存到数据库。

2.2思路

2.3代码:前往分配页面

2.3.1创建保存 Admin-Role 关联关系的数据库表

CREATE TABLE `project_crowd`.`inner_admin_role` ( `id` INT NOT NULL AUTO_INCREMENT, `admin_id` INT, `role_id` INT, PRIMARY KEY (`id`) );

这个表并不对应现实生活中或项目业务功能中的一个具体实体,所以没有对应的实体类,也不通过逆向工程做逆向生成

2.3.2修改 “ 分配 ”按钮

<a th:href="@{/assign/to/assign/role/page/{adminId}(adminId=${admin.id},pageNum=${pageInfo.pageNum},keyword=${param.keyword})}" class="btn btn-success btn-xs"><i class=" glyphicon glyphicon-check"></i></a>

2.3.3创建 AssignHandler 

@RequestMapping("/assign/to/assign/role/page/{adminId}")
public String toAssignRolePage(
        @PathVariable("adminId") Integer adminId,
        ModelMap modelMap) {
    // 1.查询本身已分配的角色
    List<Role> assignedRoleList = roleService.getAssignedRole(adminId);
    // 2.查询未分配角色
    List<Role> unAssignedRoleList = roleService.getUnAssignedRole(adminId);
    // 3.存入模型(本质上其实是:request.setAttribute("attrName",attrValue);
    modelMap.addAttribute("assignedRoleList", assignedRoleList);
    modelMap.addAttribute("unAssignedRoleList", unAssignedRoleList);
    modelMap.addAttribute("adminId",adminId);
    return "assign-role";
}

2.3.4 RoleServiceImpl中的方法

@Override
public List<Role> getAssignedRole(Integer adminId) {
    return roleMapper.selectAssignedRole(adminId);
}

@Override
public List<Role> getUnAssignedRole(Integer adminId) {
    return roleMapper.selectUnAssignedRole(adminId);
}

2.3.5 SQL 语句 

<select id="selectAssignedRole" resultMap="BaseResultMap">
    select id,name from t_role
    where id in (select role_id from inner_admin_role where admin_id=#{adminId})
</select>
<select id="selectUnAssignedRole" resultMap="BaseResultMap">
    select id,name from t_role
    where id not in (select role_id from inner_admin_role where admin_id=#{adminId})
</select>

2.3.6 在页面上显示角色数据

对 option 标签进行说明:
<option value="将来在提交表单时一起发送给 handler 的值">在浏览器上让用户看到的数据</option>
实际显示角色信息时:
<option value="角色的 id">角色的名称</option>
举例:
<option value="5">市场部经理</option>

assign-role.html关键代码:
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
    <ol class="breadcrumb">
        <li><a href="#">首页</a></li>
        <li><a href="#">数据列表</a></li>
        <li class="active">分配角色</li>
    </ol>
    <div class="panel panel-default">
        <div class="panel-body">
            <form th:action="@{/assign/do/role}" method="post" role="form" class="form-inline">
                <input type="hidden" name="adminId" th:value="${adminId}" />
                <input type="hidden" name="pageNum" th:value="${param.pageNum}" />
                <input type="hidden" name="keyword" th:value="${param.keyword}" />
                <div class="form-group">
                    <label>未分配角色列表</label><br>
                    <select class="form-control" multiple size="10" style="width:150px;overflow-y:auto;">
                        <!--<option value="将来在提交表单时一起发送给 handler 的值">在浏览器上让用户看到的数据</option>-->
                        <!--实际显示角色信息时:<option value="角色的 id">角色的名称</option>-->
                        <option th:each="role:${unAssignedRoleList}" th:value="${role.id}" th:text="${role.name}"></option>
                    </select>
                </div>
                <div class="form-group">
                    <ul>
                        <li class="toRightBtn btn btn-default glyphicon glyphicon-chevron-right"></li>
                        <br>
                        <li class="toLeftBtn btn btn-default glyphicon glyphicon-chevron-left" style="margin-top:20px;"></li>
                    </ul>
                </div>
                <div class="form-group" style="margin-left:40px;">
                    <label>已分配角色列表</label><br>
                    <select name="roleIdList" class="form-control" multiple size="10" style="width:150px;overflow-y:auto;">
                        <option th:each="role:${assignedRoleList}" th:value="${role.id}" th:text="${role.name}"></option>
                    </select>
                </div>
                <button type="submit" style="width: 150px;margin-left: 150px" class="btn btn-lg btn-success btn-block">保存</button>
            </form>
        </div>
    </div>
</div>

2.3.7效果展示

2.3.8 调整表单让表单能够提交数据

 jquery代码:

<script type="text/javascript">
    $(function () {
        $(".toRightBtn").click(function () {
            // select 是标签选择器
            // :eq(0)表示选择页面上的第一个,未分配出去的角色列表
            // :eq(1)表示选择页面上的第二个,已经分配出去的角色列表
            // “>”表示选择子元素
            // :selected 表示选择“被选中的”option
            // appendTo()能够将jQuery对象追加到指定的位置
            $("select:eq(0)>option:selected").appendTo("select:eq(1)");
        });
        $(".toLeftBtn").click(function () {
            // select 是标签选择器
            // :eq(0)表示选择页面上的第一个,未分配出去的角色列表
            // :eq(1)表示选择页面上的第二个,已经分配出去的角色列表
            // “>”表示选择子元素
            // :selected 表示选择“被选中的”option
            // appendTo()能够将jQuery对象追加到指定的位置
            $("select:eq(1)>option:selected").appendTo("select:eq(0)");
        });
    });
</script>

2.4 代码:执行分配

2.4.1handler方法

@RequestMapping("/assign/do/role/assign")
public String saveAdminRoleRelationShip(
        @RequestParam("adminId") Integer adminId,
        @RequestParam("pageNum") Integer pageNum,
        @RequestParam("keyword") String keyword,
        // 我们允许用户在页面上取消所有已分配角色再提交表单,所以可以不提供roleIdList 请求参数
        // 设置 required=false 表示这个请求参数不是必须的
        @RequestParam(value="roleIdList", required=false) List<Integer> roleIdList){
    adminService.saveAdminRoleRelationShip(adminId,roleIdList);
    return "redirect:/admin/getPage?pageNum="+pageNum+"&keyword="+keyword;
}

2.4.2Service 方法

@Override
public void saveAdminRoleRelationShip(Integer adminId, List<Integer> roleIdList) {
    // 旧数据如下:
    // adminId roleId
    // 1        1(要删除)
    // 1        2(要删除)
    // 1        3
    // 1        4
    // 1        5
    // 新数据如下:
    // adminId roleId
    // 1        3(本来就有)
    // 1        4(本来就有)
    // 1        5(本来就有)
    // 1        6(新)
    // 1        7(新)
    // 为了简化操作:先根据 adminId 删除旧的数据,再根据 roleIdList 保存全部新的数据

    // 1.根据 adminId 删除旧的关联关系数据
    adminMapper.deleteOLdRelationship(adminId);

    // 2.根据roleIdList 和 adminId 保存新的关联关系
    if(roleIdList != null && roleIdList.size() > 0){
        adminMapper.insertNewRelationship(adminId, roleIdList);
    }
}

2.4.3 SQL 语句

void deleteOLdRelationship(@Param("adminId") Integer adminId);
<delete id="deleteOLdRelationship">
    delete
    from inner_admin_role
    where admin_id = #{adminId}
</delete>

void insertNewRelationship(@Param("adminId") Integer adminId,@Param("roleIdList") List<Integer> roleIdList);
<insert id="insertNewRelationship">
    insert into inner_admin_role(admin_id,role_id)
    values <foreach collection="roleIdList" item="roleId" separator=",">(#{adminId},#{roleId})</foreach>
</insert>

2.4.4 修正 Bug

 

$("#submitBtn").click(function(){
    // 在提交表单前把“已分配”部分的 option 全部选中
    $("select:eq(1)>option").prop("selected","selected");
    // 为了看到上面代码的效果,暂时不让表单提交
    // return false;
});

3.给role分配权限(auth)

3.1目标:把角色和权限的关联关系保存到数据库

3.2思路:

3.3代码:前往分配权限页面

3.3.1创建权限表:t_auth表,填充假数据

CREATE TABLE `t_auth` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `name` varchar(200) DEFAULT NULL,
    `title` varchar(200) DEFAULT NULL,
    `category_id` int(11) DEFAULT NULL,
    PRIMARY KEY (`id`)
);

INSERT INTO t_auth(id,`name`,title,category_id) VALUES(1,'','用户模块',NULL);
INSERT INTO t_auth(id,`name`,title,category_id) VALUES(2,'user:delete','删除',1);
INSERT INTO t_auth(id,`name`,title,category_id) VALUES(3,'user:get','查询',1);
INSERT INTO t_auth(id,`name`,title,category_id) VALUES(4,'','角色模块',NULL);
INSERT INTO t_auth(id,`name`,title,category_id) VALUES(5,'role:delete','删除',4);
INSERT INTO t_auth(id,`name`,title,category_id) VALUES(6,'role:get','查询',4);
INSERT INTO t_auth(id,`name`,title,category_id) VALUES(7,'role:add','新增',4);


name 字段:给资源分配权限或给角色分配权限时使用的具体值,将来做权限验证也是使用 name 字段的值来进行比对。建议使用英文。
title 字段:在页面上显示,让用户便于查看的值。建议使用中文。
category_id 字段:关联到当前权限所属的分类。这个关联不是到其他表关联,而是就在当前表内部进行关联,关联其他记录。所以说,t_auth 表中是依靠 category_id 字段建立了“节点”之间的父子关系。

name 字段中值的格式:中间的“:”没有任何特殊含义。不论是我们自己写的代码
还是将来使用的框架都不会解析“:”。如果不用“:”,用“%、@、&、*、-”等等这样
的符号也都是可以的。 模块:操作名:user:delete

3.3.2 逆向工程生成资源:Auth、AuthExample、AuthMapper、AuthMapper.xml

3.3.3 创建角色到权限之间关联关系的中间表

CREATE TABLE `project_crowd`.`inner_role_auth` ( `id` INT NOT NULL AUTO_INCREMENT,`role_id` INT, `auth_id` INT, PRIMARY KEY (`id`) );

3.3.4 创建分配权限模态框文件并引入

<div th:include="modal-role-assign-auth"></div>

<div id="roleAssignAuthModal" class="modal fade" tabindex="-1" role="dialog">
    <div class="modal-dialog" role="document">
        <div class="modal-content">
            <div class="modal-header">
                <button type="button" class="close" data-dismiss="modal"
                        aria-label="Close">
                    <span aria-hidden="true">&times;</span>
                </button>
                <h4 class="modal-title"> 尚筹网系统弹窗</h4>
            </div>
            <div class="modal-body">
                <div class="form-group has-success has-feedback">
                    <!--这个ul标签是zTree动态生成的节点所依附的静态节点-->
                    <ul id="authTreeDemo" class="ztree"></ul>
                </div>
            </div>
            <div class="modal-footer">
                <button id="roleAssignAuthBtn" type="button" class="btn btn-primary"> 好的,我设置好了!执行分配!</button>
            </div>
        </div>
    </div>
</div>

3.3.5 给 "☑" 绑定单击响应函数,用以打开分配权限模态框

// 通过button标签的id属性把roleId值传递到按钮的单击响应函数中,在单击响应函数中使用this.id拿到其值。
var checkBtn = "<button id='"+roleId+"' type='button' class='btn btn-success btn-xs checkBtn'><i class='glyphicon glyphicon-check'></i></button>";

给分配权限按钮绑定单击响应函数
$("#rolePageBody").on("click",".checkBtn",function () {
    // 把当前角色的id存入全局变量
    window.roleId = this.id;
    // 打开模态框
    $("#roleAssignAuthModal").modal("show");
    // 在模态框中装载权限的树形结构数据
    fillAuthTree();
});

3.3.6 在role-page.html中加入zTree的环境 

<link rel="stylesheet" th:href="@{/ztree/zTreeStyle.css}"/>
<script type="text/javascript" th:src="@{/ztree/jquery.ztree.all-3.5.min.js}"></script>

3.3.7  在my-role.js中编写函数: fillAuthTree()

// 声明专门的函数用来在分配Auth的模态框中显示Auth的树形结构数据
function fillAuthTree() {
    // 1.发送Ajax请求查询Auth数据
    var ajaxReturn = $.ajax({
        "url":"/assign/get/all/auth",
        "type":"post",
        "dataType":"json",
        "async":false
    });
    console.log(ajaxReturn);
    // 判断当前响应状态码是否为200
    var statusCode = ajaxReturn.status;
    // 如果当前响应状态码不是200,说明发生错误或其他意外情况,显示提示信息,让当前函数停止执行
    if(statusCode != 200){
        layer.msg("请求处理出错!响应状态码="+statusCode+" 说明信息="+ajaxReturn.statusText);
        return;
    }
    // 2.如果当前响应状态码是200,说明请求成功,从响应结果中获取 Auth 的 JSON 数据
    var resultEntity = ajaxReturn.responseJSON;
    // 从resultEntity中获取result属性
    var result = resultEntity.operationResult;
    //判断result是否成功
    if(result != "SUCCESS"){
        layer.msg(resultEntity.operationMessage);
        return;
    }
    // 确认result为成功后获取auth
    var authList = resultEntity.queryData;
    // 3.准备对 zTree 进行设置的 JSON 对象
    // zTree的另一种使用方法:从服务器端查询到的list不需要组装成树形结构,这里我们交给zTree去组装
    var setting = {
        "data":{
            "simpleData":{
                "enable":true
            }
        }
    };
    // 4.生成树形结构
    $.fn.zTree.init($("#authTreeDemo"), setting, authList);
}

3.3.8 后端代码:获取所有的权限信息

@ResponseBody
@RequestMapping("/assign/get/all/auth")
public ResultEntity<List<Auth>> getAllAuth(){
    List<Auth> authList = authService.getAllAuth();
    return ResultEntity.successWithData(authList);
}

@Override
public List<Auth> getAllAuth() {
    return authMapper.selectByExample(new AuthExample());
}

3.3.9 效果展示

 

 3.3.10 效果修正

    var setting = {
        "data": {
            "simpleData": {
                // 开启把查询到的list组装成树形结构的功能
                "enable": true,
                // 使用categoryId属性关联父节点,不用默认的 pId 了
                "pIdKey": "categoryId"
            },
            "key": {
                // 使用 title 属性显示节点名称,不用默认的 name 作为属性名了
                "name": "title"
            }
        }        
    };

3.3.11 把已经分配的权限进行回显(完整的fillAuthTree()代码)

// 声明专门的函数用来在分配Auth的模态框中显示Auth的树形结构数据
function fillAuthTree() {
    // 1.发送Ajax请求查询Auth数据
    var ajaxReturn = $.ajax({
        "url":"/assign/get/all/auth",
        "type":"post",
        "dataType":"json",
        "async":false
    });
    console.log(ajaxReturn);
    // 判断当前响应状态码是否为200
    var statusCode = ajaxReturn.status;
    // 如果当前响应状态码不是200,说明发生错误或其他意外情况,显示提示信息,让当前函数停止执行
    if(statusCode != 200){
        layer.msg("请求处理出错!响应状态码="+statusCode+" 说明信息="+ajaxReturn.statusText);
        return;
    }
    // 2.如果当前响应状态码是200,说明请求成功,从响应结果中获取 Auth 的 JSON 数据
    var resultEntity = ajaxReturn.responseJSON;
    // 从resultEntity中获取result属性
    var result = resultEntity.operationResult;
    //判断result是否成功
    if(result != "SUCCESS"){
        layer.msg(resultEntity.operationMessage);
        return;
    }
    // 确认result为成功后获取auth
    var authList = resultEntity.queryData;
    // 3.准备对 zTree 进行设置的 JSON 对象
    // zTree的另一种使用方法:从服务器端查询到的list不需要组装成树形结构,这里我们交给zTree去组装
    var setting = {
        "data": {
            "simpleData": {
                // 开启把查询到的list组装成树形结构的功能
                "enable": true,
                // 使用categoryId属性关联父节点,不用默认的 pId 了
                "pIdKey": "categoryId"
            },
            "key": {
                // 使用 title 属性显示节点名称,不用默认的 name 作为属性名了
                "name": "title"
            }
        },
        "check": {
            // 设置zTree的节点上是否显示checkBox或radio
            "enable":true
        }
    };
    // 4.生成树形结构
    $.fn.zTree.init($("#authTreeDemo"), setting, authList);
    // 获取zTreeObj对象
    var zTreeObj = $.fn.zTree.getZTreeObj("authTreeDemo");
    // 调用zTreeObj对象的方法,把节点展开
    zTreeObj.expandAll(true);

    // 5.查询已分配的Auth的id组成的List
    ajaxReturn = $.ajax({
        "url":"/assign/get/assigned/auth/by/roleId",
        "type":"post",
        "data":{
            "roleId":window.roleId
        },
        "dataType":"json",
        "async":false
    });
    // 判断当前响应状态码是否为200
    statusCode = ajaxReturn.status;
    // 如果当前响应状态码不是200,说明发生错误或其他意外情况,显示提示信息,让当前函数停止执行
    if(statusCode != 200){
        layer.msg("请求处理出错!响应状态码="+statusCode+" 说明信息="+ajaxReturn.statusText);
        return;
    }
    // 如果当前响应状态码是200,说明请求成功,从响应结果中获取 Auth 的 JSON 数据
    var authIdArray = ajaxReturn.responseJSON.queryData;
    // 6.根据authIdArray把树形结构中对应的节点勾选上
    // ①遍历authIdArray
    for(var i = 0;i < authIdArray.length; i++){
        var authId = authIdArray[i];
        // ②根据authId去查询树形结构中对应的节点
        var treeNode  = zTreeObj.getNodeByParam("id",authId)
        // ③将treeNode设置为被勾选
        // checked设置为true表示节点勾选
        var checked = true;
        // checkTypeFlag设置为false,表示父子节点之间不联动,避免把不该勾选的给勾选上
        var checkTypeFlag = false;
        zTreeObj.checkNode(treeNode,checked,checkTypeFlag)
    }
}

3.3.12 后端代码

@ResponseBody
@RequestMapping("/assign/get/assigned/auth/by/roleId")
public ResultEntity<List<Integer>> getAssignedAuthByRoleId(
        @RequestParam("roleId") Integer roleId){
    List<Integer> authIdList = authService.getAssignedAuthByRoleId(roleId);
    return ResultEntity.successWithData(authIdList);
}

@Override
public List<Integer> getAssignedAuthByRoleId(Integer roleId) {
    return authMapper.selectAssignedAuthByRoleId(roleId);
}

List<Integer> selectAssignedAuthByRoleId(@Param("roleId") Integer roleId);

<select id="selectAssignedAuthByRoleId" resultType="int">
    select auth_id from inner_role_auth where role_id = #{roleId}
</select>

3.4 勾选权限,执行分配 

3.4.1给“执行分配”按钮绑定单击响应函数

// 14.给分配权限模态框中的"执行分配"按钮绑定单击响应函数
$("#roleAssignAuthBtn").click(function () {
    // ①.收集树形结构中被勾选的节点的id
    // 1>声明一个专门的数组存放被勾选id
    var authIdArray = [];
    // 2>获取zTreeObj对象
    var zTreeObj = $.fn.zTree.getZTreeObj("authTreeDemo");
    // 3>获取全部被勾选的节点
    var checkedNodes = zTreeObj.getCheckedNodes(true);
    // 4>遍历checkedNodes得到authIdArray
    for(var i = 0; i < checkedNodes.length; i++){
        var checkedNode = checkedNodes[i];
        var authId = checkedNode.id;
        authIdArray.push(authId);
    }
    // ②.发送请求执行分配
    var requestBody = {
        "authIdArray":authIdArray,
        // 为了服务器端handler方法能够统一使用List<Integer>方式接收数据,roleId也存入数组
        "roleId":[window.roleId]
    };
    requestBody = JSON.stringify(requestBody);
    $.ajax({
        "url":"/assign/do/role/assign/auth",
        "type":"post",
        "data":requestBody,
        "contentType":"application/json;charset=UTF-8",
        "dataType":"json",
        "success":function (response) {
            var result = response.operationResult;
            if(result == "SUCCESS") {
                layer.msg("操作成功!");
            }
            if(result == "FAILED") {
                layer.msg("操作失败!"+response.message);
            }
        },
        "error":function (response) {
            layer.msg(response.status+" "+response.statusText);
        }
    });
    // 关闭模态框
    $("#roleAssignAuthModal").modal("hide");
});

3.4.2后端代码:执行分配权限

@ResponseBody
@RequestMapping("/assign/do/role/assign/auth")
public ResultEntity<String> saveRoleAuthRelationShip(
        @RequestBody Map<String,List<Integer>> map){
    authService.saveRoleAuthRelationShip(map);
    return ResultEntity.successWithoutData();
}

@Override
public void saveRoleAuthRelationShip(Map<String, List<Integer>> map) {
    // 1.获取roleId的值
    List<Integer> roleIdList = map.get("roleId");
    Integer roleId = roleIdList.get(0);
    // 2.删除旧的关联关系
    authMapper.deleteOldRelationShip(roleId);
    // 3.获取authIdList
    List<Integer> authIdList = map.get("authIdArray");
    if(authIdList != null && authIdList.size() > 0){
        // 4.添加新的关联关系
        authMapper.insertNewRelationship(roleId,authIdList);
    }
}

void deleteOldRelationShip(@Param("roleId") Integer roleId);

void insertNewRelationship(@Param("roleId") Integer roleId,@Param("authIdList") List<Integer> authIdList);

<delete id="deleteOldRelationShip">
    delete from inner_role_auth where role_id = #{roleId}
</delete>

<insert id="insertNewRelationship">
    insert into inner_role_auth(auth_id, role_id) values
    <foreach collection="authIdList" item="authId" separator=",">(#{authId},#{roleId})</foreach>
</insert>

4.给menu分配权限(auth)(同role分配权限,略)

五、项目实战

1.众筹项目加入 SpringSecurity 环境

1.1加入依赖

<!-- SpringSecurity 对 Web 应用进行权限管理 -->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-web</artifactId>
    <version>5.4.2</version>
</dependency>
<!-- SpringSecurity 配置 -->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-config</artifactId>
    <version>5.4.2</version>
</dependency>
<!-- SpringSecurity 标签库 -->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-taglibs</artifactId>
    <version>5.4.2</version>
</dependency>

1.2在web.xml中加入Filter

<!--引入 SpringSecurity 的 Filter-->
<filter>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

1.3配置类CrowdfundingSecurityConfig 

//注意:这个类一定要放在自动扫描的包下,否则所有配置都不会生效
//将当前类标记为配置类
@Configuration
//启用 Web安全功能
@EnableWebSecurity
//注解表示启用全局方法权限管理功能。
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class CrowdfundingSecurityConfig extends WebSecurityConfigurerAdapter {
    // 用以指定正确的账号和正确的密码
    @Override
    protected void configure(AuthenticationManagerBuilder builder) throws Exception {

    }

    @Override
    protected void configure(HttpSecurity security) throws Exception {

    }
}

1.4自动扫描的包(谁来把 CrowdfundingSecurityConfig 扫描到 IOC 里?)

考虑到权限控制系统更多的需要控制 Web 请求,而且有些请求没有经过 Service 方法,所以在SpringMVC的IOC容器中扫描CrowdfundingSecurityConfig。但是,SpringSecurity是有管理 Service、Dao 方法的能力的。

1.5 多个 IOC 容器之间的关系
①.问题描述:项目启动时控制台抛异常说找不到“springSecurityFilterChain”的 bean。

 org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'springSecurityFilterChain' available

②.问题分析:
        Web 组件加载顺序:Listener→Filter→Servlet

        Spring IOC 容器:ContextLoaderListener 创建

        SpringSecurityFilterChain:从 IOC 容器中找到对应的 bean

        SpringMVC IOC 容器:DispatcherServlet 创建

DelegatingFilterProxy 查找 IOC 容器然后查找 bean 的工作机制:

ContextLoaderListener 初始化后,springSecurityFilterChain就在 ContextLoaderListener创建的 IOC 容器中查找所需要的 bean,但是我们没有在 ContextLoaderListener 的 IOC 容器中扫描 SpringSecurity 的配置类,所以 springSecurityFilterChain 对应的 bean 找不到。

③.问题解决:把两个 IOC 容器合二为一
将 ContextLoaderListener 取消,原本由 ContextLoaderListener 读取的 Spring 配置文件交给 DispatcherServlet 负责读取。

  • DelegatingFilterProxy 在初始化时查找 IOC 容器,找不到,放弃。
  • 第一次请求时再次查找。
  • 找到 SpringMVC 的 IOC 容器。
  • 从这个 IOC 容器中找到所需要的 bean。
<!--配置ContextLoaderListener加载Spring的配置文件:spring-persist-mybatis.xml和spring-persist-tx.xml-->
<!--<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:spring-persist-*.xml</param-value>
</context-param>-->
<!--<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>-->
<!--注册前端控制器DispatcherServlet-->
<servlet>
    <servlet-name>DispatcherServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:spring-web-mvc.xml,classpath:spring-persist-*.xml</param-value>
    </init-param>
    <!--
        servlet默认生命周期中,创建对象是在第一次接收到请求时,而dispatcherServlet作为框架的核心组件,
        在启动过程中有大量的初始化操作要做,而这些操作放在第一次请求时才执行会严重影响访问速度
        因此需要通过此标签将启动控制DispatcherServlet的初始化时间提前到服务器启动时
    -->
    <load-on-startup>1</load-on-startup>
</servlet>

遗憾的是:会破坏现有程序的结构。原本是 ContextLoaderListener 和 DispatcherServlet
两个组件创建两个 IOC 容器,现在改成只有一个。还有一种解决方案是:改源码。

修改 DelegatingFilterProxy 的源码,修改两处:

①.初始化时直接跳过查找 IOC 容器的环节

@Override
protected void initFilterBean() throws ServletException {
    synchronized (this.delegateMonitor) {
        if (this.delegate == null) {
            // If no target bean name specified, use filter name.
            if (this.targetBeanName == null) {
                this.targetBeanName = getFilterName();
            }
            // Fetch Spring root application context and initialize the delegate early,
            // if possible. If the root application context will be started after this
            // filter proxy, we'll have to resort to lazy initialization.
            
            /*WebApplicationContext wac = findWebApplicationContext();
            if (wac != null) {
                this.delegate = initDelegate(wac);
            }*/
        }
    }
}

②.第一次请求的时候直接找 SpringMVC 的 的 IOC 容器

@Override
public void doFilter(ServletRequest request, ServletResponse response, 
    FilterChain filterChain) throws ServletException, IOException {
    // Lazily initialize the delegate if necessary.
    Filter delegateToUse = this.delegate;
    if (delegateToUse == null) {
        synchronized (this.delegateMonitor) {
            delegateToUse = this.delegate;
            if (delegateToUse == null) {
                // 把原来的查找 IOC 容器的代码注释掉
                // WebApplicationContext wac = findWebApplicationContext();
                // 按我们自己的需要重新编写
                // 1. 获取 ServletContext 对象
                ServletContext sc = this.getServletContext();
                // 2.拼 拼接 接 SpringMVC 将 将 IOC 容器存入 入 ServletContext 域的时候使用的属性名
                String servletName = "springDispatcherServlet";
                String attrName = FrameworkServlet.SERVLET_CONTEXT_PREFIX + servletName;
                // 3. 根据 attrName 从 从 ServletContext 域中获取 IOC 容器对象
                WebApplicationContext wac = (WebApplicationContext)
                sc.getAttribute(attrName);
                if (wac == null) {
                    throw new IllegalStateException("No WebApplicationContext found: " + "no ContextLoaderListener or DispatcherServlet registered?");
                }
                delegateToUse = initDelegate(wac);
            }
            this.delegate = delegateToUse;
        }
    }
    // Let the delegate perform the actual doFilter operation.
    invokeDelegate(delegateToUse, request, response, filterChain);
}

1.6 SpringSecurity 初始设置

@Override
protected void configure(HttpSecurity security) throws Exception {
    security.authorizeRequests()
            .antMatchers("/","/bootstrap/**","/css/**","/fonts/**","/img/**"
        ,"/jquery/**","/layer/**","/script/**","/ztree/**","/crowd/**")
            .permitAll()
            .and()
            .authorizeRequests()
            .anyRequest()
            .authenticated();
}

2.登录

2.1 SpringSecurity 开启表单登录功能并前往登录表单页面 

①.登录表单

<form th:action="@{/security/do/login}" method="post" class="form-signin" role="form">
    <h2 class="form-signin-heading">
        <i class="glyphicon glyphicon-log-in"></i> 管理员登录
    </h2>
    <p th:text="${session.SPRING_SECURITY_LAST_EXCEPTION}"></p>
    <div class="form-group has-success has-feedback">
        <input type="text" name="loginAcct" class="form-control" id="inputSuccess4" placeholder="请输入登录账号" autofocus>
        <span class="glyphicon glyphicon-user form-control-feedback"></span>
    </div>
    <div class="form-group has-success has-feedback">
        <input type="text" name="userPswd" class="form-control" id="inputSuccess4" placeholder="请输入登录密码" style="margin-top:10px;">
        <span class="glyphicon glyphicon-lock form-control-feedback"></span>
    </div>
    <!--<a class="btn btn-lg btn-success btn-block" th:href="@{/main.html}"> 登录</a>-->
    <button type="submit" class="btn btn-lg btn-success btn-block">登录</button>
</form>

②.security设置

@Override
protected void configure(HttpSecurity security) throws Exception {
    security.authorizeRequests()    //对请求进行授权
            .antMatchers("/","/bootstrap/**","/css/**"
                    ,"/fonts/**","/img/**","/jquery/**","/layer/**","/script/**","/ztree/**","/crowd/**") //针对登录页以及静态资源进行设置,可以无条件访问
            .permitAll()            //设置成无条件访问
            .anyRequest()           //表示任意请求
            .authenticated()        //均需要授权才能登陆
            .and()
            .formLogin()                    //开启表单登录的功能
            .loginPage("/admin/to/loginPage")//指定前往登陆页面的地址
            .loginProcessingUrl("/security/do/login")//指定处理登录请求的地址
            .permitAll()
            .usernameParameter("loginAcct") //定制登录账号的请求参数名
            .passwordParameter("userPswd") //定制登录密码的请求参数名
            .defaultSuccessUrl("/admin/to/main/page") //指定登陆成功后前往的地址
            .and()
            .logout()                      //开启退出登录的功能
            .logoutUrl("/security/do/logout") //指定退出登录的地址
            .logoutSuccessUrl("/admin/to/loginPage")//指定退出成功以后前往的地址
            .and()
            .csrf()     //防跨站请求伪造功能
            .disable(); // 禁用 CSRF 功能;
}

③.指定正确的账号和正确的密码(假数据)

@Override
protected void configure(AuthenticationManagerBuilder builder) throws Exception {
    //builder.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
    builder.inMemoryAuthentication()
            .passwordEncoder(passwordEncoder)
            .withUser("tom")
            .password("4297F44B13955235245B2497399D7A93")
            .roles("admin");
}

④. 取消以前的自定义登录拦截器

<!--注册拦截器-->
<!--    <mvc:interceptors>-->
<!--        <mvc:interceptor>-->
<!--            &lt;!&ndash;要拦截的资源,/**是拦截所有&ndash;&gt;-->
<!--            <mvc:mapping path="/**"/>-->
<!--            &lt;!&ndash;不拦截的资源&ndash;&gt;-->
<!--            <mvc:exclude-mapping path="/admin/to/loginPage"/>-->
<!--            <mvc:exclude-mapping path="/admin/do/login"/>-->
<!--            <mvc:exclude-mapping path="/admin/do/logout"/>-->
<!--            <mvc:exclude-mapping path="/"/>-->
<!--            <mvc:exclude-mapping path="/test"/>-->
<!--            <bean class="com.atguigu.crowd.mvc.interceptor.LoginInterceptor"/>-->
<!--        </mvc:interceptor>-->
<!--    </mvc:interceptors>-->

2.2退出登录

.and()
.logout()                      //开启退出登录的功能
.logoutUrl("/security/do/logout") //指定退出登录的地址
.logoutSuccessUrl("/admin/to/loginPage")//指定退出成功以后前往的地址

2.3把内存登录(假数据)改成数据库登录

①.思路

②.代码体现

操作1:根据账号查询 Admin对象

// 1.根据用户名从数据库查询 Admin 对象
// ①创建 AdminExample 对象
AdminExample adminExample = new AdminExample();
// ②创建 Criteria 对象
AdminExample.Criteria criteria = adminExample.createCriteria();
// ③在 Criteria 对象中封装查询条件
criteria.andLoginAcctEqualTo(username);
// ④调用 AdminMapper 的方法执行查询
List<Admin> adminList = adminMapper.selectByExample(adminExample);
// ⑤得到Admin对象
if(adminList == null || adminList.size() != 1) {
    return null;
}
Admin admin = adminList.get(0);

操作2:根据 adminId 查询已分配的角色

Integer adminId = admin.getId();
List<Role> roleList = roleMapper.selectAssignedRole(adminId);

操作3:根据 adminId 查询已分配权限

List<String> authNameList = authMapper.selectAssignedAuthNameByAdminId(adminId);

//查询权限的方法:
List<String> selectAssignedAuthNameByAdminId(Integer adminId);

//查询adminId对应的权限的SQL语句:
<select id="selectAssignedAuthNameByAdminId" resultType="string">
    SELECT DISTINCT t_auth.name
    FROM t_auth
    LEFT JOIN inner_role_auth ON t_auth.id=inner_role_auth.auth_id
    LEFT JOIN inner_admin_role ON inner_admin_role.role_id=inner_role_auth.role_id
    WHERE inner_admin_role.admin_id = #{adminId} and t_auth.name != "" and t_auth.name is not null
</select>

操作4 :创建 SecurityAdmin 类

// 考虑到 User 对象中仅仅包含账号和密码,为了能够获取到原始的 Admin 对象,专门创建这个类对 User 类进行扩展
public class SecurityAdmin extends User {
    private static final long serialVersionUID = 1L;
    // 原始的 Admin 对象,包含 Admin 对象的全部属性
    private Admin originalAdmin;
    public SecurityAdmin(
        // 传入原始的 Admin 对象
        Admin originalAdmin,
        // 创建角色、权限信息的集合
        List<GrantedAuthority> authorities) {

        // 调用父类构造器
        super(originalAdmin.getLoginAcct(), originalAdmin.getUserPswd(), authorities);
        // 给本类的 this.originalAdmin 赋值
        this.originalAdmin = originalAdmin;
    }
    // 对外提供的获取原始 Admin 对象的 getXxx()方法
    public Admin getOriginalAdmin() {
        return originalAdmin;
    }
}

操作5:MyUserDetailsService的完整代码

@Component
public class MyUserDetailsService implements UserDetailsService {
    @Autowired
    private AdminMapper adminMapper;
    @Autowired
    private RoleMapper roleMapper;
    @Autowired
    private AuthMapper authMapper;

    // 总目标:根据表单提交的用户查询User对象,并装配角色、权限等信息
    @Override
    public UserDetails loadUserByUsername(
            // 表单提交的用户名
            String username

        ) throws UsernameNotFoundException {
        // 1.根据用户名从数据库查询 Admin 对象
        // ①创建 AdminExample 对象
        AdminExample adminExample = new AdminExample();
        // ②创建 Criteria 对象
        AdminExample.Criteria criteria = adminExample.createCriteria();
        // ③在 Criteria 对象中封装查询条件
        criteria.andLoginAcctEqualTo(username);
        // ④调用 AdminMapper 的方法执行查询
        List<Admin> adminList = adminMapper.selectByExample(adminExample);
        // ⑤得到Admin对象
        if(adminList == null || adminList.size() != 1) {
            return null;
        }
        Admin admin = adminList.get(0);
        // ⑥.获取数据库中密码
        String userPswd = admin.getUserPswd();
        // 2.根据adminId查询admin对应的权限信息(包括角色、权限)
        Integer adminId = admin.getId();
        List<Role> roleList = roleMapper.selectAssignedRole(adminId);
        // 3.根据adminId查询admin对应的权限信息(包括角色、权限)
        List<String> authNameList = authMapper.selectAssignedAuthNameByAdminId(adminId);
        // 4.给admin设置角色权限信息
        // ①创建集合用来存放权限信息
        ArrayList<GrantedAuthority> authorities = new ArrayList<>();
        // ②遍历 roleList 存入角色信息
        for (Role role : roleList) {
            String roleName = role.getName();
            // 注意:一定要加 "ROLE_"
            authorities.add(new SimpleGrantedAuthority("ROLE_" + roleName));
        }
        // ③.遍历 authNameList 存入权限信息
        for (String authName : authNameList) {
            authorities.add(new SimpleGrantedAuthority(authName));
        }
        // 5.把admin对象和authorities封装到SecurityAdmin中
        SecurityAdmin securityAdmin = new SecurityAdmin(admin, authorities);
        return securityAdmin;
    }
}

操作6:配置类CrowdfundingSecurityConfig中引入  UserDetailsService

@Override
protected void configure(AuthenticationManagerBuilder builder) throws Exception {
    // 正式功能中使用基于数据库的认证
    builder.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
    //        临时使用内存版登录的模式测试代码
    //        builder.inMemoryAuthentication()
    //                .passwordEncoder(passwordEncoder)
    //                .withUser("tom")
    //                .password("4297F44B13955235245B2497399D7A93")
    //                .roles("admin");
}

操作7:密码加密

①MD5加密

// 用以解决"java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null""报错问题
@Component
public class MyPasswordEncoder implements PasswordEncoder {
    //encode()方法对明文进行加密。
    @Override
    public String encode(CharSequence rawPassword) {
        return privateEncode(rawPassword);
    }
    //matches()方法对明文加密后和密文进行比较。
    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        // 1.对明文密码进行加密
        String formPassword = privateEncode(rawPassword);
        // 2.声明数据库查询出来的密码
        String databasePassword = encodedPassword;
        // 3.比较
        return Objects.equals(formPassword,databasePassword);
    }

    private String privateEncode(CharSequence rawPassword){
        try {
            // 1.创建MessageDigest对象
            String algorithm = "MD5";
            MessageDigest messageDigest = MessageDigest.getInstance(algorithm);

            // 2.获取rawPassword的字节数组
            byte[] input = ((String) rawPassword).getBytes();

            // 3.加密
            byte[] output = messageDigest.digest(input);

            // 4.创建 BigInteger 对象
            int signum = 1;//控制bigInteger是正数
            BigInteger bigInteger = new BigInteger(signum, output);

            // 5.按照 16 进制将 bigInteger 的值转换为字符串
            int radix = 16;
            String encoded = bigInteger.toString(radix).toUpperCase();

            return encoded;
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
            return null;
        }
    }
}

②带盐值的加密

I.准备BCryptPasswordEncoder对象,放到IOC容器里面

// 装配
@Autowired
private BCryptPasswordEncoder passwordEncoder;
// 保存到IOC容器
@Bean
public BCryptPasswordEncoder getPasswordEncoder(){
    return new BCryptPasswordEncoder();
}
// 使用
builder.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);

操作8:使用BCryptPasswordEncoder在保存admin时加密

在AdminServiceImpl中进行修改

//对密码进行md5加密
//String userPswd = admin.getUserPswd();
//String md5UserPswd = CrowdUtil.md5(userPswd);

//对密码进行盐值加密
String userPswd = admin.getUserPswd();
String bCryptUserPswd = passwordEncoder.encode(userPswd);

admin.setUserPswd(bCryptUserPswd);

操作9:在页面上显示用户昵称

SpringSecurity处理完登录操作之后把登陆成功的User对象以principal属性名存入了UsernamePasswordAuthenticationToken对象。

<p sec:authentication="name">
<p sec:authentication="principal.originalAdmin.loginAcct">

2.4权限控制

操作1:设置测试数据(更改数据库)

运行时计算权限需要的数据:
    用户:adminOperator
        角色:经理
        权限:无
        角色:经理操作者
        权限:user:save
        最终组装后:ROLE_经理,ROLE_经理操作者,user:save
    用户:roleOperator
        角色:部长
        权限:无
        角色:部长操作者
        权限:role:delete
        最终组装后:ROLE_部长,ROLE_部长操作者,role:delete,user:get
测试时进行操作的数据:
        admin01
        admin02
        ……
        role01
        role02
        ……

操作2:给资源上锁

①.访问 Admin 分页功能时具备“经理”角色

.antMatchers("/admin/getPage") //针对分页显示Admin数据设定访问控制
.hasRole("经理") //要求具备经历角色

效果:adminOperator可以访问,roleOperator不可以访问

②.访问 Role 分页功能时具备部长的角色

另一种方法:使用注解@PreAuthorize("hasRole('部长')")

@PreAuthorize("hasRole('部长')")
@ResponseBody
@RequestMapping("/ajax/role/get/page")
public ResultEntity<PageInfo<Role>> getRoleForAjax(
        @RequestParam(value = "keyword",defaultValue = "") String keyword,
        @RequestParam(value = "pageNum",defaultValue = "1") Integer pageNum,
        @RequestParam(value = "pageSize",defaultValue = "5") Integer pageSize){
    // 调用 Service 方法获取分页数据
    PageInfo<Role> rolePageInfo = roleService.getRolePageInfo(keyword, pageNum, pageSize);
    // 封装到 ResultEntity 对象中返回(如果上面的操作抛出异常,交给异常映射机制处理)
    return ResultEntity.successWithData(rolePageInfo);
}

注解生效的前提条件: 需要在配置类上加注解@EnableGlobalMethodSecurity(prePostEnabled = true),该注解表示启用全局方法权限控制功能,并且设置 prePostEnabled = true。保证@PreAuthority、@PostAuthority、@PreFilter、@PostFilter 生效

③.访问拒绝后的处理

这个结果为什么没有经过异常映射机制?

 所以要在 SpringSecurity 的配置类中进行配置

.exceptionHandling()
.accessDeniedHandler(new AccessDeniedHandler() {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
        request.setAttribute("exception","抱歉!您无法访问这个资源!");
        request.getRequestDispatcher("/to/no/auth/page").forward(request,response);
    }
});

④.访问 Admin 保存功能时具备 user:save 权限

@PreAuthorize("hasAuthority('user:save')")
@RequestMapping("/admin/addPage")
public String insertAdmin(Admin admin){
    adminService.saveAdmin(admin);
    return "redirect:/admin/getPage?pageNum="+Integer.MAX_VALUE;
}

⑤.访问 Admin 分页功能时具备“经理”角色或“user:get”权限二者之一

security
.antMatchers("/admin/getPage")// 针对分页显示 Admin 数据设定访问控制
.access("hasRole('经理') OR hasAuthority('user:get')") //要求具备"经理"角色和"user:get"权限二者之一

其他注解(了解)
@PostAuthorize:先执行方法然后根据方法返回值判断是否具备权限。

例如:查询一个 Admin 对象,在@PostAuthorize 注解中和当前登录的 Admin 对象进行比较,如果不一致,则判断为不能访问。实现“只能查自己”效果。@PostAuthorize("returnObject.data.loginAcct == principal.username")使用 returnObject 获取到方法返回值,使用 principal 获取到当前登录用户的主体对象

@PreFilter:在方法执行前对传入的参数进行过滤。只能对集合类型的数据进行过滤。

@PostFilter:在方法执行后对方法返回值进行过滤。只能对集合类型的数据进行过滤。 

⑥. 页面元素的权限控制

// access 属性可以传入权限控制相关的表达式
<security:authorize access="hasRole('经理')">
    <!-- 开始和结束标签之间是要进行权限控制的部分。检测当前用户是否有权限,有权限
就显示这里的内容,没有权限就不显示。 -->
……
</security:authorize>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值