文章
问答
冒泡
React工程认证鉴权方案
前言
绝大多数的系统都是需要登录才能访问的,因为很多信息要根据当前执行人的身份进行获取,并且不同的人可能权限也不一样。
首先,我们要区分下认证和鉴权,认证,就是确认你的身份信息,一般来说你成功登录就是通过认证。鉴权,就是鉴别你是否有权限,比如同样一个操作A可以进行,B就不能进行。
 
方案
那么在react的前端工程中,如果进行人认证和鉴权呢?我们主要考虑以下几个方案。
1.页面渲染的时候,进行认证校验,如果未认证则跳转到登录页或指定页面。
2.认证通过的标准是什么?web应用很多是无状态的,我们会在客户端通过 cookie或者sessionstorage来记录token,但是token的有效时间与服务端可能不一致,或者存在被T下线等情况。所以token的是否存在并不能作为是否登录的标准。
3.鉴权策略的实现。前端的功能权限控制,主流方案就是通过接口获取所有的权限资源,然后根据是否拥有当前资源的来判断是否拥有权限。那么,当前用户的权限策略请求在什么时间比较好?如果未登录,肯定获取不到权限策略。那么,如何让登录成功的时候触发获取权限策略请求。
4.接口未授权的处理。如果在操作过程中,token已经超时了那么请求就会报未授权异常,这个时候应该怎么处理,需要结合react单页应用的特性。
 
对于认证功能,我们需要在每个页面进行初始化的时候,进行判断,如果没有身份认证,就跳转到登录页。对于专业的需求,实现思路就是用一个认证组件对页面组件进行包裹,在挂载的时候去判断是否已登录,没有登录就执行未认证操作。对于页面的鉴权也是同样思路,在挂载的时候判断是否有权限。
一般情况下react工程的路由组件都是采用的 react-router-dom v6 ,每个页面的展示都是基于react-router-dom 的控制的。一般情况下,我们通过配置一个树形的结构来让react-router-dom进行渲染。但是官方提供的并没有认证这样的功能,所以,我们需要自己进行封装。在定义路由项的时候,需要增加是否允许匿名,是否有授权策略的配置项。所以,我们基于react-router-dom的结构进行二次封装,然后在处理的时候,再转换成react-router-dom的结构。
 
具体实现思路
这里主要分如下几个模块
  • 认证组件: 进行身份认证
  • 鉴权组件:进行权限鉴权
  • 路由组件:封装路由
认证组件 Authentication
基于react的Context实现状态的共享,在与服务端交互之后,设置状态值,让各个视图组件感知状态变化驱动事件的触发。
定义context
import {createContext} from "react";

export interface AuthContextProps {
  /**
   * 认证是否同步
   */
  authSynced?: boolean;
  /**
   * 是否已认证
   */
  authenticated?: boolean;
  /**
   * 当前用户信息
   */
  actor?: any;
  setActor?: (actor: any) => void;
  /**
   * 未认证时的回调
   */
  onUnAuthenticated?: () => void;
}

export const AuthContext = createContext<AuthContextProps>({});
定义Provider
import React, {FC, useMemo, useState} from "react";
import {AuthContext} from "./context";

type AuthProviderProps = {
    children: React.ReactElement;
    /**
     * 认证请求, 可以根据token去获取当前用户信息,如果没有token可以直接返回null
     */
    authRequest?: () => Promise<any>;
    onUnAuthenticated?: () => void;
};

export const AuthProvider: FC<AuthProviderProps> = ({children, authRequest, onUnAuthenticated}) => {
    const [authSynced, setAuthSynced] = useState(false);
    const [authenticated, setAuthenticated] = useState(false);
    const [actor, setActor] = useState<any>();

    useMemo(() => {
        if (authRequest) {
            authRequest()
                .then((res) => {
                    setAuthenticated(!!res);
                    setActor(res || null);
                })
                .catch((err) => {
                    setAuthenticated(false);
                    setActor(null);
                })
                .finally(() => {
                    setAuthSynced(true);
                });
        } else {
            setAuthSynced(true);
            setAuthenticated(false);
        }
    }, [authRequest]);


    const handleSetActor = (actor: any) => {
        setAuthenticated(!!actor)
        setAuthSynced(true)
        setActor(actor);
    };


    return (
        <AuthContext.Provider
            value={{
                authSynced,
                authenticated,
                actor,
                setActor: handleSetActor,
                onUnAuthenticated,
            }}>
            {children}
        </AuthContext.Provider>
    );
};
hooks
import {AuthContext} from "./context";
import {useContext} from "react";

