前言
为什么会聊到这个话题呢,是因为这段时间公司局势动荡,让我有了想要看看更多机会的想法,因此在interview的时候被问到,你们公司的系统是怎么做鉴权的呢,当然被问到这个问题后,凭借着我的记忆瞎答了一通,事后去梳理了一下,发现公司的鉴权逻辑原来是这样。
项目架构
公司内系统的鉴权架构图如下:
根据上面的架构图进行从底向上的逐层剖析
两种鉴权模式
- RBAC(role-Based Access Control)基于角色的访问控制
整体的设计思路就是创建多个角色(即权限所属的集合),然后将具体的功能权限点按角色定位分配给相应角色,然后赋予用户指定角色即可。 - ABAC(Attribute-Based Access Control)基于属性的权限控制
整体的设计思路是通过实体的属性、操作类型、相关的环境来控制是否有对操作对象的权限
二者相对优势:
- RBAC优势
- 角色明确逻辑简单明了易于理解,适合角色功能简单化耦合关系较小的权限控
- ABAC优势
- 控制粒度更细
- 可以解决复杂计算组合的权限场景
当前厂内采用的是RBAC的模式,逻辑简明易管理。
Node端
项目整体采用了BFF模式,因此在整个前端项目中会多出node端这个东西。Node端采用midway狂降,主要做的是将申请好的ACL key值,配置到config文件中,然后写好service和controller,来获取ACL那边配置好的角色信息。
前端
前端采用了umi框架,前端内主要做了两方面,一是全局鉴权,二是路由鉴权。
全局鉴权
全局鉴权函数的主要代码如下:主要会调用具体的函数,如checkAuth,来更新authList,再根据authList里面的角色权限去做具体的判断
import { getAuthsByUser, getRolesByUser } from '@/services/acl/api';
import { useCallback, useRef, useState } from 'react';
export default function useAuthority() {
const [updateTime, setUpdateTime] = useState<number>(Date.now());
const aclObj = useRef<any>({ authList: [], roleList: [] });
const rigister = useRef<any>([]);
const fetching = useRef<boolean>(false);
const loop = useCallback(
(list: any) => {
if (list.length) {
const current = list.pop();
current(aclObj.current);
loop(list);
}
return;
},
[aclObj],
);
const initData = useCallback(async () => {
if (fetching.current) {
return new Promise((resolve) => {
rigister.current.push(resolve);
});
} else {
fetching.current = true;
const roles = await getRolesByUser();
aclObj.current.roleList = roles;
const auths = await getAuthsByUser();
aclObj.current.authList = auths;
fetching.current = false;
if (rigister.current.length) {
loop(rigister.current);
}
return Promise.resolve(aclObj.current);
}
}, [fetching, rigister]);
const checkBlankData = useCallback(async () => {
if (!aclObj.current.authList.length || !aclObj.current.authList.length) {
setUpdateTime(Date.now());
return initData();
} else {
if (Date.now() - updateTime < 60000) {
return Promise.resolve(aclObj.current);
} else {
setUpdateTime(Date.now());
return initData();
}
}
}, [updateTime]);
/**
* 判断是否有对应权限或同时有一组权限
*/
const checkAllAuth = useCallback(async (keys: string | string[]) => {
if (!keys) {
return false;
}
const res = await checkBlankData();
if (typeof keys === 'string') {
return res.authList.includes(keys);
}
return keys.filter((key) => res.authList.includes(key)).length === keys.length;
}, []);
/**
* 判断是否是对应角色或同时属于多个角色
*/
const checkAllRole = useCallback(async (keys: string | string[]) => {
if (!keys) {
return false;
}
const res = await checkBlankData();
if (typeof keys === 'string') {
return res.roleList.includes(keys);
}
return keys.filter((key) => res.roleList.includes(key)).length === keys.length;
}, []);
/**
* 判断是否有对应权限或其中某个权限
*/
const checkAuth = useCallback(async (keys: string | string[]) => {
if (!keys) {
return false;
}
const res = await checkBlankData();
if (typeof keys === 'string') {
return res.authList.includes(keys);
}
return !!keys.filter((key) => res.authList.includes(key)).length;
}, []);
/**
* 判断是否是对应角色或属于其中某个角色
*/
const checkRole = useCallback(async (keys: string | string[]) => {
if (!keys) {
return false;
}
const res = await checkBlankData();
if (typeof keys === 'string') {
return res.roleList.includes(keys);
}
return !!keys.filter((key) => res.roleList.includes(key)).length;
}, []);
return {
authList: rigister.current.authList,
roleList: rigister.current.roleList,
checkAllAuth,
checkAllRole,
checkAuth,
checkRole,
};
}
路由权限
路由文件配置
let routes: MenuDataItem = [
{
name: '一级菜单',
path: '/x',
icon: 'AccountBookOutlined',
component: '@/layouts/page-layout',
auths: ['auth-flag'], // 在权限范围内可以访问(acl功能权限标识)
routes: [
{
path: '/x/list',
name: '二级菜单',
component: './x/list', // 具体的页面组件
auths: ['auth-flag'], // 在权限范围内可以访问(acl功能权限标识)
wrappers: ['@/components/auths/page-auths'], // 拓展的鉴权组件,用于验证当前页面权限
}
],
},
]
菜单渲染工具:patchRoutes
是umi
运行时的配置,可以拿到打平的路由表,感兴趣的可以去看下umi
框架
import { checkRouteAuths } from './utils/utils';
export function patchRoutes({ routes = [] as any }) {
routes = checkRouteAuths(auths, routes);
}
/**
* 路由权限处理,控制菜单是否展示
* @param auths
* @param routes
*/
export function checkRouteAuths(auths: string[], routes: any[]) {
routes.forEach((route) => {
if (route.routes) {
route.routes = checkRouteAuths(auths, route.routes);
}
if (route.auths != null) {
const res = route.auths.find((auth: string) => auths.includes(auth));
if (!res) {
route.hideInMenu = true;
}
}
});
return routes;
}
综上依靠整体结构来支撑各个业务方;
分享到这里,希望看到的小伙伴能点赞支持一下,白菜会每周不定期分享。