您好,我正在尝试将 Auth0 的 spa 示例与 react 一起使用,并且我正在使用 useAuth0 钩子,我也使用 Apollo 客户端进行查询,我需要获取令牌并在请求中设置它,但是我无法设置它。
如果有人能为我指出正确的方向,我将不胜感激。
我尝试在查询/突变组件中使用上下文属性,但我无法弄清楚,也找不到有关如何使用它的信息。
我遇到了同样的困境,特别是因为 Auth0 钩子只能在功能组件中使用,但文档似乎在索引文件中设置了 ApolloProvider。
通过一些实验,我设法通过创建一个包装器组件来解决这个问题,该组件允许我使用useAuth0
钩子并将令牌异步获取/附加到每个请求。
我创建了一个新文件AuthorizedApolloProvider.tsx
:
import { ApolloClient, ApolloProvider, createHttpLink, InMemoryCache } from '@apollo/client';
import { setContext } from '@apollo/link-context';
import React from 'react';
import { useAuth0 } from '../react-auth0-spa';
const AuthorizedApolloProvider = ({ children }) => {
const { getTokenSilently } = useAuth0();
const httpLink = createHttpLink({
uri: 'http://localhost:4000/graphql', // your URI here...
});
const authLink = setContext(async () => {
const token = await getTokenSilently();
return {
headers: {
Authorization: `Bearer ${token}`
}
};
});
const apolloClient = new ApolloClient({
link: authLink.concat(httpLink),
cache: new InMemoryCache(),
connectToDevTools: true
});
return (
<ApolloProvider client={apolloClient}>
{children}
</ApolloProvider>
);
};
export default AuthorizedApolloProvider;
然后在我的 index.tsx 文件中,我用我的新AuthorizedApolloProvider
包装App
,而不是直接使用ApolloProvider
。
ReactDOM.render(
<Auth0Provider
domain={config.domain}
client_id={config.clientId}
redirect_uri={window.location.origin}
audience={config.audience}
onRedirectCallback={onRedirectCallback}>
<AuthorizedApolloProvider>
<App />
</AuthorizedApolloProvider>
</Auth0Provider>,
document.getElementById('root')
);
注意:上面的例子使用的是阿波罗客户端 3 测试版,除了@apollo/client
之外,我还必须安装@apollo/link-context
。我想阿波罗客户端版本所需的导入可能不同。
我解决这个问题的方法是编辑我在网上找到的一篇文章 https://hasura.io/
换句话说,它使用 react 的useContext()
钩子和useEffect()
来检查并获取 jwt 令牌,使用 auth0 的getTokenSilently()
函数。
我将只写相关的部分:
import React, { FC, ReactNode } from 'react'
import { useAuth0 } from '@auth0/auth0-react'
import { ApolloProvider } from 'react-apollo'
import { ApolloClient, HttpLink, InMemoryCache } from 'apollo-boost'
import { setContext } from 'apollo-link-context'
import { useState, useEffect } from 'react'
const httpLink = new HttpLink({
uri: 'yourdomain.test/graphql',
})
const Page: FC<{}> = ({children }) => {
const [accessToken, setAccessToken] = useState('')
const [client, setClient] = useState() as [ApolloClient<any>, any] // that could be better, actually if you have suggestions they are welcome
const { getAccessTokenSilently, isLoading } = useAuth0()
// get access token
useEffect(() => {
const getAccessToken = async () => {
try {
const token = await getAccessTokenSilently()
setAccessToken(token)
} catch (e) {
console.log(e)
}
}
getAccessToken()
}, [])
useEffect(() => {
const authLink = setContext((_, { headers }) => {
const token = accessToken
if (token) {
return {
headers: {
...headers,
authorization: `Bearer ${token}`,
},
}
} else {
return {
headers: {
...headers,
},
}
}
})
const client = new ApolloClient({
link: authLink.concat(httpLink),
cache: new InMemoryCache(),
})
setClient(client)
}, [accessToken])
if (!client) {
return <h1>Loading...</h1>
}
return (
<ApolloProvider client={client}>
{...children}
</ApolloProvider>
)
}
主要问题是 react hook 不能在函数组件之外使用。但是,ApolloClient 的初始化发生在组件外部,它需要访问令牌来调用后端 graphql API,这可以通过调用getTokenSilently()
方法来实现。为了解决这个问题,我手动导出了getTokenSilently()
方法(在Auth0Provider之外(。
例如:
import React, { useState, useEffect, useContext } from "react";
import createAuth0Client from "@auth0/auth0-spa-js";
const DEFAULT_REDIRECT_CALLBACK = () =>
window.history.replaceState({}, document.title, window.location.pathname);
export const Auth0Context = React.createContext();
export const useAuth0 = () => useContext(Auth0Context);
let _initOptions, _client
const getAuth0Client = () => {
return new Promise(async (resolve, reject) => {
let client
if (!client) {
try {
client = await createAuth0Client(_initOptions)
resolve(client)
} catch (e) {
console.log(e);
reject(new Error('getAuth0Client Error', e))
}
}
})
}
export const getTokenSilently = async (...p) => {
if(!_client) {
_client = await getAuth0Client()
}
return await _client.getTokenSilently(...p);
}
export const Auth0Provider = ({
children,
onRedirectCallback = DEFAULT_REDIRECT_CALLBACK,
...initOptions
}) => {
const [isAuthenticated, setIsAuthenticated] = useState();
const [user, setUser] = useState();
const [auth0Client, setAuth0] = useState();
const [loading, setLoading] = useState(true);
const [popupOpen, setPopupOpen] = useState(false);
useEffect(() => {
const initAuth0 = async () => {
_initOptions = initOptions;
const client = await getAuth0Client(initOptions)
setAuth0(client)
// const auth0FromHook = await createAuth0Client(initOptions);
// setAuth0(auth0FromHook);
if (window.location.search.includes("code=")) {
console.log("Found code")
const { appState } = await client.handleRedirectCallback();
onRedirectCallback(appState);
}
const isAuthenticated = await client.isAuthenticated();
setIsAuthenticated(isAuthenticated);
if (isAuthenticated) {
const user = await client.getUser();
setUser(user);
}
setLoading(false);
};
initAuth0();
// eslint-disable-next-line
}, []);
const loginWithPopup = async (params = {}) => {
setPopupOpen(true);
try {
await auth0Client.loginWithPopup(params);
} catch (error) {
console.error(error);
} finally {
setPopupOpen(false);
}
const user = await auth0Client.getUser();
setUser(user);
setIsAuthenticated(true);
};
const handleRedirectCallback = async () => {
setLoading(true);
await auth0Client.handleRedirectCallback();
const user = await auth0Client.getUser();
setLoading(false);
setIsAuthenticated(true);
setUser(user);
};
return (
<Auth0Context.Provider
value={{
isAuthenticated,
user,
loading,
popupOpen,
loginWithPopup,
handleRedirectCallback,
getIdTokenClaims: (...p) => auth0Client.getIdTokenClaims(...p),
loginWithRedirect: (...p) => auth0Client.loginWithRedirect(...p),
getTokenSilently: (...p) => auth0Client.getTokenSilently(...p),
getTokenWithPopup: (...p) => auth0Client.getTokenWithPopup(...p),
logout: (...p) => auth0Client.logout(...p)
}}
>
{children}
</Auth0Context.Provider>
);
};
现在,没有任何限制,我们可以在函数组件或类组件或任何其他位置调用getTokenSilently()
方法。
我使用以下代码来初始化ApolloClient
并在调用ApolloProvider
时传递客户端。
import React from "react";
import { Router, Route, Switch } from "react-router-dom";
import { Container } from "reactstrap";
import PrivateRoute from "./components/PrivateRoute";
import Loading from "./components/Loading";
import NavBar from "./components/NavBar";
import Footer from "./components/Footer";
import Home from "./views/Home";
import Profile from "./views/Profile";
import { useAuth0 } from "./react-auth0-spa";
import history from "./utils/history";
import "./App.css";
import { ApolloProvider } from '@apollo/react-hooks';
import initFontAwesome from "./utils/initFontAwesome";
import { InMemoryCache } from "apollo-boost";
import { ApolloClient } from 'apollo-client';
import { HttpLink } from 'apollo-link-http';
import { ApolloLink, Observable } from 'apollo-link';
import { onError } from 'apollo-link-error';
import { withClientState } from 'apollo-link-state';
import {getTokenSilently} from "./react-auth0-spa";
initFontAwesome();
let API_URL="https://[BACKEND_GRAPHQL_API_URL]/graphql";
const cache = new InMemoryCache();
cache.originalReadQuery = cache.readQuery;
cache.readQuery = (...args) => {
try {
return cache.originalReadQuery(...args);
} catch (err) {
return undefined;
}
};
const request = async (operation) => {
const token = await getTokenSilently();
operation.setContext({
headers: {
authorization: token ? `Bearer ${token}` : ''
}
});
};
const requestLink = new ApolloLink((operation, forward) =>
new Observable(observer => {
let handle;
Promise.resolve(operation)
.then(oper => request(oper))
.then(() => {
handle = forward(operation).subscribe({
next: observer.next.bind(observer),
error: observer.error.bind(observer),
complete: observer.complete.bind(observer),
});
})
.catch(observer.error.bind(observer));
return () => {
if (handle) handle.unsubscribe();
};
})
);
const client = new ApolloClient({
link: ApolloLink.from([
onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors) {
console.log("Graphqlerrors"+graphQLErrors)
// sendToLoggingService(graphQLErrors);
}
if (networkError) {
console.log("Network error"+networkError)
// logoutUser();
}
}),
requestLink,
withClientState({
defaults: {
isConnected: true
},
resolvers: {
Mutation: {
updateNetworkStatus: (_, { isConnected }, { cache }) => {
cache.writeData({ data: { isConnected }});
return null;
}
}
},
cache
}),
new HttpLink({
uri: API_URL,
// credentials: 'include'
})
]),
cache
});
const App = () => {
const { loading } = useAuth0();
if (loading) {
return <Loading />;
}
return (
<ApolloProvider client={client}>
<Router history={history}>
<div id="app" className="d-flex flex-column h-100">
<NavBar />
<Container className="flex-grow-1 mt-5">
<Switch>
<Route path="/" exact component={Home} />
<PrivateRoute path="/profile" component={Profile} />
</Switch>
</Container>
<Footer />
</div>
</Router>
</ApolloProvider>
);
};
export default App;
因此,我正在使用@auth0/auth0-react
和@apollo/client
,并且我设法使其按如下方式工作:
我的应用程序index.tsx
:
<AuthProvider>
<CustomApolloProvider>
<Router>
<MY_ROUTES>
</Router>
</CustomApolloProvider>
</AuthProvider>
注意:AuthProvider
只是本答案中Auth0Provider
的别名。
CustomApolloProvider
我有以下内容:
- 进口:
import React, { useEffect, useState } from 'react';
import { ApolloClient, ApolloProvider, InMemoryCache, HttpLink } from '@apollo/client';
import { useAuth0 } from '@auth0/auth0-react';
- 使用
useAuth0
获取身份验证上下文并创建客户端状态:
const { isAuthenticated, isLoading, getIdTokenClaims } = useAuth0();
const [client, setClient] = useState(undefined as unknown as ApolloClient<any>)
- 当
- auth0 准备就绪时触发
setClient
:
useEffect(() => {
if (!isLoading && isAuthenticated) {
// Here createApolloClient is a function that takes token as input
// and returns `ApolloClient` instance.
getIdTokenClaims().then(jwtToken => setClient(createApolloClient(jwtToken.__raw)));
}
}, [isAuthenticated, isLoading]);
- 客户端
- 可用时加载页面:
if (!client)
return <PageLoader />
return (
<ApolloProvider client={client}>
{children}
</ApolloProvider>
);
可以在GitHub上找到一个工作示例:https://github.com/atb00ker/ideation-portal/tree/1c6cbb26bb41f5a7b13a5796efd98bf1d77544cd/src/views