React通用解决方案——组件权限控制

4,865 阅读4分钟

前话

具备用户体系的业务系统往往需要具备权限控制的能力。前后不分离的开发模式权限控制主要由后端结合模版引擎实现,而前后分离的开发模式由前后两端协商权限配置分别进行数据权限控制和组件权限控制。

正文

权限配置格式不限,为进行后续演示本篇设定权限配置如下:

export type IAuthConfig = {
  /** 权限标识列表 */
  keys: string[];
};

且提供一个Hook直接获取当前用户信息:

export type IUser = {
  /** 权限配置 */
  authConfig: IAuthConfig;
};

/** 用户信息Hook */
export function useUser() {
  // ...略
  return user;
}

首先我们先定义一个权限Hook,内容如下:

/**
 * 转换为权限标识数组
 * @param auth 权限标识
 * @returns
 */
function getAuthKeys(auth: string | string[]) {
  return Array.isArray(auth) ? auth : [auth];
}

/** 权限鉴权类型 */
export enum EAuthType {
  /** 或权限(至少匹配一个) */
  "some" = "some",
  /** 与权限(全部匹配) */
  "every" = "every",
}

/** 权限Hook */
export function useAuth() {
  const { authConfig } = useUser();

  // 权限标识列表
  const authKeys = useMemo(() => authConfig?.keys ?? [], [authConfig]);
  // 校验是否具备权限
  const hasAuth = useCallback(
    (auth: string | string[], type?: EAuthType) =>
      getAuthKeys(auth)[type ?? EAuthType.every]((key) =>
        authKeys.includes(key)
      ),
    [authKeys]
  );

  const ret: [typeof authKeys, typeof hasAuth] = [authKeys, hasAuth];
  return ret;
}

1. 控制方式

对于前端开发而言一般只需做组件控制,控制方式有很多种写法。

1.1 直接计算

const Cmpt: React.FC = () => {
  const [, hasAuth] = useAuth();
  // 计算是否有权限
  const authorized = useMemo(() => hasAuth("test"), [hasAuth]);

  // ...略
};

1.2 通用权限Hoc

export function withAuth<P extends Record<string, unknown> = {}>(
  auth: string | string[],
  type?: EAuthType
) {
  return function (Component: any) {
    const WrappedComponent: React.FC<P> = (props) => {
      const [, hasAuth] = useAuth();

      const instance = React.createElement(Component, {
        ...props,
      });

      // 计算是否有权限
      const authorized = hasAuth(auth, type);

      // ...略
    };

    WrappedComponent.displayName = `withAuth(${getDisplayName(Component)})`;

    return WrappedComponent;
  };
}

1.3 权限包裹组件

const AuthWrapper: React.FC<IProps> = ({ auth, type, children }) => {
  const [, hasAuth] = useAuth();
  // 计算是否有权限
  const authorized = useMemo(() => hasAuth(auth, type), [auth, type, hasAuth]);

  // ...略
};

2. 控制结果

常见控制结果为控制组件的显示或隐藏,或根据是否具备权限做组件的自定义渲染。

为方便演示后面统一使用权限包裹组件进行说明。

2.1 显隐控制

具备权限的组件直接渲染,否则返回null,即可实现显隐控制。权限包裹组件实现如下:

const AuthWrapper: React.FC<IProps> = ({ auth, type, children }) => {
  const [, hasAuth] = useAuth();
  // 计算是否有权限
  const authorized = useMemo(() => hasAuth(auth, type), [auth, type, hasAuth]);
  // 具备权限的组件直接渲染,否则返回null
  return authorized ? children : null;
};

在需要权限控制的组件外包裹权限组件即可。

const Cmpt: React.FC = () => {
  <>
    <AuthWrapper auth="test">
      <Button>Hello World</Button>
    </AuthWrapper>
  </>;
};

2.2 自定义渲染

实际业务场景中,存在需要提醒用户没有权限的情况,这时需要权限包裹组件支持自定义渲染,实现方式有多种:

  • 静态props注入
  • 动态props映射
  • render props

相比较之下render props更为自由,权限包裹组件完善后实现如下:

type IProps = {
  /** 权限标识 */
  auth: string | string[];
  /** 鉴权类型 */
  type?: EAuthType;
  /** 子内容 */
};

const AuthWrapper: React.FC<IProps> = ({ auth, type, children }) => {
  const [, hasAuth] = useAuth();
  // 计算是否有权限
  const authorized = useMemo(() => hasAuth(auth, type), [auth, type, hasAuth]);

  // 自定义渲染
  if (typeof children === "function") {
    return children(authorized);
  }

  // 显隐控制
  return authorized ? children : null;
};

这时就可以渲染提示无权限组件:

const Cmpt: React.FC = () => {
  <>
    <AuthWrapper auth="test">
      {(authorized) => <Button disabled={!authorized}>Hello World</Button>}
    </AuthWrapper>
  </>;
};

3. 权限数据

前端开发做组件控制的颗粒度取决于权限数据的类型,权限数据类型分为静态权限和动态权限。

其中静态权限一般在完成登录认证后即可一次性获取,而动态权限则在操作数据时进行权限校验。

因此不难发现静态权限往往用于控制路由、页面或者粗糙的操作权限。而动态权限则能够对动态数据进行更细粒度的操作权限控制(需后端数据权限控制能力配合)。

3.1 静态权限

如前面描述,登录认证后即可从用户信息中得到权限标识,同样前面的栗子均也为静态权限示例。

3.2 动态权限

需要动态权限校验的场景在业务系统中也较为常见,例如用户能够看到列表数据,但禁止查阅无权限的数据详情。

由此可知对于用户而言,获取的动态的列表数据需要逐一进行权限校验,这时我们对权限Hook和包裹组件进行改造,改造后代码如下:

type IAuthDynamicConfig = {
  // ...略
};

/** 权限Hook */
export function useAuth() {
  // ...略

  // 动态校验是否具有权限
  const dynamicAuth = useCallback(
    (auth: string | string[], type?: EAuthType, config?: IAuthDynamicConfig) =>
      // 批量异步校验权限,实现略
      append2DynamicAuthTask(auth, type, config),
    []
  );

  const ret: [typeof authKeys, typeof hasAuth, typeof dynamicAuth] = [
    authKeys,
    hasAuth,
    dynamicAuth,
  ];
  return ret;
}

/** 权限包裹组件 */
const AuthWrapper: React.FC<IProps> = ({ auth, type, config, children }) => {
  const [, hasAuth, dynamicAuth] = useAuth();

  const [authorized, setAuthorized] = useState(false);

  // 计算是否有权限
  useEffect(() => {
    if (config === undefined) {
      setAuthorized(hasAuth(auth, type));
      return;
    }
    dynamicAuth(auth, type, config).then(setAuthorized);
  }, [auth, type, config, hasAuth, dynamicAuth]);

  // ...略
};

使用方式如下:

const Cmpt: React.FC = () => {
  // ...略

  <>
    {data.map((item) => (
      <div key={item.id}>
        <div>{item.title}</div>
        <div>
          <AuthWrapper
            auth="detail"
            config={{
              module: "demo",
              identify: item.id,
            }}
          >
            {(authorized) => (
              <Button disabled={!authorized} onClick={handleDetail}>
                详情
              </Button>
            )}
          </AuthWrapper>
        </div>
      </div>
    ))}
  </>;
};