来自反应组件之外的 auth0provider 的访问令牌

access token from auth0provider outside of react components

我正在使用用户在登录时提供的 auth0 令牌通过 useAuth0.getTokenSilently 进行 api 调用。

在此示例中,fetchTodoListaddTodoItemupdateTodoItem 都需要令牌进行授权。我希望能够将这些函数提取到一个单独的文件中(例如 utils/api-client.js 并导入它们而不必显式传入令牌。

import React, { useContext } from 'react'
import { Link, useParams } from 'react-router-dom'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faCircle, faList } from '@fortawesome/free-solid-svg-icons'
import axios from 'axios'
import { queryCache, useMutation, useQuery } from 'react-query'
import { TodoItem } from '../models/TodoItem'
import { TodoInput } from './TodoInput'
import { TodoList as TodoListComponent } from './TodoList'
import { TodoListsContext } from '../store/todolists'
import { TodoListName } from './TodoListName'
import { TodoList } from '../models/TodoList'
import { useAuth0 } from '../utils/react-auth0-wrapper'

export const EditTodoList = () => {

  const { getTokenSilently } = useAuth0()

  const fetchTodoList = async (todoListId: number): Promise<TodoList> => {
    try {
      const token = await getTokenSilently!()

      const { data } = await axios.get(
        `/api/TodoLists/${todoListId}`,
        {
          headers: {
            Authorization: `Bearer ${token}`
          }
        }
      )
      return data
    } catch (error) {
      return error
    }
  }

  const addTodoItem = async (todoItem: TodoItem): Promise<TodoItem> => {
    try {
      const token = await getTokenSilently!()

      const { data } = await axios.post(
        '/api/TodoItems',
        todoItem,
        {
          headers: {
            Authorization: `Bearer ${token}`,
          }
        }
      )
      return data
    } catch (addTodoListError) {
      return addTodoListError
    }
  }

  const updateTodoItem = async (todoItem: TodoItem) => {
    try {
      const token = await getTokenSilently!()

      const { data } = await axios.put(
        '/api/TodoItems',
        todoItem,
        {
          headers: {
            Authorization: `Bearer ${token}`,
          }
        }
      )
      return data
    } catch (addTodoListError) {
      return addTodoListError
    }
  }

  const [updateTodoItemMutation] = useMutation(updateTodoItem, {
    onSuccess: () => {
      queryCache.refetchQueries(['todoList', todoListId])
    }
  })

  const [addTodoItemMutation] = useMutation(addTodoItem, {
    onSuccess: () => {
      console.log('success')
      queryCache.refetchQueries(['todoList', todoListId])
    }
  })

  const onAddTodoItem = async (todoItem: TodoItem) => {
    try {
      await addTodoItemMutation({ 
        ...todoItem, 
        todoListId: parseInt(todoListId, 10) 
      })
    } catch (error) {
      // Uh oh, something went wrong
    }
  }

  const { todoListId } = useParams()
  const { status, data: todoList, error } = useQuery(['todoList', todoListId], () => fetchTodoList(todoListId))
  const { todoLists, setTodoList } = useContext(TodoListsContext)
  const todoListIndex = todoLists.findIndex(
    list => todoListId === list.id.toString()
  )

  const setTodoItems = (todoItems: TodoItem[]) => {
    // if(todoList) {
    //   const list = { ...todoList, todoItems }
    //   setTodoList(todoListIndex, list)
    // }
  }

  const setTodoListName = (name: string) => {
    // setTodoList(todoListIndex, { ...todoList, name })
  }

  return (
    <>
      <Link className="block flex align-items-center mt-8" to="/">
        <span className="fa-layers fa-fw fa-3x block m-auto group">
          <FontAwesomeIcon 
            icon={faCircle} 
            className="text-teal-500 transition-all duration-200 ease-in-out group-hover:text-teal-600" 
          />
          <FontAwesomeIcon icon={faList} inverse transform="shrink-8" />
        </span>
      </Link>

      {status === 'success' && !!todoList && (
        <>
          <TodoListName
            todoListName={todoList.name}
            setTodoListName={setTodoListName}
          />
          <TodoInput 
            onAddTodoItem={onAddTodoItem} 
          />

          <TodoListComponent
            todoItems={todoList.todoItems}
            setTodoItems={setTodoItems}
            updateTodo={updateTodoItemMutation}
          />
        </>
      )}
    </>
  )
}

