DEV Community

inno
inno

Posted on

react+react-router前端路由的rbac实现方案

当前项目的角色

enum Role {
  owner = 1,
  member,
  admin,
  account,
}
Enter fullscreen mode Exit fullscreen mode

预计实现效果,通过可配置项对前端路由自动过滤。

export const permission: Record<Role, PayoutPermission> = {
  [Role.owner]: {
    entry: true,
  },
  [Role.admin]: {
    entry: false,
  },
  [Role.member]: {
    entry: false,
  },
  [Role.account]: {
    entry: true,
  },
}
Enter fullscreen mode Exit fullscreen mode

实现步骤

  • 在需要增加rbac的Route中增加对应字段
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,
      },
    },
 ]
}

Enter fullscreen mode Exit fullscreen mode
  • 在src下创建rbac文件夹,为需要rbac的route增加对应的permission
export const permission: Record<Role, PayoutPermission> = {
  [Role.owner]: {
    entry: true,
  },
  [Role.admin]: {
    entry: false,
  },
  [Role.member]: {
    entry: false,
  },
  [Role.account]: {
    entry: true,
  },
}
Enter fullscreen mode Exit fullscreen mode
  • rbac暴露统一的getPermissions(role)方法,getPermissions根据角色返回对应route的permission
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>
Enter fullscreen mode Exit fullscreen mode

Image description

  • 对当前route tree进行递归遍历,把如果route中无rbac字段直接返回,有rbac字段,进行权限校验
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
}
Enter fullscreen mode Exit fullscreen mode
  • testPermission根据rbac进行权限校验
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
}
Enter fullscreen mode Exit fullscreen mode

当我们拿到过滤完成后的路由时,就可以对router进行render

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)