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 users = authorityService.getRoleListByName(name);
        Set roleSet = new HashSet<>();
        roleSet.add(Constants.ORDINARY_USER);
        if (users != null && users.size() > 0) {
            List allRoles = authorityService.getAllRoles();
            ConcurrentHashMap 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 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 result = new ArrayList<>();
        //根据登录信息获取name的接口需根据自己的登录系统实现
        List userList = authorityService.getRoleListByName(getName());
        Resources resources = new Resources();
        if (userList != null && userList.size() > 0) {
            List roleList = authorityService.getAllRoles();
            Map 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 resourcesList = authorityService.getAllResourcesByRole(resources);
        result = generateAuthority(resourcesList, 0, "", result);
        return Result.success(result);
    }

    private List generateAuthority(List resourcesList, int parentId, String parentStr, List 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中的路由列表。

你可能感兴趣的:(Springboot+vue权限管理)