react+react-router前端路由的rbac实现方案
Published: 2023-05-16 09:38:24

当前项目的角色

```javascript
enum Role {
  owner = 1,
  member,
  admin,
  account,
}
```
预计实现效果，通过可配置项对前端路由自动过滤。

```typescript
export const permission: Record<Role, PayoutPermission> = {
  [Role.owner]: {
    entry: true,
  },
  [Role.admin]: {
    entry: false,
  },
  [Role.member]: {
    entry: false,
  },
  [Role.account]: {
    entry: true,
  },
}
```
## 实现步骤

- 在需要增加rbac的Route中增加对应字段
```typescript
const DashboardRouteConfig: RouteProps = {
  path: '/dashboard',
  wrapper: PrivateAuthWrapper,
  component: PrivateLayout,
  children: [
    {
      path: '',
      exact: true,
      component: UsersHome,
      rbac: 'user.entry',
      navItem: {
        label: 'Home',
        icon: 'Home',
        index: 1,
      },
    },
 ]
}

```
- 在src下创建rbac文件夹，为需要rbac的route增加对应的permission
```typescript
export const permission: Record<Role, PayoutPermission> = {
  [Role.owner]: {
    entry: true,
  },
  [Role.admin]: {
    entry: false,
  },
  [Role.member]: {
    entry: false,
  },
  [Role.account]: {
    entry: true,
  },
}
```
- rbac暴露统一的getPermissions（role）方法，getPermissions根据角色返回对应route的permission
```javascript
export const getPermissions = (role: Role) => {
  const debitCard = debitCardPermission[role]
  const boosterCard = boosterCardPermission[role]
  const standardCard = standardCardPermission[role]
  const transaction = transactionPermission[role]
  const team = teamPermission[role]
  const setting = settingPermission[role]
  const wallet = WalletPermission[role]
  const payout = PayoutPermission[role]
  const invoice = InvoicePermission[role]
  const user = UserPermission[role]
  const tally = TallyPermission[role]
  const rewards = RewardsPermission[role]
  const payroll = PayrollPermission[role]
  const ledgerRole = LedegerRolePermission[role]

  return {
    debitCard,
    boosterCard,
    standardCard,
    transaction,
    team,
    setting,
    rewards,
    wallet,
    payout,
    invoice,
    user,
    tally,
    payroll,
    ledgerRole,
  }
}

export type Rbac = ReturnType<typeof getPermissions>
```

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xatmjsli0v8r1iyxofpt.png)
- 对当前route tree进行递归遍历，把如果route中无rbac字段直接返回，有rbac字段，进行权限校验
```typescript
const routeFilter = (
  route: RouteProps,
  testPermission: ReturnType<typeof permissionsVerify>,
  rootState?: Partial<RootState>
) => {
  if (route.devRoute && process.env.NODE_ENV !== 'development') {
    return null
  }
  if (route.filter && rootState && !route.filter(rootState)) {
    return null
  }
  if (route.rbac && !testPermission(route.rbac)) {
    return null
  }
  if (route.children) {
    const filteredChildren = []
    for (const childRoute of route.children) {
      const validRoute = routeFilter(childRoute, testPermission, rootState)
      if (validRoute) {
        filteredChildren.push(validRoute)
      }
    }
    if (filteredChildren.length === 0) {
      return null
    }
    route = {
      ...route,
      children: filteredChildren,
    }
  }
  return route
}
```
- testPermission根据rbac进行权限校验
```typescript
export const permissionsVerify =
  (role: Role, isAccountant?: boolean) => (rules: string | string[]) => {
    if (isAccountant && role === 3) {
      role = 4
    }
    const rolePermissionTree = getPermissions(role)
    rules = Array.isArray(rules) ? rules : [rules]
    return rules.every((rule) => !!getPath(rolePermissionTree, rule))
  }
export function getPath(obj: any, path: string) {
  const normalizedPath = normalizeKeypath(path)
  if (normalizedPath.indexOf('.') < 0) {
    return obj[path]
  }
  const pathList = normalizedPath.split('.')

  let d = -1
  const l = pathList.length

  while (++d < l && obj != null) {
    obj = obj[pathList[d]]
  }
  return obj
}
```
## 当我们拿到过滤完成后的路由时，就可以对router进行render
```typescript
function RoutesRender({ routes }: { routes: RouteProps[] }) {
  const { path } = useRouteMatch()
  return (
    <Switch>
      {routes.map(
        (
          {
            path: relativePath,
            children,
            component: RenderComponent,
            wrapper: Wrapper,
            ...routeProps
          },
          index
        ) => {
          const basePath = urlJoin(path)
          const pathname = basePath(relativePath || '')
          const withWrapperRender = withWrapperComponent(Wrapper)

          if (!children) {
            // avoid members to go to the default route '/dashboard' which is a blank page in this routes
            return (
              <Route
                {...routeProps}
                path={pathname}
                key={(relativePath || '') + index}
                render={() => withWrapperRender(<RenderComponent />)}
              />
            )
          }
          return (
            <Route path={pathname} exact={routeProps.exact} key={relativePath}>
              {withWrapperRender(
                <RenderComponent>
                  <RoutesRender routes={children} />
                </RenderComponent>
              )}
            </Route>
          )
        }
      )}
    </Switch>
  )
}
```