React App Framework - 6. Permission

前言

在大型项目内, 权限控制也是个老生常谈的问题了, 目前已有的权限系统没有继承的功能, 在多维度管理的时候非常难用而且无法统一管理, 所以我独立设计了一套基于RBAC的带继承的权限系统, 我的设计理念也非常简单, 权限只需要提供一个最简单的查询接口: 根据子应用(app)/项目(project)/用户(user)返回他所有的权限点集合就行.

具体的权限设计这里不展开详细讲解, 重点核心内容是基础组与继承, 子应用和项目实际用到的角色都是由基础组派生出来的, 所以在新增权限点和修改角色权限的时候都非常简单. 而且能生成一份完整的角色树缓存, 在修改时修改db后更新缓存树, 在查询的时候的性能也非常高.

这里主要说说前端如何最简单的使用这套权限系统

应用

用户信息

一般的流程是进入系统后发现没有登录, 自动跳转到登录页, 只需要在Root组件里加上登录状态的检查即可

这里需要注意的是为了减少渲染和防止信息泄露, 需要在登录之后再去加载子应用和菜单组件
登录状态检查不依赖任何数据检查, 直接尝试请求用户信息接口, 如果请求成功则认为用户有效, 并写入全局store
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
const dispatch = useDispatch();
const userInfo = useSelector((state: AppState) => state.globalConfig.userInfo);

const [loginState, setLoginState] = useState<{
loading: boolean;
data: AuthUserInfo;
error: Error;
}>({
loading: false,
data: null,
error: null,
});

useEffect(() => {
if (userInfo) {
return;
}
(async () => {
setLoginState({
loading: true,
data: null,
error: null,
});
try {
const result = await UTILS.get<{ code: number; msg: string; data: AuthUserInfo }>(
`${api.uauth.getUserInfo()}?${queryString.stringify(
{ apps: _.uniq(_.map(CONSTANT.ALL_APPS, (app) => app.authApp)) },
{ arrayFormat: 'index' },
)}`,
);

if (result.status === 200) {
setLoginState({
loading: false,
data: result.data.data,
error: null,
});
dispatch(userInfoDucks.refreshUserInfo(result.data.data));
} else if (result.status !== 200) {
throw new Error(result.data.msg);
}
} catch (e) {
console.warn(`Login error:`, e);
setLoginState({
loading: false,
data: null,
error: e,
});
goLogin();
}
})();
}, [userInfo]);

项目列表

在上面的userInfo接口中, 已经返回了该用户拥有的各子应用有权限的项目, 这里只需要在项目列表渲染时取出对应的项目信息即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// app-header-nav.tsx
const RootAppHeaderNav = () => {
const projects = globalConfig.appConfig[globalConfig.app]?.allProjects || [];

return (
<div>
{projectSearchShowState && (
<HeaderProjectSelect
projects={projects}
project={globalConfig.project}
onSelect={(project: Project) => {
dispatchProjectSearchShowState({ type: 'HIDE' });
changeProjectWithRouter(project);
}}
onCancel={() => dispatchProjectSearchShowState({ type: 'HIDE' })}
/>
)}
</div>
);
};
main组件渲染时候需要检查是否选择过项目以及项目是否有权限, 否则默认跳转到上次打开的项目或者第一个有权限的项目
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// app-main.tsx
const RootAppProjectContainer: React.FC<{
routeProps: RouteComponentProps,
}> = ({ routeProps }) => {
const { globalConfig, userInfo } = useSelector((state: AppState) => ({ globalConfig: state.globalConfig, userInfo: state.userInfo }), _.isEqual);

const projects = globalConfig.appConfig[globalConfig.app].allProjects;
return !userInfo ? null : projects.length === 0 ? (
<Redirect to='/user/project-list' />
) : (
<Switch>
<Route
exact
path={`${routeProps.match.url}`}
render={(props) => {
const project = getProject(app, projects);
return <TransformDefaultProject project={project} url={routeProps.match.url} />;
}}
/>
<Route
path={`${routeProps.match.url.replace(/\/$/, '')}/:project`}
render={(props) => {
const project = getProject(app, projects, props.match.params.project);
return globalConfig.project?.code !== project.code ? (
<TransformProject project={project} changeRouter={project.code !== props.match.params.project} />
) : (
<RouteLazyComponent
app={app}
isApp
project={project}
title={title}
import={import(/* webpackChunkName: "[request]" */ `./../../../containers/${app}/index.tsx`)}
routeProps={props}
/>
);
}}
/>
<Route render={() => <Redirect to={routeProps.match.url} />} />
</Switch>
);
};

项目权限

