如何处理从Node Express重定向到React Client的身份验证react-router-dom和useContext

问题描述

我正在为服务器使用Node Express的简单React应用程序使用Spotify Web API实现授权代码流,并且无法弄清楚如何将身份验证凭据从服务器传递到客户端。

我正在使用React的useContext钩子来存储授权凭证。

import React,{ createContext,useState,useEffect } from "react";

// the shape of the default value must match
// the shape that consumers expect
// auth is an object,setAuthData is a function
export const AuthContext = createContext({
  auth: {},setAuthData: () => {},});

const AuthProvider = ({ children }) => {
  const [auth,setAuth] = useState({ loading: true,data: null });

  const setAuthData = (data) => {
    setAuth({ data: data });
  };

  // on component mount,set the authorization data to
  // what is found in local storage
  useEffect(() => {
    setAuth({
      loading: false,data: JSON.parse(window.localStorage.getItem("authData")),});
    return () => console.log("AuthProvider...");
  },[]);

  // when authorization data changes,update the local storage
  useEffect(() => {
    window.localStorage.setItem("authData",JSON.stringify(auth.data));
  },[auth.data]);

  return (
    <AuthContext.Provider value={{ auth,setAuthData: setAuthData }}>
      {children}
    </AuthContext.Provider>
  );
};

export default AuthProvider;

在index.js内部,我已将我的应用包装在AuthProvider中

import ReactDOM from "react-dom";

import AuthProvider from "./contexts/AuthContext";
import App from "./Components/App/AppRouter";

import * as serviceWorker from "./serviceWorker";

ReactDOM.render(
  <React.StrictMode>
    <AuthProvider>
      <App />
    </AuthProvider>
  </React.StrictMode>,document.getElementById("root")
);

在该应用程序中,我正在使用react-router-dom来管理路由和受保护的路由。

// External libraries
import React from "react";
import {
  browserRouter as Router,Switch,Route,} from "react-router-dom";

import { AuthContext } from "../../contexts/AuthContext";

// Components
import { PrivateRoute } from "../PrivateRoute/PrivateRoute";

function AppRouter(props) {
  return (
    <Router>
      <Switch>
        <Route path="/login" component={Login} />
        <PrivateRoute path="/" component={AppLite} />
      </Switch>
    </Router>
  );
}

在PrivateRoute内部,我允许根据身份验证上下文中的内容访问路由。

import React,{ useContext } from "react";
import { Route,Redirect } from "react-router-dom";

import { AuthContext } from "../../contexts/AuthContext";

import Layout from "../App/Layout";

export const PrivateRoute = ({ component: Component,...rest }) => {
  const { auth,setAuthData } = useContext(AuthContext);

  // if loading is set to true,render loading text
  if (auth.loading) {
    return (
      <Route
        render={() => {
          return (
            <Layout>
              <h2>Loading...</h2>
            </Layout>
          );
        }}
      />
    );
  }

  // if the user has authorization render the component,// otherwise redirect to the login screen
  return auth.data ? <Component /> : <Redirect to="/login" />;
};

用户转到主页时,他们将被重定向登录屏幕。在那里,链接用户重定向到/ api / login。 / api路由被代理到节点服务器,/ api / login启动Spotify授权调用。将用户定向到Spotify登录名,输入他们的信息,然后我得到一个访问令牌和刷新令牌。一切正常。