这是回购的 link:https://github.com/gpspake/todo-client

有多种方法可以解决这个问题。

不要过多更改您的代码库。我会选择 storeproviderhook。那里有许多商店图书馆。

这是一个小版本,也可以在 React 渲染之外使用。
https://github.com/storeon/storeon

这只是我能找到的符合要求的一家非常小的商店的一个例子。

在 React 之外使用存储库可能如下所示:

import store from './path/to/my/store.js;'

// Read data
const state = store.get();

// Save data in the store
store.dispatch('foo/bar', myToken);

我不太清楚为什么您无法在您的各个函数中访问令牌?是因为它们不是 React 函数组件而是普通函数吗?

我所做的其中一件事是创建一个 useFetch 挂钩,它可以获取用户令牌并将其附加到请求本身。然后,我可以调用这个新的获取挂钩,而不是专门导出那些函数。这是我的意思的一个例子。

import React from "react"
import { useAuth0 } from "../utils/auth"

const useFetch = () => {
  const [response, setResponse] = React.useState(null)
  const [error, setError] = React.useState(null)
  const [isLoading, setIsLoading] = React.useState(false)
  const { getTokenSilently } = useAuth0()

  const fetchData = async (url, method, body, authenticated, options = {}) => {
    setIsLoading(true)
    try {
      if (authenticated) {
        const token = await getTokenSilently()
        if (!options.headers) {
          options.headers = {}
        }
        options.headers["Authorization"] = `Bearer ${token}`
      }
      options.method = method
      if (method !== "GET") {
        options.body = JSON.stringify(body)
      }
      const res = await fetch(url, options)
      const json = await res.json()
      setResponse(json)
      setIsLoading(false)
      if (res.status === 200) {
        return json
      }
      throw { msg: json.msg }
    } catch (error) {
      console.error(error)
      setError(error)
      throw error
    }
  }
  return { response, error, isLoading, fetchData }
}

export default useFetch

好的,知道了!

现在我理解得更好了,我真正的问题是如何为 axios 请求提供 auth0 令牌,这样它们就不需要在组件中声明。

简短回答: 在 auth0 初始化时获取令牌并注册一个 axios interceptor 以将该令牌设置为所有 axios 请求的 header 值。

长答案(打字稿中的示例):

声明一个接受令牌并注册一个 axios interceptor

的函数
const setAxiosTokenInterceptor = async (accessToken: string): Promise<void> => {
  axios.interceptors.request.use(async config => {
    const requestConfig = config
    if (accessToken) {
      requestConfig.headers.common.Authorization = `Bearer ${accessToken}`
    } 
    return requestConfig
  })
}

在auth0provider wrapper中,当auth0客户端被初始化和认证时,使用setAxiosTokenInterceptor获取token并将其传递给注册拦截器的函数(修改自Auth0 React SDK Quickstart的示例):

useEffect(() => {
    const initAuth0 = async () => {
        const auth0FromHook = await createAuth0Client(initOptions)
        setAuth0(auth0FromHook)

        if (window.location.search.includes('code=')) {
            const { appState } = await auth0FromHook.handleRedirectCallback()
            onRedirectCallback(appState)
        }

        auth0FromHook.isAuthenticated().then(
            async authenticated => {
                setIsAuthenticated(authenticated)
                if (authenticated) {
                    auth0FromHook.getUser().then(
                        auth0User => {
                            setUser(auth0User)
                        }
                    )
                    // get token and register interceptor
                    const token = await auth0FromHook.getTokenSilently()
                    setAxiosTokenInterceptor(token).then(
                        () => {setLoading(false)}
                    )
                }
            }
        )


    }
    initAuth0().catch()
}, [])

在解决承诺时调用 setLoading(false) 可确保如果 auth0 已完成加载,则已注册拦截器。由于 none 发出请求的组件会在 auth0 完成加载之前呈现,这可以防止在没有令牌的情况下进行任何调用。

