Springboot+vue权限管理

总体设计

  • 通过数据库存储角色、用户、资源信息;
  • 后端通过springboot拦截器对api权限进行控制;
  • 后端提供接口返回用户可访问的模块及组件信息;
  • 前端通过用户可访问模块及组件信息动态加载侧边栏和页面中组件;

与其他设计的不同点

在设计过程中,也参考了很多权限模块的设计方案,具体链接如下:
springboot+shiro+mysql+mybatis(通用mapper)+freemarker+ztree+layui实现通用的java后台管理系统(权限管理+用户管理+菜单管理)
vue权限控制
手摸手,带你用vue撸后台 系列二(登录权限篇)
Vue 动态路由的实现(后台传递路由,前端拿到并生成侧边栏)
这几种设计方案均将角色传到前端,通过动态路由对界面展示进行控制。本文的设计采用了不同方案:将用户可访问模块及组件信息传到前端,对界面展示进行控制。
相对来说,他人的方案需要非常明确每个组件需要那些角色可以访问,当后期需要更改时,需要修改对应的前端代码才能完成授权。
本文方案的优点在于:将维护工作放在后端,角色权限分配改变时,不需要更改前端代码。

本文涉及的权限处理

  • 后端权限拦截:对api做权限控制,手动配置权限;
  • 侧边栏动态加载:不同权限对应不同路由,侧边栏根据用户权限异步生成;
  • 页面内组件动态加载:页面内的组件根据用户权限展示和隐藏。

权限相关数据库设计

数据库表还是经典的三张表:角色表(role),用户表(user),资源表(resources)。

  • role表:默认所有人都有普通用户的权限
id role_name role_desc
1 admin 管理员
2 manager_one 高级用户1
3 manager_two 高级用户2
4 ordinary 普通用户
  • user表:普通用户不需要专门授权,一个人可以对应多个角色
id name role_id
1 wangwu 1
2 zhangsan 2
3 zhangsan 3
  • resources表:存储所有资源
    资源表可以理解为树形结构,所有最顶级的组件的parent_id为0;
    若要后端对权限进行控制,则需要将uri录入,否则默认不拦截;
    若要增加新的权限类型,则资源表也需要增加一个对应的字段控制每个资源的权限。
id module_name parent_id uri admin manager_one manager_two ordinary
1 sidebar_a 0 /api/test_a 1 1 1 0
2 sidebar_child_a 1 /api/test_a/child_a 1 1 1 0
3 button_a 2 /api/test_a/bt_a 1 1 1 0
4 sidebar_b 0 /api/test_b 1 1 1 1
5 button_b 4 1 1 0 0
6 button_c 4 /api/test_b/bt_c 1 0 1 1
create table role (
  id           int auto_increment  comment '主键id'  primary key,
  role_name    varchar(100)  not null  comment '角色名称',
  role_desc    varchar(200)  default null  comment '角色描述'
) comment '角色管理表'  charset = utf8mb4;

create table user (
  id         bigint auto_increment  comment '主键id'  primary key,
  name       varchar(100)  not null  comment '姓名',
  role_id    int(5)  not null  comment '角色ID',
  foreign key(role_id) references role(id)
) comment '用户管理表'  charset = utf8mb4;

create table resources (
  id             int auto_increment  comment '主键id'  primary key,
  module_name    varchar(100)  not null  comment '模块名称',
  parent_id      int(11)  not null  comment '父模块ID',
  uri            varchar(128) default null  comment 'uri',
  admin          tinyint(1)   default 0  comment '管理员权限 1有权限 0无权限',
  manager_one    tinyint(1)   default 0  comment '高级用户1权限 1有权限 0无权限',
  manager_two    tinyint(1)   default 0  comment '高级用户2权限 1有权限 0无权限',
  ordinary_user  tinyint(1)   default 0 comment '普通用户权限 1有权限 0无权限'
) comment '资源管理表'  charset = utf8mb4;

后端权限控制

后端主要做两件事:1.对uri的权限进行控制;2.提供根据姓名获取可访问资源的接口。

uri权限控制

该部分主要通过拦截器控制。

import entity.Resources;
import entity.Role;
import entity.User;
import service.AuthorityService;
import util.UserUtils;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

