如果 Firebase 令牌在 React 应用程序中过期,如何不调用任何 api 端点?

Posted

技术标签:

【中文标题】如果 Firebase 令牌在 React 应用程序中过期,如何不调用任何 api 端点?【英文标题】:How to not call any api endpoints if firebase token is expired in React app? 【发布时间】:2021-08-22 06:02:52 【问题描述】:

我在 React 应用程序中使用 firebase-ui-react 进行身份验证,但问题是,如果有人打开应用程序并且在一个小时内没有进行任何活动,并且在一小时后单击一个链接,那么所有端点被多次调用,直到应用程序崩溃。 index.jsx 看起来像这样:

import React from "react";
import ReactDOM from "react-dom";
import  BrowserRouter, Route  from "react-router-dom";
import  Provider  from "react-redux";
import * as Sentry from "@sentry/react";
import store from "./Redux/store";
import App from "./App";
import ReactGA from "react-ga";
import  AuthProvider  from "./Containers/Auth/AuthProvider";
import * as firebaseconfig from "./deployment_config.json";

const fbconfig =
  firebaseconfig.default[process.env.REACT_APP_ENV_NAME][
    process.env.REACT_APP_CLIENT
  ];

Sentry.init( dsn: fbconfig.SENTRY_DSN );
ReactGA.initialize(fbconfig.GOOGLE_ANALYTICS_TRACKING);

