我正在使用Spotify Web API为一个简单的React应用程序实现授权代码流,该应用程序使用Node Express作为服务器,但我不知道如何将身份验证凭据从服务器传递到客户端。
我使用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登录,输入他们的信息,我最终得到一个访问令牌和刷新令牌。这一切都有效。
使用访问令牌和刷新令牌,我可以将用户重定向到具有这些参数的URL(例如/#/user/${access_token}/${refresh_token}
(,但我不知道如何将这些参数放入我的授权上下文中。请注意,从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将返回主应用程序组件。