总体设计
- 通过数据库存储角色、用户、资源信息;
- 后端通过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中的路由列表。