2

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

我正在使用 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)...)?

4

1 回答 1

2

这里的核心问题是我试图从子组件的 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 返回主应用程序组件。

于 2020-11-16T15:22:00.570 回答