public class AuthenticationInterceptor extends HandlerInterceptorAdapter {

    private static final String ADMIN = "admin";
    private static final String MANAGER_ONE = "manager_one";
    private static final String MANAGER_TWO = "manager_two";
    private static final String ORDINARY_USER = "ordinary_user";
    private AuthorityService authorityService;

    public AuthenticationInterceptor(AuthorityService authorityService) {
        super();
        this.authorityService = authorityService;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String uri = request.getRequestURI();
        System.out.println(uri);
        if (checkAuth(uri)) {
            return true;
        }
        // 拦截之后返回没有权限的异常
        Exception e = new RuntimeException("no permission!!!");
        throw e;
//        return false;
    }

    private boolean checkAuth(String uri) {
        Resources resources = authorityService.getResourcesByUri(uri);
        if (resources == null) {
            return true;
        }
        //根据登录信息获取name,需要根据自己的登录系统实现
        String name = UserUtils.getUser().getLogin();
        if (name == null || "".equals(name)) {
            return false;
        }
        List<User> users = authorityService.getRoleListByName(name);
        Set<String> roleSet = new HashSet<>();
        roleSet.add(Constants.ORDINARY_USER);
        if (users != null && users.size() > 0) {
            List<Role> allRoles = authorityService.getAllRoles();
            ConcurrentHashMap<Integer, String> roleMap = new ConcurrentHashMap<>(allRoles.size());
            for (Role role: allRoles) {
                roleMap.put(role.getId(), role.getRoleName());
            }
            for (User user : users) {
                roleSet.add(roleMap.get(user.getRoleId()));
            }
        }
        Set<String> resourcesRole = new HashSet<>();
        if (resources.getAdmin()) {
            resourcesRole.add(ADMIN);
        }
        if (resources.getManagerOne()) {
            resourcesRole.add(MANAGER_ONE);
        }
        if (resources.getManagerTwo()) {
            resourcesRole.add(MANAGER_TWO);
        }
        if (resources.getOrdinaryUser()) {
            resourcesRole.add(ORDINARY_USER);
        }
        int sum = roleSet.size() + resourcesRole.size();
        roleSet.addAll(resourcesRole);
        return sum != roleSet.size();
    }
}

根据姓名获取可访问资源的接口

@Controller
@ResponseBody
public class AuthorityController extends BaseController {
    private static final String ADMIN = "admin";
    private static final String MANAGER_ONE = "manager_one";
    private static final String MANAGER_TWO = "manager_two";
    private static final String ORDINARY_USER = "ordinary_user";

    @Autowired
    private AuthorityService authorityService;

    private final static Logger logger = new Logger(AuthorityController.class);

    @RequestMapping(value = "/authority/getAuthority", method = {RequestMethod.GET})
    private Result getAuthority() {
        List<String> result = new ArrayList<>();
        //根据登录信息获取name的接口需根据自己的登录系统实现
        List<User> userList = authorityService.getRoleListByName(getName());
        Resources resources = new Resources();
        if (userList != null && userList.size() > 0) {
            List<Role> roleList = authorityService.getAllRoles();
            Map<Integer, String> roleMap = new HashMap<>(roleList.size());
            for (Role role: roleList) {
                roleMap.put(role.getId(), role.getRoleName());
            }
            for (User user: userList) {
                String roleStr = roleMap.get(user.getRoleId());
                if (ADMIN.equals(roleStr)) {
                    resources.setAdmin(true);
                } else if (MANAGER_ONE.equals(roleStr) || MANAGER_TWO.equals(roleStr)){
                    if (MANAGER_ONE.equals(roleStr)) {
                        resources.setManagerOne(true);
                    }
                    if (MANAGER_TWO.equals(roleStr)) {
                        resources.setManagerTwo(true);
                    }
                } else {
                    resources.setOrdinaryUser(true);
                }
            }
        }
        List<Resources> resourcesList = authorityService.getAllResourcesByRole(resources);
        result = generateAuthority(resourcesList, 0, "", result);
        return Result.success(result);
    }

