1. 权限控制各端需要做的工作
- 前端:
- 菜单/按钮作为页面的入口,让用户直接操作就能进入到对应的页面,是需要做权限控制的;菜单通常是无权限直接就不展示,按钮分情况不展示或点击给出提示,结合具体的需求场景来实现
- 路由权限控制:只控制菜单/按钮的不展示,并不完善,因为还可以通过输入URL来进入相应页面,而路由决定了能否进入当前页面,所以还需要前端对路由进行权限控制
- 后端:后端相对前端来说,权限控制更为重要,因为可以不通过界面而是直接通过接口来获取数据,所以后端必须对接口访问权限进行控制;同时如果对接口进行了控制,返回无权限状态码,前端同样可以根据状态码来进行重定向至无权限界面。
2. 前端权限控制实现
1. 获取权限列表的时机
应当是在用户登录后,内容页面展示之前获取权限列表,这样在展示具体页面前就把路由和菜单展示控制好。
2. 控制菜单的显示
改造前的项目菜单为配置式,由后台返回,后期因为要结合4A平台实现权限控制,后台改造较为麻烦,改为前端本地固定写死,后端只返回权限列表,这样就需要本地配置一个菜单表,结合后台返回的权限列表,对菜单进行权限赋值,如果有权限的情况下,菜单展示,无权限则直接不展示。因为菜单通常都是嵌套的,所以这里主要的实现就递归:
// 使用递归菜单权限赋值,获取到有权限的菜单列表functiongetMenuAuthor(menuList, resources) { menuList.forEach(item=> { if (item.children) { // 如果包含子项,递归getMenuAuthor(item.children, resources); } elseif (resources.includes(item.url)) { // 权限赋值item.authority=true; } }); returnmenuList; }
参考:
- 菜单数据demo:
// 菜单列表constmenuList= [ { "key": "sub1", "name": "Navigation One", "children": [ { "key": "g1", "name": "item1", "icon": "comment", "children": [ { "key": "1", "name": "Option 1", "icon": null, "url": "/sub1/item1/option1", "children": null }, { "key": "2", "name": "Option 2", "icon": null, "url": "/sub1/item1/option2", "children": null }, ] }, { "key": "g2", "name": "item2", "icon": "setting", "children": [ { "key": "1", "name": "Option 1", "icon": null, "url": "/sub1/item2/option1", "children": null }, { "key": "2", "name": "Option 2", "icon": null, "url": "/sub1/item2/option2", "children": null }, ] }] }, { "key": "sub2", "name": "Navigation Two", "children": [ { "key": "g1", "name": "item1", "icon": "comment", "children": [ { "key": "1", "name": "Option 1", "icon": null, "url": "/sub2/item1/option1", "children": null }, { "key": "2", "name": "Option 2", "icon": null, "url": "/sub2/item1/option2", "children": null }, ] }, { "key": "g2", "name": "item2", "icon": "setting", "children": [ { "key": "1", "name": "Option 1", "icon": null, "url": "/sub2/item2/option1", "children": null }, { "key": "2", "name": "Option 2", "icon": null, "url": "/sub2/item2/option2", "children": null }, ] }] }, ];
- 权限列表demo:
constresourceList= [ "/sub1/item1/option1", "/sub1/item1/option2", "/sub1/item2/option1", "/sub2/item2/option1", "/sub2/item2/option1", ];
这里还有个问题,点击顶部菜单的时候,需要进入对应模块的第一个有权限的页面,因为把页面菜单都是层层嵌套的,所以,这里需要找出第一个有权限的页面就需要再做一点工作:
setMenu= (e) => { // 筛选出第一个有权限的子菜单functionfirstAuthMenuItem(menus) { // 这里使用reduce函数,将层层嵌套的菜单的所有 children 扁平化,都放在一个数组中,这样就能很方便的找到第一个有权限的子菜单constgetMenuChild=menuList=>menuList.reduce((totalArr, current) => ( totalArr.concat(Array.isArray(current.children) ?getMenuChild(current.children) : current) ), []); returngetMenuChild(menus).find(item=>item.authority); } this.setState( { menuType: e, }, () => { const { menuArr } =this.state; const { history } =this.props; leturl; // 点击顶部菜单,进入第一个有权限的子菜单switch (e) { case'Navigation One': constsystemMenu=menuArr.filter(item=>item.name==='Navigation One'); url=firstAuthMenuItem(systemMenu.children).url; break; case'Navigation Two': constmessageMenu=menuArr.filter(item=>item.name==='Navigation Two'); url=firstAuthMenuItem(messageMenu.children).url; break; default: url='/document' ; break; } history.push(url); } ); };
3. 路由的控制
之前做Vue项目是使用 vue-router 提供的路由守卫在跳转前后取消跳转的方式,来实现路由权限的控制。react-router 相对于 vue-router 来说更为灵活,轻巧,所以没有内置这些API,但是基于它的灵活性,可以实现类似导航守卫的功能。
1. 实现思路
主要思路是使用了 React 的 Render Props
,Render Props 相比于固定写死的组件来说更为灵活,可以动态的决定渲染的结果。正如官网所说:
render prop 是一个用于告知组件需要渲染什么内容的函数 prop。
reacr-router 内置了 Render Props
,使用 来取代 ,根据路由权限,动态修改当前路由要渲染的结果。
2. 实现方案
- 首先,将所有的路由信息放在一起,构建一个路由表:
constrouterMap= [ { path: `/home`, component: HomePage, exact: true }, { path: `/test/page1`, component: Page1, exact: true }, { path: `/test/page2`, component: Page2, auth: true }, // 白名单页面 { path: `/exception/403`, component: Exception, auth: true } ];
- 这样,就能通过遍历的方式输出所有的路由信息,而且方便在遍历时进行一些统一操作。对不需要权限控制的页面组件,设置
auth:true
即可设置白名单。 - 遍历路由信息,为有权限的路由进行赋权。
// 遍历路由,权限赋值constauthRouterMap=routerMap.forEach(item=> { constitemCopy=Object.assign({}, item); if (resourceList.includes(item.path)) { itemCopy.auth=true; } returnitemCopy; });
- 使用
Route
组件的render prop
渲染组件
return ( <Switch><Routepath={`/`} render={() => ( // 重定向至第一个有权限的界面<Redirectto={routerMap.find(item=>item.auth).path} /> )} exact/> { routerMap.map((item, index) => ( <Routekey={index} path={item.path} exact={item.exact} render={props=> { if (item.auth) { // 如果有权限就渲染对应的页面组件return (<item.component {props} key={index} />); } // 如果无权限就渲染重定向至403页面return<Redirectto={{ pathname: `/exception/403` }} />; }} /> )) } </Switch>);
- 注意:如果使用了
render
函数,就不能再使用component
的方式了,因为component
的方式要优先于render
,render
会被覆盖掉。