如何处理对经过身份验证的 API 的同时请求,在获取新的访问令牌时暂停除第一个之外的所有请求?

ana*_*ous 7 javascript reactjs next.js

对于这个问题,我将声明我正在使用 JWT 令牌。这些令牌的复杂性和处理效果非常好。有刷新令牌和访问令牌,对于这个问题,我们只需要担心如何以及何时发送检索新访问令牌的请求。

我的应用程序(NextJS)正在请求检索数据。端点需要身份验证令牌。该令牌及其到期日期存储在内存中(React 状态,我构建了一个自定义挂钩(useAuth),并且其状态在我的应用程序中全局可用。

当发出请求时,我们检查过期时间,如果令牌被视为无效(过期日期已过),我们会在继续获取请求之前请求一个新令牌。

如果在没有有效令牌的情况下尝试端点,我们会得到 false 的返回值。

当存在一个提取请求时,此过程效果很好,而我遇到的问题(因此这个问题)是我是否几乎同时发生多个提取请求。虽然它可以工作,但每个请求都希望检索一个新的访问令牌,而当一个访问令牌就足够时,我们最终会收到太多的访问令牌请求。

理想情况下,我希望第一个请求检索访问令牌(如果需要),而其他获取则等到新的访问令牌获得授予后再继续。

是时候看一些代码示例了

对于获取请求,我使用 useSWR ( https://swr.vercel.app/ )。这是一个例子。

const auth = useAuth();

// Example of ONE fetch (multiply this and use different endpoints, variable deconstruction etc)

const {
        data: venue,
        error: venueError,
        mutate: venueMutate,
    } = useSWR(auth?.isLoggedIn ? ['/api/venues/getVenue', venueUUID] : null, (url, venueUUID) =>
        auth.fetchWithUUID(url, venueUUID)
    );
Run Code Online (Sandbox Code Playgroud)

您无需关心 auth?.isLoggedIn 或 VenueUUID 等变量;知道这个(这些)请求正在发生。

该函数 - auth.fetchWithUUID() - 看起来像这样;

const fetchWithUUID = async (url, uuid, payload = {}) => {
    if (!ref?.current?.accessToken?.expiry || ref?.current?.accessToken?.expiry < +new Date()) {
        console.log('new call');
        await retrieveAccessToken('/api/getAccessToken', true);

        let apiResponse = await fetch_retry(url, uuid, payload, 3);
        return apiResponse?.res;
    } else {
        let apiResponse = await fetch_retry(url, uuid, payload, 3);
        return apiResponse?.res;
    }
};
Run Code Online (Sandbox Code Playgroud)

ref?.current允许我们访问当前状态(https://www.npmjs.com/package/react-usestateref

函数-retrieveAccessToken() -看起来像这样;

const retrieveAccessToken = async (url, override = false) => {
    if (!isLoggedIn || override === true) {
        let apiResponse = await fetch(url, {
            method: 'GET',
        }).then((res) => {
            if (res.status >= 300 && res.status != 401) throw new Error('API Client error');
            return res.json();
        });

        console.log('setting access token');

        setUser(apiResponse?.user);

        return isLoggedInRef.current === true ? true : false;
    }

    return null;
};
Run Code Online (Sandbox Code Playgroud)

最后,fetch_retry()是这样的;

const fetch_retry = async (url, uuid, payload, n) => {
    try {
        let thisRes = await fetch(url, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                Authorization: `Bearer ${ref?.current?.accessToken?.token}`,
            },
            body: JSON.stringify({
                UUID: uuid,
                ...payload,
            }),
        }).then((res) => {
            if (res.status >= 300 && res.status != 401) throw new Error('API Client error');
            return res.json();
        });

        if (thisRes === false) {
            if (n === 1) return false;

            return await fetch_retry(url, uuid, payload, n - 1);
        } else {
            return thisRes;
        }
    } catch (err) {
        if (n === 1) throw err;
        return await fetch_retry(url, uuid, payload, n - 1);
    }
};
Run Code Online (Sandbox Code Playgroud)

在当前配置中,我按照预期从获取中获取了所有结果。其他端点的行为方式相同;他们获取不同的数据。但它们都请求访问令牌,并且它们发生的时间相隔几毫秒。

expiry 1632450789126
date now - 1632450800173
new call

expiry 1632450789126
date now 1632450800177
new call

expiry 1632450789126
date now 1632450800180
new call

expiry 1632450789126
date now 1632450800182
new call

Run Code Online (Sandbox Code Playgroud)

那么,谁能带领我走上新的道路呢?:) 我需要创建一个排队系统吗?

小智 2

你可以使用像这样的信号量

const tokenRequest = useRef(false);
function checkTokenRequest() {
    return new Promise(function(resolve, reject) {
        (function waitForTokenRequest() {
            if (!tokenRequest.current) return resolve();
            setTimeout(waitForTokenRequest, 30);
        })();
    });
}

const retrieveAccessToken = async (url, override = false) => {
    if (!isLoggedIn || override === true) {
        if (tokenRequest.current) {
            await checkTokenRequest();
        } else {
            tokenRequest.current = true;

            let apiResponse = await fetch(url, {
                method: 'GET'
            }).then((res) => {
                if (res.status >= 300 && res.status != 401) throw new Error('API Client error');
                return res.json();
            });

            console.log('setting access token');

            setUser(apiResponse?.user);

            tokenRequest.current = false;
        }

        return isLoggedInRef.current === true ? true : false;
    }

    return null;
};
Run Code Online (Sandbox Code Playgroud)

因此,不会同时发起超过一个令牌请求

这里的最小示例https://codesandbox.io/s/white-paper-lp7vg

等待函数的更多示例可以在这里找到