export const useAuth = (): {
    authSynced?: boolean;
    authenticated?: boolean;
    actor?: any;
    setActor?: (actor: any) => void;
    onUnAuthenticated?: () => void;
} => {
    const {authSynced, authenticated, actor, setActor, onUnAuthenticated} = useContext(AuthContext);
    return {authSynced, authenticated, actor, setActor, onUnAuthenticated};
};
Authentication组件
import React, {FC, useEffect} from "react";
import {useAuth} from "./use-auth";

type AuthenticatedProps = {
    children?: React.ReactElement;
};
export const Authentication: FC<AuthenticatedProps> = ({children}) => {
    const {authSynced, authenticated, onUnAuthenticated} = useAuth();

    useEffect(() => {
        if (authSynced && !authenticated) {
            onUnAuthenticated?.(); //未认证时候触发
        }
        return () => {
        };
    }, [authSynced, authenticated]);

    return authSynced && authenticated ? <>{children}</> : null;
};
鉴权组件
鉴权模块需要结合服务端的权限体系来实现,这里按照我们的方案,是服务端会返回所有的授权策略,然后根据组件需要的权限去匹配。例如,我们一个服务端返回一个security的权限
{
    master:false,
    policies:["security"]
}
那么,如果我们某个地方需要 security 这个权限,就是可以满足的。
这里同样是基于context来实现的。
context
import React, {createContext} from "react";

export interface PermissionContextProps {
  policySynced?: boolean; //策略是否同步
  master?: boolean; //super管理员
  setMaster?: (value: boolean) => void;
  authenticate?: (policy: string | any[]) => boolean; //鉴权
  policies?: any[];
  setPolicies?: (policies: any[]) => void;
  unauthorized?: React.ReactElement | React.ReactNode; //未授权时的内容
  onUnauthorized?: () => void; //未授权时的回调
}

export const PermissionContext = createContext<PermissionContextProps>({});
Provider
import React, {FC, useEffect, useState} from "react";
import _ from "lodash";
import {PermissionContext} from "./context";
import {PolicyResponse} from "./types";
import {useAuth} from "../authentication";

type PermissionProviderProps = {
    children?: React.ReactNode;
    /**
     * 权限请求,返回一个PolicyResponse
     * @param params
     */
    permissionRequest?: (params?: any) => Promise<PolicyResponse>;
    /**
     * 权限转换,默认为拼接成字符串,例如参数是["a"],则返回"a",参数为["a","b"],则返回"a::b",当配置customAuthenticate 时,该参数无效
     * @param policy
     */
    policyTransform?: (policy: any | any[]) => any;
    /**
     * 自定义鉴权
     * @param policy 需要的权限
     * @param policies 当前用户拥有的权限策略
     */
    customAuthenticate?: (policy: any | any[], policies: any[]) => boolean;
    unauthorized?: React.ReactElement | React.ReactNode; //未授权时的内容
    onUnauthorized?: () => void; //未授权时的回调
};
export const PermissionProvider: FC<PermissionProviderProps> = ({
                                                                    children,
                                                                    permissionRequest,
                                                                    policyTransform = (policy) => {
                                                                        return _.concat([], policy).join("::");
                                                                    },
                                                                    customAuthenticate,
                                                                    unauthorized,
                                                                    onUnauthorized
                                                                }) => {
    const {authSynced, authenticated} = useAuth();
    const [policySynced, setPolicySynced] = useState(false);
    const [master, setMaster] = useState(false);
    const [policies, setPolicies] = useState<any[]>([]);

    /**
     * 鉴权,如果有自定义鉴权 customAuthenticate,则使用自定义鉴权,否则使用默认鉴权
     * @param policyFilter
     */
    const handleAuthenticate = (policyFilter: any | any[]) => {
        if (!policySynced) {
            return false;
        }
        if (customAuthenticate) {
            return customAuthenticate(policyFilter, policies);
        } else {
            let policyFiler = policyTransform(policyFilter);
            return _.some(policies, (policy: any) => {
                return policy === policyFiler;
            });
        }

    };

    useEffect(() => {
        if (authSynced && authenticated) {
            if (permissionRequest) {
                permissionRequest?.()
                    .then((res: PolicyResponse) => {
                        setMaster?.(res.master || false);
                        setPolicies?.(res.policies || []);
                    })
                    .finally(() => {
                        setPolicySynced(true);
                    });
            } else {
                setPolicySynced(true);
            }
        }
    }, [authSynced, authenticated]);

    return (
        <PermissionContext.Provider
            value={{
                policySynced,
                master,
                setMaster,
                authenticate: handleAuthenticate,
                policies,
                setPolicies,
                unauthorized,
                onUnauthorized
            }}>
            {children}
        </PermissionContext.Provider>
    );
};
hooks
import {useContext} from "react";
import {PermissionContext, PermissionContextProps} from "./context";

