简易的后台管理权限设计

前言

因为想做一个快速的后台开发模板框架(方便以后直接开发功能而不用纠结环境和页面框架搭建的选择),当时在权限控制方面纠结于spring security和shiro,但是由于对这2个框架理解都不深,只是停留在基础的使用上面,而且一般的后台管理也用不了那么多的功能,所以思前想后还是决定自己做一套权限系统设计,第一方便扩展,第二自己做的也更熟悉,更方便做特定功能的定制。看本文之前可以先看看我做的简易开发模板框架,最好看完之后运行一下,或许更方便理解本文

需求明确

做什么事情之前首先要明确需求,以下就是我整理的我所需要的功能

一、控制用户在没有权限访问时,访问了URL会提示没有权限,没有登录时访问会跳转到登录页面
二、菜单要根据用户拥有的权限填充菜单(只出现用户有权限的菜单),包括页面
三、尽可能的不拦截静态资源,如:css、js、HTML等
四、有默认权限功能,用户一旦创建可以拥有默认的权限,而避免没有任何权限无法进入系统
五、权限以角色分组,用户可以拥有多个角色,权限累加
六、有超级用户,不用授予角色就能访问所有功能,超级用户通过特定标识来识别,标识只能通过修改数据来实现(为什么会有这个功能,是因为一旦权限开启了,默认是没有其他用户的,这时候需要用超级用户来进行创建用户和授权,当然也是方便系统数据错乱时能进入系统调整)
七、如果是直接打开的网址,在没有权限时要跳转页面,如果是Ajax请求,那么则需要返回对应没有权限的json

数据库设计

需求明确后就要开始数据库的设计了,这一步因工程而异,因为我只涉及后台,前端只是简单的展示,所以才直接设计数据库,如果是做APP这种,那么还是先把原型设计好,再来设计数据库,一共有5张表:

菜单表
用户表
角色表
角色与菜单关联表
用户与角色关联表

先放一张数据UML图

数据库UML图
首先先看菜单设计
--创建菜单表
DROP TABLE IF EXISTS menu;
CREATE TABLE IF NOT EXISTS menu(
    id INT NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
    title VARCHAR(32) NOT NULL COMMENT '菜单名称',
    url VARCHAR(500) COMMENT '网址',
    icon VARCHAR(20) COMMENT '显示的图标',
    menu_type ENUM('0','1','2') NOT NULL DEFAULT '0' COMMENT '类型,0 菜单,1 连接网址,2 隐藏连接',
    display INT NOT NULL DEFAULT 1 COMMENT '显示排序',
    parent_id INT NOT NULL DEFAULT 0 COMMENT '父级的id,引用本表id字段',
    creator INT NOT NULL DEFAULT 0 COMMENT '创建者id,0为超级管理员',
    create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    update_user INT  COMMENT '更新者id',
    update_time TIMESTAMP NULL COMMENT '更新时间',
    status ENUM('0','1') NOT NULL DEFAULT '1' COMMENT '是否启用,0 禁用,1启用'
)ENGINE=InnoDB;

url 字段对应的是点击打开的网址,因为可能是父级菜单,所以可以为空,menu_type 对应的是菜单类型,0是菜单(指下面有子菜单的类型),链接网址就代表需要打开页面的菜单,这2个类型都是需要显示到菜单栏的(一般后台管理系统都有菜单栏),而隐藏链接呢就是不会显示在菜单栏,但是会显示在页面中或者页面中都不显示的,主要是针对页面中需要进入编辑页面和保存这类的,另外被禁用了的菜单也是不会显示到菜单列表的(超级用户除外),菜单是无限层级的

再看看角色表的设计
-- 创建角色表
DROP TABLE IF EXISTS role;
CREATE TABLE IF NOT EXISTS role(
  id INT NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '角色表主键,-1为默认角色',
  name VARCHAR(20) NOT NULL COMMENT '角色名称',
  create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  creator INT DEFAULT 0 COMMENT '创建的用户id',
  description VARCHAR(200) COMMENT '角色描述',
  update_user INT  COMMENT '更新者id',
    update_time TIMESTAMP NULL COMMENT '更新时间'
)ENGINE=InnoDB;