这让我可以将我所有的 axios 函数移动到一个单独的文件中,并将它们导入到需要它们的组件中。当调用这些函数中的任何一个时,拦截器会将令牌添加到 header utils/todo-client.ts


import axios from 'axios'
import { TodoList } from '../models/TodoList'
import { TodoItem } from '../models/TodoItem'

export const fetchTodoLists = async (): Promise<TodoList[]> => {
  try {
    const { data } = await axios.get(
      '/api/TodoLists'
    )
    return data
  } catch (error) {
    return error
  }
}

export const fetchTodoList = async (todoListId: number): Promise<TodoList> => {
  try {
    const { data } = await axios.get(
      `/api/TodoLists/${todoListId}`
    )
    return data
  } catch (error) {
    return error
  }
}

export const addTodoItem = async (todoItem: TodoItem): Promise<TodoItem> => {
  try {
    const { data } = await axios.post(
      '/api/TodoItems',
      todoItem
    )
    return data
  } catch (addTodoListError) {
    return addTodoListError
  }
}
...

Full source on github

这是@james-quick 回答的一个变体,我在其中使用“RequestFactory”生成 axios 格式的请求,然后只添加来自 Auth0[ 的 auth header =14=]

我遇到了同样的问题,我通过将所有 API 调用逻辑移动到我创建的自定义挂钩中来绕过这个限制:

import { useAuth0 } from '@auth0/auth0-react';
import { useCallback } from 'react';
import makeRequest from './axios';

export const useRequest = () => {
  const { getAccessTokenSilently } = useAuth0();

  // memoized the function, as otherwise if the hook is used inside a useEffect, it will lead to an infinite loop
  const memoizedFn = useCallback(
    async (request) => {
      const accessToken = await getAccessTokenSilently({ audience: AUDIANCE })
      return makeRequest({
        ...request,
        headers: {
          ...request.headers,
          // Add the Authorization header to the existing headers
          Authorization: `Bearer ${accessToken}`,
        },
      });
    },
    [isAuthenticated, getAccessTokenSilently]
  );
  return {
    requestMaker: memoizedFn,
  };
};

export default useRequest;

用法示例:

 import { RequestFactory } from 'api/requestFactory';

 const MyAwesomeComponent = () => {
   const { requestMaker } = useRequest(); // Custom Hook
   ...
   requestMaker(QueueRequestFactory.create(queueName))
     .then((response) => {
       // Handle response here
       ...
     });
 }

RequestFactory 为我的不同 API 调用定义并生成请求负载,例如:

export const create = (queueName) => ({ method: 'post', url: '/queue', data: { queueName } });

Here 是一个完整的 Auth0 集成 PR 供参考。

关于如何在 React 组件之外使用 getAccessTokenSilently 我遇到了类似的问题,我最终得到的是:

我的 HTTP 客户端包装器

export class HttpClient {
  constructor() {
    HttpClient.instance = axios.create({ baseURL: process.env.API_BASE_URL });

    HttpClient.instance.interceptors.request.use(
      async config => {
        const token = await this.getToken();

        return {
          ...config,
          headers: { ...config.headers, Authorization: `Bearer ${token}` },
        };
      },
      error => {
        Promise.reject(error);
      },
    );

    return this;
  }

  setTokenGenerator(tokenGenerator) {
    this.tokenGenerator = tokenGenerator;
    return this;
  }

  getToken() {
    return this.tokenGenerator();
  }
}


在我的 App root 上,我从 auth0

传递 getAccessTokenSilently
 useEffect(() => {
    httpClient.setTokenGenerator(getAccessTokenSilently);
  }, [getAccessTokenSilently]);

就是这样!

您现在有一个 axios 实例准备好通过

执行经过身份验证的请求

我喜欢将 API 调用放在它们自己的目录中(例如在 /api 下),并让调用 API 的代码尽可能小。我在这里采用了与其他人类似的方法,使用 Auth0、TypeScript、Axios(包括拦截器)和 React hooks。

TLDR 答案

将您的 Axios 拦截器放在一个钩子中,然后在分段的 API 个钩子中使用该钩子(即 useUserApiuseArticleApiuseCommentApi 等等) .然后,您可以使用 Auth0.

干净地调用您的 API