使用访问令牌和刷新令牌,我可以将用户重定向到具有这些参数(例如/#/user/${access_token}/${refresh_token})的URL,但我不知如何将这些参数获取到授权上下文中。请注意,从URL获取令牌不是问题。

我试图做的是在我的PrivateRoute中添加一个useEffect,它从URL获取参数,然后在发现授权上下文后对其进行更新。

const { auth,setAuthData } = useContext(AuthContext);
  const location = useLocation();

  // on mounting the component,check the URL for
  // authentication data,if it is present,set
  // it on the authorization context
  useEffect(() => {
    let authData = getHashParams(location);

    authData && setAuthData(authData);

    return () => {
      authData = null;
    };
  },[location,setAuthData]);

但是,这会使事情陷入无限循环。我似乎只有在由onClick事件触发时才能成功使用setAuthData。我应该如何拦截来自api路由器的重定向,以便可以在授权上下文中更新数据,然后转到PrivateRoute?

或者,有没有一种方法可以将我的所有api路由器逻辑封装在onClick事件中,并从中获取最终响应(例如fetch("/api/login")....user gets redirected,fills in info,exchange code for tokens,send tokens back as response...then((response) => setAuthData(response)...)?

解决方法

这里的核心问题是我试图从子组件的useEffect回调中更新父组件(AuthProvider)的状态。尽管useContext使得可以从子组件更新父组件的状态,但是使用useEffect这样做会触发无限循环,因为setState调用成为useEffect的依赖项。

从AppRouter中删除身份验证,然后将URL身份验证检查移至AuthProvider本身即可解决此问题。

在index.js内,我对组件包装重新排序:

import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter as Router } from "react-router-dom";
import AuthProvider from "./contexts/AuthContext";
import App from "./Components/App/AppRouter";
import * as serviceWorker from "./serviceWorker";

ReactDOM.render(
  <React.StrictMode>
    <Router>
      <AuthProvider>
        <App />
      </AuthProvider>
    </Router>
  </React.StrictMode>,document.getElementById("root")
);

在AuthContext中,我正在使用react-router-dom中的useLocation来检查URL的更改并在发生更改时更新身份验证数据。

import React,{ createContext,useState,useEffect } from "react";
import { useHistory,useLocation } from "react-router-dom";
import { getHashParams } from "../util/auth";

export const AuthContext = createContext([{},() => {}]);

const AuthProvider = (props) => {
  const [auth,setAuth] = useState({ loading: true,data: null });
  const location = useLocation();
  const history = useHistory();

  const setAuthData = (data) => {
    setAuth((state) => ({
      ...state,data: data,}));
  };

  // on component mount,set the authorization data to
  // what is found in local storage
  useEffect(() => {
    setAuth({
      loading: false,data: () => {
        try {
          return JSON.parse(window.localStorage.getItem("authData"));
        } catch (err) {
          console.error(err.message);
          return null;
        }
      },});
  },[]);

  // check the URL for tokens whenever it is updated
  // if authentication tokens are found,update the
  // authentication data and redirect
  useEffect(() => {
    const checkTokens = () => {
      // get authentication tokens from the URL
      // returns null if no hash with parameters in URL
      let tokens = getHashParams(location);
      if (tokens && tokens.path === "error") {
        alert("There was an error during authentication.");
      } else if (tokens) {
        setAuthData(tokens);
        history.replace("/");
      }
    };
    checkTokens();
  },[location,history]);

  // when authorization data changes,update the local storage
  useEffect(() => {
    window.localStorage.setItem("authData",JSON.stringify(auth.data));
  },[auth.data]);

  return (
    <AuthContext.Provider value={[auth,setAuthData]}>
      {props.children}
    </AuthContext.Provider>
  );
};

export default AuthProvider;

AppRouter很简单。

function AppRouter(props) {
  const [auth] = useContext(AuthContext);

  return (
    <Switch>
      <PrivateRoute exact path="/" component={App} auth={auth} />
      <Route path="/login" component={Login} />
    </Switch>
  );
}

PrivateRoute只是作为身份验证接收身份验证数据,然后呈现组件或重定向到登录页面。

import React from "react";
import { Route,Redirect } from "react-router-dom";
import Layout from "../App/Layout";

export const PrivateRoute = ({ component: Component,auth,...rest }) => {
  // if loading is set to true,render loading text
  if (auth.loading) {
    return (
      <Route
        render={() => {
          return (
            <Layout>
              <h2>Loading...</h2>
            </Layout>
          );
        }}
      />
    );
  }

  // if the user has authorization render the component,// otherwise redirect to the login screen
  return auth.data ? <Component auth={auth.data} /> : <Redirect to="/login" />;
};

现在,当从回调端点重定向用户时,访问和刷新令牌将在URL中更新,并且AuthContext得到更新。随着身份验证数据的更新,PrivateRoute返回主应用程序组件。