角色表就很简单了,唯一注意点就是当id为-1是属于默认角色,不管这个角色叫啥名字,都是属于默认角色,而且是在代码中控制的,也就是说用户不需要对默认角色进行赋予,接下来看角色和菜单的关联表

角色和菜单关联表
-- 创建角色与菜单(资源的关联表)
DROP TABLE IF EXISTS role_menu;
CREATE TABLE IF NOT EXISTS role_menu(
 roleid INT NOT NULL COMMENT '角色id',
 menuid INT NOT NULL COMMENT '菜单id',
 creator INT NOT NULL COMMENT '创建人,0为初始化',
 create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'
)ENGINE=InnoDB;
ALTER TABLE role_menu add constraint PK01_role_menu primary key (menuid,roleid);

角色和菜单关联表,简单来说就是用来控制那个角色可以访问那些菜单,以角色id和菜单id作为主键

用户表
-- 后台管理用户表
DROP TABLE IF EXISTS admin_user;
CREATE TABLE IF NOT EXISTS admin_user(
  id INT NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '用户表主键,id为-1则是超级用户',
  name VARCHAR(20) NOT NULL COMMENT '用户名',
  psw VARCHAR(32) NOT NULL COMMENT '用户密码MD5加密',
  email VARCHAR(32) NOT NULL COMMENT '用户邮箱',
  creator INT NOT NULL COMMENT '创建人,0为初始化',
  flag INT(1) NOT NULL DEFAULT 1 COMMENT '用户状态,1启用,0禁用',
  last_login_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '最后登录时间',
  update_user INT  COMMENT '更新者id',
  update_time TIMESTAMP NULL COMMENT '更新时间'
)ENGINE=InnoDB;

这里和角色一样,id为-1的时候用户为超级用户

用户角色关联表
-- 创建用户与角色关联表
DROP TABLE IF EXISTS user_role;
CREATE TABLE IF NOT EXISTS user_role(
 userid INT NOT NULL COMMENT '用户id',
 roleid INT NOT NULL COMMENT '角色id',
 creator INT NOT NULL COMMENT '创建人,0为初始化',
 create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'
)ENGINE=InnoDB;
ALTER TABLE user_role add constraint PK01_user_role primary key (userid,roleid);

此表表明用户被授予了哪些角色,用户与角色属于多对多关系,用户id和角色id为联合主键

权限拦截流程设计

不管我们做什么开发,写代码之前画好流程图是有必要的,这样既能方便后期开发,也容易查出前期的设计是否有问题,极大的减少了后期修改的可能,下面就流程图

权限拦截流程图

接下来我就根据流程图的模块一块一块的进行开发就好了

用户登录

用户登录与普通用户登录一样,只是在登录成功后需要根据用户id查询出用户拥有的权限集合,并保存在session中(因为前端页面需要用到),同时查询用户的权限的时候需要判断用户的id是否为0,如果是0则查询出所有(只是查询出所有的菜单URL),下面是查询的sql语句

