问题描述
我正在为服务器使用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返回主应用程序组件。