如果 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 上自定义通知