export const usePermission = (): PermissionContextProps => {
  return useContext(PermissionContext);
};
 
import {usePermission} from "./use-permission";

export const useAuthenticate = () => {
    const {authenticate} = usePermission();
    return authenticate;
};
Authorization
import React, {FC} from "react";
import {usePermission} from "./use-permission";

type AuthorizationProps = {
    children?: React.ReactNode;
    policy?: string | string[]; //鉴权需要的策略
    authenticate?: (policies?: any[]) => boolean; //自定义鉴权
    unauthorized?: React.ReactElement | React.ReactNode; //未授权时的内容
    onUnauthorized?: () => void; //未授权时的回调
};
export const Authorization: FC<AuthorizationProps> = ({
                                                          children,
                                                          policy = [],
                                                          authenticate,
                                                          unauthorized,
                                                          onUnauthorized
                                                      }) => {
    const permission = usePermission();

    if (!permission.policySynced) {
        return null
    } else {
        let authorized: boolean
        if (authenticate) {
            authorized = authenticate?.(permission.policies)
        } else {
            authorized = permission.authenticate?.(policy) || false
        }
        if (authorized) {
            return <>{children}</>
        } else {
            if (onUnauthorized) {
                onUnauthorized()
            } else if (permission.onUnauthorized) {
                permission.onUnauthorized()
            }
            return unauthorized ?? permission.unauthorized ?? null
        }
    }
};
 
路由组件
虽然认证和鉴权组件是可以单独使用的,但是在业务场景中,一般还是结合路由一起。比如未认证的时候,我们需要跳转到登录页,没有权限的时候我们可能要跳转到某个默认页面。这里,我们是基于react-router-dom进行了封装。
route-object
由于要增加认证,鉴权的配置,这里对原来的RouetObjec进行了扩展
import {DataRouteObject} from "react-router-dom";
import React from "react";

export type RouteItem = {
    /**
     * 路由名称
     */
    label?: string;
    id?: string;
    /**
     * 路由,可以支持根据参数生成路由
     * @param params
     */
    path?: (...params: any[]) => string;
    /**
     * 是否允许匿名访问
     */
    anonymous?: boolean;
    /**
     * 权限策略
     */
    policy?: string | string[];
    /**
     * 未授权时的内容
     */
    unauthorized?: React.ReactElement | React.ReactNode; //未授权时的内容
    /**
     * 未授权时的回调
     */
    onUnauthorized?: () => void; //未授权时的回调
};

export type TrionesRouteObject = Omit<DataRouteObject, "id" | "path" | "children"> &
    RouteItem & {
    children?: TrionesRouteObject[];
};
由于我们自己定义的路由对象是不能直接用于 react-router-dom的,这里我们还需要对配置进行转换。
import {TrionesRouteObject} from "./route-object";
import {createBrowserRouter, createHashRouter, RouteObject} from "react-router-dom";
import {Authorization} from "../permission";
import {Authentication} from "../authentication";

export const routesConvert = (routes: TrionesRouteObject[]): RouteObject[] => {
    return routes.map((route: any) => {
        if (typeof route.path == "function") {
            route.path = route.path();
        }
        if (route.policy) {
            if (route.element) {
                route.element = <Authorization policy={route.policy} unauthorized={route.unauthorized}
                                               onUnauthorized={route.onUnauthorized}>{route.element}</Authorization>;
            }
            if (route.Component) {
                let Component = route.Component;
                route.Component = () => (
                    <Authorization policy={route.policy} unauthorized={route.unauthorized}
                                   onUnauthorized={route.onUnauthorized}>
                        <Component/>
                    </Authorization>
                );
            }
        }

        if (!route.anonymous) {
            if (route.element) {
                route.element = <Authentication>{route.element}</Authentication>;
            }
            if (route.Component) {
                let Component = route.Component;
                route.Component = () => (
                    <Authentication>
                        <Component/>
                    </Authentication>
                );
            }
        }

        if (route.children) {
            route.children = routesConvert(route.children);
        }
        return route as RouteObject;
    });
};