ReactDOM.render(
  <BrowserRouter>
    <AuthProvider fbconfig=fbconfig>
      <Provider store=store>
        <App />
      </Provider>
    </AuthProvider>
  </BrowserRouter>,
  document.getElementById("root")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>

App.jsx 看起来像:

import React,  useState, useContext, useEffect  from "react";
import  AuthContext  from "./Containers/Auth/AuthProvider";
import  withRouter  from "react-router-dom";
import  Provider, useDispatch, useSelector  from "react-redux";
//lots of other imports


const App = (props) => 
  const  location  = props;
  const  user, accessTokenExpired, loading, userRole, uiConfig  = useContext(
    AuthContext
  );

  toast.configure();
  const [checkSession, setCheckSession] = useState(false);
  const [modal, setModal] = useState(true);
  const change_body = useSelector((state) => state.Common.change_body);
  const ErrorStatusState = useSelector(
    (state) => state.GetGlobalError.ErrorCode
  );
  const configdata = useSelector((state) => state.GetConfig.data);

  const dispatch = useDispatch();

  useEffect(() => 
    dispatch(GetConfig());
  , []);

  axios.interceptors.response.use(
    (response) => response,
    (error) => 

      if (error.message === "Network Error") 
        setCheckSession(true);
      

      return Promise.reject(error);
    
  );

  if (loading) 
    return <PageLoader />;
  

  if (user === null) 
    return <Login uiConfig=uiConfig user=user userRole=userRole />;
  

  if (userRole === null) 
    return <Verification />;
  

  if (ErrorStatusState > 0 || configdata.maintenance) 
    return (
      <>
        <Provider store=store>
          <GlobalStyle />
          <Header />
          <ContentWrapper>
            <MainContentWrapper>
              <AppWrapper>
                <PageWrapper>
                  <MainWrapper>
                    <ErrorClassifyRender
                      ErrorStatus=ErrorStatusState
                      maintenance=configdata.maintenance
                    />
                  </MainWrapper>
                </PageWrapper>
              </AppWrapper>
            </MainContentWrapper>
          </ContentWrapper>
        </Provider>
      </>
    );
  

  return (
    <Provider store=store>
      <GlobalStyle />
      <Header />
      <ContentWrapper>
        <MainContentWrapper>
          <Navigation />
          <AppWrapper>
            <MainApp id="appDiv">
            

              <PageWrapper>
                <MainWrapper>
                  <Routes />
                </MainWrapper>

              </PageWrapper>
            </MainApp>
          </AppWrapper>
        </MainContentWrapper>
      </ContentWrapper>
    </Provider>
  );
;

export default withRouter(App);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>

AuthProvider 看起来像:

import  useEffect, createContext, useState  from "react";
import  isAfter  from "date-fns";
import firebase from "firebase/app";
import axios from "axios";

export const AuthContext = createContext();

export const AuthProvider = ( children, fbconfig ) => 
  const baseURL = process.env.REACT_APP_BACKEND_URL || window.location.origin;

  const [user, setUser] = useState(null);
  const [userRole, setUserRole] = useState(null);
  const [isEmailVerified, setIsEmailVerified] = useState(false);
  const [loading, setLoading] = useState(true);
  const [verificationLoading, setVerificationLoading] = useState(false);
  const [verifyRequestBlocked, setVerifyRequestBlocked] = useState(false);
  const [emailSent, setEmailSent] = useState(false);
  const [isNewUser, setIsNewUser] = useState(false);
  const [accessTokenExpired, setAccessTokenExpired] = useState(false);

  // Configure Firebase.
  const config = 
    apiKey: fbconfig.FIREABSE_API_KEY,
    authDomain: fbconfig.FIREABSE_AUTH_DOMAIN,
    projectId: fbconfig.FIREABSE_PROJECT_ID,
  ;

  // Configure FirebaseUI.
  if (!firebase.apps.length) 
    firebase.initializeApp(config);
  

  const sendEmail = async ( email, isNewUser = false ) => 
    // setLoading(true);
    setVerificationLoading(true);

    await axios
      .post(`$baseURL/send_email_verification`, 
        email,
        link: window.location.origin,
      )
      .then((response) => 
        if (response.data.error) 
          setUser(response);
          setEmailSent(true);
          setVerifyRequestBlocked(true);
          setVerificationLoading(false);
         else 
          setUser(response);
          setEmailSent(true);
          setVerificationLoading(false);
        
      )
      .catch((error) => 
        setVerifyRequestBlocked(true);
        setUser( data:  email: email  );
        setVerificationLoading(false);
        setEmailSent(true);
      );

    setIsNewUser(isNewUser);
  ;

  const uiConfig = 
    signInFlow: "popup",
    signInOptions: [
      firebase.auth.GoogleAuthProvider.PROVIDER_ID,
      firebase.auth.EmailAuthProvider.PROVIDER_ID,
    ],
    signInSuccessUrl: "/",
    callbacks: 
      signInSuccessWithAuthResult: async (authResult) => 
        if (authResult.additionalUserInfo.isNewUser) 
          await sendEmail(
            isNewUser: true,
            email: authResult.user.email,
          );
         else 
          setIsNewUser(false);
          setLoading(false);
        

        return false;
      ,
    ,
  ;

  useEffect(() => 
    firebase.auth().onAuthStateChanged(async (user) => 
      setLoading(true);
      if (user === null) 
        setUser(null);
        setLoading(false);
        return;
      

      if (user.emailVerified) 
        setIsEmailVerified(user.emailVerified);
        await user.getIdToken().then(async (accessToken) => 
          setLoading(true);
          try 
            const result = await axios.post(
              `$baseURL/user`,
              
                token: accessToken,
              ,
               headers:  Authorization: `Bearer $accessToken`  
            );

            const expirationTimeInSeconds =
              JSON.parse(atob(accessToken.split(".")[1])).exp * 1000;

            const dateResult = isAfter(
              new Date(),
              new Date(expirationTimeInSeconds)
            );

            if (dateResult) 
              setAccessTokenExpired(true);
            

            localStorage.setItem("token", accessToken);
            localStorage.setItem("user", JSON.stringify(result.data));

            setUser(
              ...result.data,
              token: accessToken,
              status: result.status,
            );
            setUserRole(result.data.role);
            setLoading(false);
           catch (error) 
            setUser(null);
            setLoading(false);
          
        );
       else 
        setUser( data: user );
        setLoading(false);
      
    );
  , []);

  return (
    <AuthContext.Provider
      value=
        user,
        accessTokenExpired,
        emailSent,
        verifyRequestBlocked,
        isEmailVerified,
        loading,
        userRole,
        uiConfig,
        isNewUser,
        verificationLoading,
        sendEmail,
      
    >
      children
    </AuthContext.Provider>
  );
;
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>

其中一个端点调用如下:

import React,  useRef, useState, useEffect  from "react";
import styled from "styled-components";
import  useSelector, useDispatch  from "react-redux";
import ReactEcharts from "echarts-for-react";
import 
  fetchHistory,
 from "../../Redux";


const History = (props) => 
  const barRef = useRef();

  const 
    brand,
    parentStartDate,
    parentEndDate,
    start_date,
    end_date,
    selectedId,
    tabID,
    showTabs,
    showComponent,
    detailed,
    graphs,
    tooltipExtra,
   = props;

  const predict_data_response = useSelector((state) => state.History.predict_data);


  useEffect(() => 
      dispatch(
        fetchHistoryroi(
          brand: brand.name,
          parentStartDate,
          parentEndDate,
          componentName,
          tabID,
          nestedObjects,
        )
      );
  , [parentStartDate, parentEndDate]);




  const predict_data = ResponseFilter( responseFilterProps );


  const options = 
    toolbox: getToolBox(),
    textStyle: 
      color: colors.white,
    ,
    tooltip: 
      formatter: (params) => 
        return getTooltip(
          params,
          avarage,
          devider,
          componentName: "History",
        );
      ,
      backgroundColor: colors.cardBg,
      extraCssText: extraCssText,
    ,
    grid: 
      left: "7px",
      right: "7px",
      bottom: "3%",
      containLabel: true,
    ,
    xAxis: [
      
        axisLabel: 
          fontSize: 10,
        ,
        type: "category",
        data: labels,
        axisTick: 
          alignWithLabel: true,
        ,
      ,
      
        axisLabel: 
          fontSize: 10,
        ,
        position: "bottom",
        offset: 15,
        axisLine: 
          show: false,
        ,
        axisTick: 
          show: false,
        ,
        data: labelsValues,
      ,
    ],
    yAxis: [
      
        axisLabel: 
          fontSize: 10,
          formatter: (value, index) => 
            return value;
          ,
        ,
        splitLine: 
          lineStyle: 
            color: colors.lightGray,
          ,
        ,
        type: "value",
      ,
    ],
    series: [
      
        type: "bar",
        label: 
          show: true,
          position: ["30%", "30%"],
          formatter: ( data ) => 
            const  value  = data;
            return value === avarage / devider && avarage !== 0 ? "NA" : "";
          ,
        ,
        barMaxWidth: 40,
        data: nums,
      ,
    ],
  ;

  return (
    <ContainerWrapper>
     <ReactEcharts
       ref=barRef
       option=options
       data-test="HistoryGraph"/>      
    </ContainerWrapper>
  );
;

export default History;
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>

这是关于一个小时不活动后有人单击链接时发生的情况的图像

请帮忙?

【问题讨论】:

【参考方案1】:

Firebase SDK 不直接解析访问令牌,而是提供User#getIdTokenResult(),它提供令牌的元数据以及令牌本身。

const  token, expirationTime  = await firebase.auth().currentUser.getIdTokenResult();

与其将访问令牌存储在您传递给setUser() 的对象中,更好的选择是添加一个Axios 请求拦截器,就像您为响应所做的那样。这将允许您像往常一样使用 Axios 触发请求,但您不必在每个请求上添加 headers: Authorization: `Bearer $accessToken`

以下useEffect() 在挂载时附加一个拦截器,在卸载时将其分离,其中拦截器将用户的最新 ID 令牌附加到请求中。在这里,我还添加了一个子句,如果令牌在接下来的 5 分钟内到期,则会强制刷新。

useEffect(() => 
  const authRequestInterceptor = axios.interceptors.request.use(async (config) 
    if (!config.url.startsWith(baseURL))
      return config; // this request doesn't target our API, leave untouched

    const currentUser = firebase.auth().currentUser;
    if (currentUser === null)
      return config; // no user signed in, return config unmodified

    // getIdTokenResult provides token metadata in the result
    // will return current token, unless it's expired where a fresh token is grabbed
    let  token, expirationTime  = await currentUser.getIdTokenResult();

    const expiresMS = (new Date(expirationTime)).getTime();
    if (expiresMS - Date.now() < 300000) 
      // if the token will expire in the next 5 minutes, 
      // forcefully grab a fresh token
      token = await currentUser.getIdToken(true);
    

    // Attach the token to the request
    config.headers.Authorization = `Bearer $token`;

    return config;
  );

  return () => axios.interceptors.request.eject(authRequestInterceptor);
, []);

注意:您应该对响应拦截器使用相同的useEffect() 策略,并且应该使用return firebase.auth().onAuthStateChanged(...),以便onAuthStateChanged 返回的清理函数可以被其useEffect() 访问。

【讨论】:

可能是 AuthProvider,因为它与 Auth 相关。老实说,你可以把它放在任何你想要的地方。

以上是关于如果 Firebase 令牌在 React 应用程序中过期,如何不调用任何 api 端点?的主要内容,如果未能解决你的问题,请参考以下文章

用户注销 React Native 应用程序时如何删除 Firebase 云消息传递令牌?

如何在单个 Firebase 数据库中为多个应用构建推送令牌

Firebase React Native——如何获取和传输设备 ID/令牌?

使用 react-native-firebase 在 React Native 上自定义通知

使用@react-native-firebase/messaging 收到通知后应用程序崩溃

Firebase 推送通知 - 如何跟踪用户 FCM 令牌?