    private List<String> generateAuthority(List<Resources> resourcesList, int parentId, String parentStr, List<String> result) {
        if (resourcesList == null || resourcesList.size() == 0) {
            return null;
        }
        resourcesList.stream()
                .filter(c -> c.getParentId() == parentId)
                .forEach(c -> {
                    if (parentId == 0) {
                        result.add(c.getModuleName());
                        result.addAll(generateAuthority(resourcesList, c.getId(), c.getModuleName(), new ArrayList<>()));
                    } else {
                        result.add(parentStr + ":" + c.getModuleName());
                        result.addAll(generateAuthority(resourcesList, c.getId(), parentStr + ":" + c.getModuleName(), new ArrayList<>()));
                    }
                });
        return result;
    }
}

前端Vuex权限控制

所有的数据和操作都是通过vuex全局管理控制的。

  • 使用 authInfo 的接口来获取用户的权限信息(用户可以访问的模块或组件名称)列表,例如:sidebar_a:sidebar_child_a。
  • 利用权限信息列表计算出用户可访问的路由,通过 router.addRoutes 动态挂载这些路由。===>侧边栏
  • 需手动配置页面中组件的权限。===>组件
    只需权限控制的组件上添加 v-show="this.checkUserAuth('sidebar_b:button_c')"

router/index.js

  • 页面在初始化时加载所有人都可以访问的路由:constantRoutes
  • 动态路由通过增加meta字段来控制,用router.addRoutes动态挂载
export const constantRoutes = [
  {
    path: '/callback',
    component: SSOCallback,
    name: 'sso回调页面',
    hidden: true
  },
  {
    path: '/api/test_b',
    component: SidebarB
  }
];

export const asyncRoutes = [
  {
    path: '/api/test_a',
    component: SidebarA,
    meta: {
      authStr: 'sidebar_a'
    },
    children: [
      {
        path: '/child_a',
        component: SidebarChildA,
        meta: {
          authStr: 'sidebar_a:sidebar_child_a'
       }
    ]
  },
    {
    path: '/api/test_b',
    component: SidebarB,
    meta: {
      authStr: 'sidebar_b'
    }
  }
];

const createRouter = () => new Router({
    routes: constantRoutes
});
const router = createRouter();
export default router;

main.js

Vue.prototype.checkUserAuth = function(name) {
    try {
        let authList = sessionStorage.getItem('authList');
        return authList.indexOf(name) !== -1;
    } catch (e) {
        console.log(e);
    }
    return false;
};

var getRouter;

function hasPermission(authList, route) {
    if (route.meta && route.meta.authStr) {
        return authList.some(auth => route.meta.authStr === auth);
    }
    return true;
}

export function filterAsyncRoutes(routes, authList) {
    const res = [];
    routes.forEach(route => {
        const tmp = route;
        if (hasPermission(authList, tmp)) {
            if (tmp.children) {
                tmp.children = filterAsyncRoutes(tmp.children, authList);
            }
            res.push(tmp);
        }
    });
    return res;
}

function saveObjArr(name, data) {
    console.log(JSON.stringify(data));
    window.sessionStorage.setItem(name, JSON.stringify(data));
}

function getObjArr(name) {
    return JSON.parse(window.sessionStorage.getItem(name));
}

function routerGo(to, next) {
    let authList = getObjArr('authList');
    authList = Array.from(authList);
    let routes = Array.from(asyncRoutes);
    getRouter = filterAsyncRoutes(routes, authList);
    router.options.routes = getRouter;
    router.addRoutes(getRouter);
    // global.antRouter = getRouter;
    next({ ...to, replace: true });
}

router.beforeEach(async(to, from, next) => {
    console.log('getRouter' + getRouter);
    if (!getRouter) {
        let authList = [];
        console.log(store.state.auth.length === 0);
        if (store.state.auth.length === 0) {
            const res = await store.dispatch('getAuthInfo');
            console.log('res:' + res);
            authList = res.data.data.items;
        } else {
            authList = store.state.auth.authList;
        }
        console.log(authList);
        saveObjArr('authList', authList);
        routerGo(to, next);
    } else {
        next();
    }
});

后期维护

本文方法的最大优势就是后期维护工作较为容易。当有新的权限加入时,后端只需要维护三张表,代码做少量维护。前端则需修改router/index.js中的路由列表。

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

推荐阅读更多精彩内容