SELECT url FROM menu WHERE id IN(SELECT menuid FROM role_menu WHERE roleid IN(SELECT roleid FROM user_role WHERE userid=#{userid}) OR roleid=-1) AND `status`='1'"

or roleid=-1 这是为了方便查询出默认的角色,而我们的拦截器主要是根据请求的URL拦截,所以我们这里只需要查询菜单的URL集合就行,而页面展示的菜单是需要在另外一个地方查询出来的

查询菜单的sql语句
SELECT id, name, url, icon, menu_type, display, parent_id FROM menu WHERE id IN(SELECT menuid FROM role_menu WHERE roleid IN (SELECT roleid FROM user_role WHERE userid=#{userid}) OR roleid=-1) AND menu_type<>'2' AND `status`='1'"

查询出来后需要在代码中进行分级,一下是Java的实现

        HashMap<Integer,ArrayList<Menu>> map = new HashMap<Integer, ArrayList<Menu>>();
        List<Menu> tempMenus = null;
        if(userId == -1){
            MenuCriteria criteria = new MenuCriteria();
            criteria.createCriteria().andStatusEqualTo("1").andMenuTypeNotEqualTo("2");
            tempMenus = mapper.selectByExample(criteria);
        }else {
            tempMenus = mapper.selectByUser(userId);
        }
        for(Menu menu : tempMenus){
            int parentid = menu.getParentId();
            if(map.containsKey(parentid)){
                map.get(parentid).add(menu);
            }else{
                ArrayList<Menu> temp = new ArrayList<Menu>();
                temp.add(menu);
                map.put(menu.getParentId(),temp);
            }
        }
        for(Menu menu : tempMenus){
            int id = menu.getId();
            if(map.containsKey(id)){
                menu.setType("folder");
                menu.setChildren(map.get(id));
            }else{
                menu.setType("item");
            }
        }
        return map.get(0);

拦截器核心

根据前面的需求,所以我是直接采用的Spring的aop来进行拦截,这样做的好处就是不会拦截到页面和静态资源,只会对Controller进行拦截,进入拦截器中,首先会判断请求的URL是否是登录页面,如果是直接不拦截,如果不是则从session中获取当前登录的用户,然后判断是否为null,如果是null,则判断被调用的方法的返回值类型(因为没有找到获取方法有哪些注解方法,所以只能通过这种方法来曲线救国),如果是返回的String或者ModelAndView,则返回跳转到登录页面的ModelAndView,如果返回值是WebResult(指定的用@ResponseBody),则返回没有登录的json,如果是登录了,则判断是否为超级用户(超级用户无需拦截),不是则判断当前请求的URL是否在权限集合中(登录时查询出来的URL集合),如果没有在权限集合则同没有登录相同处理,只是返回的值不一样而已,如果有,则不拦截

前端不展示没有权限的链接

菜单本身是根据角色查询出来的,所以不会存在可见的菜单没有权限,但是页面中的一些链接,就需要进行控制了,而我采用的是用标签判断链接的URL或者按钮事件会请求的URL是否在权限集合中,如果存在则显示,不存在则不显示(是直接不会有这段HTML,不是简单的页面隐藏),至此一个简易的权限控制就设计完成了

结尾

最后附上几个存储过程

因为是无限级联的设计,所以如果删除菜单后没有删除子菜单,那么就会出现垃圾数据

删除菜单的存储过程
-- 删除菜单的存储过程
DROP PROCEDURE IF EXISTS `delete_menu`;
CREATE  PROCEDURE `delete_menu`(IN `menuid` int)
BEGIN

    DECLARE rowNUM INT DEFAULT 0;
    create temporary table if not exists menu_del_temp -- 不存在则创建临时表
  (
     id INT
  );
    create temporary table if not exists menu_del_temp2 -- 不存在则创建临时表
  (
     id INT
  );
create temporary table if not exists menu_del_temp3 -- 不存在则创建临时表
  (
     id INT
  );
    TRUNCATE TABLE menu_del_temp2;
    TRUNCATE TABLE menu_del_temp; -- 清空临时表
        INSERT INTO menu_del_temp SELECT id FROM  menu where parent_id=menuid;
    -- DELETE FROM category WHERE ID IN (SELECT id FROM category_del_temp);
    INSERT INTO menu_del_temp2 SELECT id FROM  menu where parent_id IN (SELECT id FROM menu_del_temp);
    SELECT COUNT(id) INTO rowNUM FROM menu_del_temp2;
    WHILE rowNUM > 0 DO
        INSERT INTO menu_del_temp SELECT id FROM menu_del_temp2;
        TRUNCATE TABLE menu_del_temp3;
        INSERT INTO menu_del_temp3 SELECT id FROM menu_del_temp2;
        TRUNCATE TABLE menu_del_temp2;
        INSERT INTO menu_del_temp2 SELECT id FROM  menu where parent_id IN (SELECT id FROM menu_del_temp3);
        SELECT COUNT(id) INTO rowNUM FROM menu_del_temp2;
    END WHILE;
    INSERT INTO menu_del_temp(id) values(menuid);
    DELETE FROM menu WHERE id IN (SELECT id FROM menu_del_temp);
    DELETE FROM role_menu WHERE menuid IN (SELECT id FROM menu_del_temp);
END;
更新角色菜单存储过程
DELIMITER $$

DROP function IF EXISTS `func_split_TotalLength` $$

CREATE FUNCTION `func_split_TotalLength`

(f_string varchar(1000),f_delimiter varchar(5)) RETURNS int(11)

BEGIN

    return 1+(length(f_string) - length(replace(f_string,f_delimiter,'')));

END$$

DELIMITER;
-- 拆分传入的字符串,返回拆分后的新字符串
DELIMITER $$

DROP function IF EXISTS `func_split` $$

CREATE  FUNCTION `func_split`

(f_string varchar(1000),f_delimiter varchar(5),f_order int) RETURNS varchar(255) CHARSET utf8

BEGIN
        declare result varchar(255) default '';

        set result = reverse(substring_index(reverse(substring_index(f_string,f_delimiter,f_order)),f_delimiter,1));

        return result;

END$$

DELIMITER;
-- 更新角色权限的存储过程
delimiter $$
DROP PROCEDURE IF EXISTS `role_menu_update` ;

CREATE PROCEDURE `role_menu_update`

(IN menuids varchar(3000),IN i_roleid INT,IN userid INT)

BEGIN

-- 拆分结果

DECLARE cnt INT DEFAULT 0;

DECLARE i INT DEFAULT 0;

SET cnt = func_split_TotalLength(menuids,',');
DELETE FROM role_menu WHERE roleid = i_roleid;

WHILE i < cnt

DO

    SET i = i + 1;

    INSERT INTO role_menu(roleid,menuid,creator) VALUES (i_roleid,func_split(menuids,',',i),userid);

END WHILE;

END $$
更新用户角色
-- 更新用户角色信息
delimiter $$
DROP PROCEDURE IF EXISTS `user_role_update` ;

CREATE PROCEDURE `user_role_update`

(IN roleids varchar(3000),IN i_userid INT,IN i_creator INT)

BEGIN

-- 拆分结果

DECLARE cnt INT DEFAULT 0;

DECLARE i INT DEFAULT 0;

SET cnt = func_split_TotalLength(roleids,',');
DELETE FROM user_role WHERE userid = i_userid;

WHILE i < cnt

DO

    SET i = i + 1;

    INSERT INTO user_role(userid,roleid,creator) VALUES (i_userid,func_split(roleids,',',i),i_creator);

END WHILE;

END $$

另外附上本项目地址

github

oschina git

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,417评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,921评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,850评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,945评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,069评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,188评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,239评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,994评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,409评论 1 304
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,735评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,898评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,578评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,205评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,916评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,156评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,722评论 2 363
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,781评论 2 351

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,904评论 25 707
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,644评论 18 139
  • 权限的理解 权限的话,权限做起来就是5张表;用户表、角色表、用户角色表、权限表、权限角色表。给用户授角色给角色授权...
    柠檬冰块阅读 809评论 2 0
  • 01 “陈飞,你别走呀,你走了,我怎么办?陈飞,陈飞……”我追着陈飞的公交跑,公交离我越远,我就喊得越大声,直到公...
    青如许阅读 1,046评论 8 15
  • 今天是2017.11.1,意味着2017年已经过了六分之五,也意味着,离过年不远了。单身的你,准备好了父母或者七八...
    简Jane安阅读 512评论 0 1