export const trionesCreateBrowserRouter = (routes: TrionesRouteObject[]) => {
    return createBrowserRouter(routesConvert(routes));
};

export const trionesCreateHashRouter = (routes: TrionesRouteObject[]) => {
    return createHashRouter(routesConvert(routes));
};
为了让用户有更友好的用户体验,我们将对象名转为了react-router-dom中原来的名称。
import {trionesCreateBrowserRouter, trionesCreateHashRouter} from "./hooks";
import {RouteItem, TrionesRouteObject} from "./route-object";
import {RouterProvider, Outlet,Link, useNavigate} from "react-router-dom"

export type {RouteItem, TrionesRouteObject as RouteObject};
export {
    trionesCreateBrowserRouter as createBrowserRouter,
    trionesCreateHashRouter as createHashRouter,
    RouterProvider,
    Outlet,
    Link,
    useNavigate
};
 
效果测试
我们用3个页面来进行测试
  • 登录页,可以匿名访问
  • dashboard ,只需要认证即可
  • security,需要有 security权限才可以访问
SignInPage
import {useAuth, useNavigate} from "@trionesdev/commons-react";

export const SignInPage = () => {
    const navigate = useNavigate()
    const {setActor} = useAuth();
    return <div>
        <h1 style={{
            textAlign: "center"
        }}>登录页</h1>
        <div style={{textAlign: "center"}}>
            <button onClick={() => {
                setActor?.({"id": "1", "username": "admin"})
                navigate("/")
            }}>进入
            </button>
        </div>
    </div>
}
DashboardPage
import {Link, useAuth} from "@trionesdev/commons-react";

export const DashboardPage = () => {
    const {actor} = useAuth()
    return <div>
        <h1>Dashboard</h1>
        <div>当前用户:{actor.username}</div>
        <Link to={"/security"}>Security</Link>
    </div>
}
SecurityPage
export const SecurityPage = () => {
    return <div>
        <div style={{textAlign: "center"}}>
            <h1>Security Page</h1>
        </div>
    </div>
}
路由如下
import {createHashRouter, RouteObject, RouterProvider} from "@trionesdev/commons-react";
import {SignInPage} from "./SignInPage";
import {DashboardPage} from "./DashboardPage";
import {SecurityPage} from "./SecurityPage";

export const routes: RouteObject[] = [
    {
        path: () => "/sign-in",
        element: <SignInPage/>,
        anonymous: true
    },
    {
        path: () => "/",
        element: <DashboardPage/>,
    },
    {
        path: () => "/security",
        element: <SecurityPage/>,
        policy: "security",
        unauthorized: <div>未授权</div>,
    }
]

export const AppRouter = () => {
    return <RouterProvider router={createHashRouter(routes)}/>;
};
入口文件
import React from 'react';
import './App.css';
import {AppRouter} from "./router";
import {AuthProvider, PermissionProvider} from "@trionesdev/commons-react";

function App() {
    return (
        <div>
            <AuthProvider authRequest={() => Promise.resolve(false)} onUnAuthenticated={() => {
                console.log("onUnAuthenticated");
                window.location.href = "/#/sign-in";
            }}>
                <PermissionProvider permissionRequest={() => Promise.resolve({master: false, policies: []})}>
                    <AppRouter/>
                </PermissionProvider>
            </AuthProvider>
        </div>
    );
}

export default App;
直接请求根目录,就会因为没有登录而跳转到登录页
点击进入按钮,会设置当前用户信息,然后跳转到Dashboard页面
点击Security链接,跳转到Security页面,由于当前用户没有security权限,则显示未授权
修改代码,设置用户有security权限
import React from 'react';
import './App.css';
import {AppRouter} from "./router";
import {AuthProvider, PermissionProvider} from "@trionesdev/commons-react";

function App() {
    return (
        <div>
            <AuthProvider authRequest={() => Promise.resolve(false)} onUnAuthenticated={() => {
                console.log("onUnAuthenticated");
                window.location.href = "/#/sign-in";
            }}>
                <PermissionProvider permissionRequest={() => Promise.resolve({master: false, policies: ["security"]})}>
                    <AppRouter/>
                </PermissionProvider>
            </AuthProvider>
        </div>
    );
}

export default App;
再尝试,页面可以展示
react
react-router-dom

关于作者

落雁沙
非典型码农
获得点赞
文章被阅读