在切换项目或者切换app的时候, 都会触发changeProjectAppaction, 这时候通过redux-thunk去获取用户在项目中的权限点, 并且更新到全局store中.

这里注意在reduce的时候, 需要判断传入的信息和当前系统状态是否一致, 否则可能发生快速多次切换项目后权限出错的情况
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
dispatch({
type: 'GLOBAL_CHANGE_USER_PROJECT_PERMISSION_LOADING_STATUS',
payload: { project: project, app: app, loading: true },
});
try {
const permission = await UTILS.restful.fetch<{ code: number; data: { codes: string[] }; msg: string }>(
api.uauth.getUserPermission(CONSTANT.ALL_APPS[app].authApp, project.code),
);
if (permission.data?.code !== 200) {
throw new Error(permission.data?.msg);
}
permissionSet = new Set(permission.data?.data?.codes || []);
} catch (e) {
console.warn(`get [${app}] [${project.code}] user permission error`, e);
} finally {
dispatch({
type: 'GLOBAL_CHANGE_USER_PROJECT_PERMISSION',
payload: { project: project, app: app, permission: permissionSet },
});
dispatch({
type: 'GLOBAL_CHANGE_USER_PROJECT_PERMISSION_LOADING_STATUS',
payload: { project: project, app: app, loading: false },
});
}

菜单权限

这一步就比较简单了, 直接从全局store里获取到用户当前权限, 判断菜单配置的权限点, 如果没有配置或者拥有对应权限, 就进行直接渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
const AppLeftMenu = () => {
const permission = useSelector((state: AppState) => state.globalConfig.permission);
const noEmptyPermission = useMemo(() => permission || new Set(), [permission]);

return (
<div>
{menu.children
.filter((c) => !c.hide && (!c.permission || noEmptyPermission.has(c.permission)))
.map((c) => (
<NavLink
exact={!!c.exact || c.path === ''}
activeClassName='active'
className='menu-item-children-item'
to={c.to ? c.to : `${!c.path ? match.url : match.url.replace(/\/$/, '')}${c.path}`}
key={c.name}
>
<span className='menu-item-children-item-icon' />
<span className='menu-item-children-item-text'>{t(c.name)}</span>
</NavLink>
))}
</div>
);
};

const AppMain = () => {
const permission = useSelector((state: AppState) => state.globalConfig.permission);
return (
<>
{!permission ? null : (
<Switch>
{menu
.filter((c) => !c.permission || permission.has(c.permission))
.map((p) => {
const pc = p as SubAppMenuParentHasChildren;
const pi = p as SubAppMenuParentItem;
return _.size(pc.children) > 0 ? (
pc.children
.filter((c) => !c.permission || permission.has(c.permission))
.map((c) => (
<Route
exact={!c.path ? true : !!c.exact}
key={c.name}
path={!c.path ? match.url : `${match.url.replace(/\/$/, '')}${c.path}`}
render={(props) => (
<RouteLazyComponent
app={app}
subApp={subApp}
isApp={false}
path={c.component_name}
title={c.title}
project={project}
import={import(/* webpackChunkName: "[request]" */ `./../../containers/${app}/containers/${c.component_name}/index.tsx`)}
routeProps={props}
/>
)}
/>
))
) : (
<Route
exact={!pi.path ? true : !!pi.exact}
key={p.name}
path={!pi.path ? match.url : `${match.url.replace(/\/$/, '')}${pi.path}`}
render={(props) => (
<RouteLazyComponent
app={app}
subApp={subApp}
isApp={false}
path={pi.component_name}
title={p.title}
project={project}
import={import(/* webpackChunkName: "[request]" */ `./../../containers/${app}/containers/${pi.component_name}/index.tsx`)}
routeProps={props}
/>
)}
/>
);
})}
{_.size(menu) && <Route render={() => <Redirect to={`${match.url}`} />} />}
</Switch>
)}
</>
);
};

权限组件

为了减少需要使用权限的组件的开发量, 提供一个通用的权限控制组件, 可以减少代码中的store的引用

1
2
3
4
5
6
7
8
const PermissionComponent: React.FC<{
code: string,
children: ReactElement,
empty?: ReactElement,
}> = ({ code, children, empty = null }) => {
const permission = useSelector((state: AppState) => state.globalConfig.permission);
return permission.has(code) ? children : empty;
};

小结

外层将权限封装完成后, 对于各子应用来说, 各自的权限使用和限制就非常简单了, 也不用担心出bug, 不过后端接口的权限限制还是必须得有的, 同样是通过上面的用户项目权限接口验证即可, 对于egg来说, 使用一个通用的权限中间件就能简单的搞定

作者

Mosby

发布于

2019-12-05

许可协议

评论