长答案

定义你的 Axios 钩子,我只介绍了我当前使用的 HTTP 方法:

# src/api/useAxios.ts

import { useAuth0 } from '@auth0/auth0-react';
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';

// We wrap Axios methods in a hook, so we can centrally handle adding auth tokens.
const useAxios = () => {
  const { getAccessTokenSilently } = useAuth0();

  axios.interceptors.request.use(async (config: any) => {
    if (config.url.indexOf('http') === -1) {
      config.url = `${process.env.REACT_APP_API_ENDPOINT}/${config.url}`;
    }

    if (typeof config.headers.Authorization === 'undefined') {
      config.headers.Authorization = `Bearer ${await getAccessTokenSilently()}`;
    }
    return config;
  });

  return {
    get: async (url: string, config?: AxiosRequestConfig<any> | undefined): Promise<AxiosResponse> => axios.get(url, config),
    delete: async (url: string, config?: AxiosRequestConfig<any> | undefined): Promise<AxiosResponse> => axios.delete(url, config),
    post: async (url: string, data?: any, config?: AxiosRequestConfig<any> | undefined): Promise<AxiosResponse> => axios.post(url, data, config),
    put: async (url: string, data?: any, config?: AxiosRequestConfig<any> | undefined): Promise<AxiosResponse> => axios.put(url, data, config),
    patch: async (url: string, data?: any, config?: AxiosRequestConfig<any> | undefined): Promise<AxiosResponse> => axios.patch(url, data, config),
  }
};

export default useAxios;

我在这里所做的是通过调用 getAccessTokensSilently() 添加不记名令牌(如果尚未定义的话)。此外,如果我的 URL 中不存在 HTTP,那么我会从我的环境变量中附加默认的 API URL - 这意味着我可以保持我的请求简短而不必使用完整的URL 每次。

现在我根据我的用户 API 定义一个钩子如下:

# src/api/useUserApi.ts

import { UserInterface } from '[REDACTED]/types';
import { AxiosResponse } from 'axios';
import useAxios from './useAxios';

const useUserApi = () => {
  const { get, post, put } = useAxios();
  return {
    getUser: (id: string): Promise<AxiosResponse<UserInterface>> => get(`user/${id}`),
    putUser: (user: UserInterface) => put('user', user),
    postUser: (user: UserInterface) => post('user', user),
  }
};

export default useUserApi;

你可以看到我从axios中暴露了底层的HTTP方法,然后在API具体场景getUserputUserpostUser中使用它们。

现在我可以继续在某些应用程序逻辑中调用我的 API,将 API 代码保持在绝对最低限度,但仍然允许完全传递和键入 Axios 对象。

import { useAuth0 } from '@auth0/auth0-react';
import { useNavigate } from 'react-router';
import useUserApi from '../../../api/useUserApi';

const LoginCallback = (): JSX.Element => {
  const navigate = useNavigate()
  const { user, isAuthenticated, isLoading } = useAuth0();
  const { getUser, putUser, postUser} = useUserApi();
  const saveUserToApi = async () => {
    if (!user?.sub) {
      throw new Error ('User does not have a sub');
    }

    // Try and find the user, if 404 then create a new one for this Auth0 sub
    try {
      const userResult = await getUser(user.sub);
      putUser(Object.assign(user, userResult.data));
      navigate('/');
    } catch (e: any) {
      if (e.response.status === 404) {
        postUser({
          id: user.sub,
          email: user.email,
          name: user.name,
          givenName: user.givenName,
          familyName: user.familyName,
          locale: user.locale,
        });
        navigate('/');
      }
    }
  }

  if (isLoading) {
    return <div>Logging you in...</div>;
  }

  if (isAuthenticated && user?.sub) {
    saveUserToApi();
    return <p>Saved</p>
  } else {
    return <p>An error occured whilst logging in.</p>;
  }
};

export default LoginCallback;

可以注意到上面的postUserputUsergetUserAPI请求都是一个行,只有声明(const { getUser, putUser, postUser} = useUserApi();)和否则需要导入。

这个答案绝对是站在巨人的肩膀上,但我认为它对喜欢保持 API 通话尽可能干净的人来说同样有用。