在 react.js 中存储访问令牌的位置?

IT技术 reactjs access-token
2021-05-18 23:52:44

我正在 Reactjs 中构建一个应用程序。在验证 access_token 后,我必须进行 fetch 调用。注册时,access_token 是从后端服务器获取的。但是,在哪里存储这些 access_token。有没有办法让这些 access_token 成为全局的,以便所有组件都可以访问它。我使用过本地存储、缓存和会话存储,但这些都是不可取的。这几天一直在纠结这个问题,求解决方法。提前谢谢。

3个回答

可用选项和限制:

有两种类型的选项可用于存储您的令牌:

  1. Web Storage API:它提供 2 种机制:sessionStoragelocalStorage. 存储在此处的数据将始终可供您的 Javascript 代码使用,并且无法从后端访问。因此,例如,您必须手动将其添加到您的请求中的标头中。此存储仅适用于您应用的域,而不适用于子域。这两种机制的主要区别在于数据过期:
  • sessionStorage:数据仅可用于会话(直到浏览器或选项卡关闭)。
  • localStorage: 存储没有过期日期的数据,只能通过 JavaScript 清除,或清除浏览器缓存/本地存储的数据
  1. Cookies:随后续请求自动发送到您的后端。可以控制您的 Javascript 代码的到期时间和可见性。可用于您应用的子域。

在设计身份验证机制时,您必须考虑两个方面:

  • 安全性:访问或身份令牌是敏感信息。要始终考虑的主要攻击类型是跨站点脚本(XSS) 和跨站点请求伪造(CSRF)。
  • 功能需求:浏览器关闭时用户是否应该保持登录状态?他的会议将持续多久?等等

出于安全考虑,OWASP不建议将敏感数据存储在 Web 存储中。您可以查看他们的CheatSheetSeries页面。您还可以阅读这篇详细的文章以了解更多详细信息。

原因主要与XSS漏洞有关。如果您的前端不是 100% 抵御 XSS 攻击,那么恶意代码可能会在您的网页中执行,并且可以访问令牌。完全防 XSS 非常困难,因为它可能是由您使用的 Javascript 库之一引起的。

另一方面,如果 Cookie 设置为HttpOnly. 现在 cookie 的问题是它们很容易使您的网站容易受到 CSRF 的攻击。SameSitecookie 可以减轻这种类型的攻击。但是,旧版本的浏览器不支持这种类型的 cookie,因此可以使用其他方法,例如使用状态变量。这篇 Auth0 文档文章中对此进行了详细说明

建议的解决方案:

为了安全地存储您的令牌,我建议您使用 2 个 cookie 的组合,如下所述:

JWT 令牌具有以下结构: header.payload.signature

通常,有效载荷中存在有用的信息,例如用户角色(可用于调整/隐藏 UI 的一部分)。因此,让该部分对 Javascript 代码可用非常重要。

身份验证流程完成并在后端创建 JWT 令牌后,其想法是:

  1. header.payload部件存储SameSite SecureCookie 中(因此只能通过 https 使用,并且仍可用于 JS 代码)
  2. signature零件存储SameSite Secure HttpOnlyCookie 中
  3. 在后端实现一个中间件,以从这 2 个 cookie 中重新构建 JWT 令牌并将其放入标头中: Authorization: Bearer your_token

您可以为 cookie 设置到期时间以满足您的应用程序的要求。

Peter Locke这篇文章中提出并很好地描述了这个想法

虽然参加晚会很晚,但我想分享我对这个话题的看法。 Anouar 给出了一个很好的答案,包括被认为是防止 XSS 的 http-only cookie,指出了 CSRF 漏洞并链接了 Peter Locke 的文章。

但是,就我而言,我需要应用程序 100% 无状态,这意味着我不能使用 cookie。

从安全的角度来看,将访问令牌存储在持久位置(如 localStorage、window 等)是不好的做法。因此,您可以使用 redux(或在 state/context 中构建的 react.js)将 JWT 存储在变量中。这将保护令牌免受上述攻击,但在页面刷新后将其置空。

我为解决这个问题所做的是使用刷新令牌,我将其存储在 localStorage 中(如果您愿意,可以使用会话存储或类似存储)。该刷新令牌的唯一目的是获得一个新的访问令牌,后端确保刷新令牌不被窃取(例如实现一个被检查的计数器)。我将访问令牌保存在缓存中(我的应用程序中的一个变量),一旦由于重新加载而过期或丢失,我将使用刷新令牌来获取新的访问令牌。

显然,这仅在您还构建后端时才有效(或者至少如果后端实现了刷新令牌)。如果您处理未实现刷新令牌等的现有 API,并且将访问令牌保存在变量中不是您的选择(由于重新加载时为 null),您还可以在您之前使用应用程序机密对令牌进行加密将其保存到 localStorage(或会话存储,或者...是的,您明白了)。请注意,解密令牌需要一些时间并且会降低您的应用程序的速度。因此,您可以将加密的令牌保存到 localStorage(或...)并在刷新后仅对其解密一次,然后将其保持在状态/redux 变量中,直到您再次刷新/再次从 localStorage 解密等。

关于这个主题的最后一句话:身份验证是应用程序的关键基础设施,虽然有趣的游戏和网上银行之间存在明显的区别(你可能想对那家银行“偏执”,而只“关心”游戏),诸如“localStorage 完全没问题”或“在最坏的情况下会发生什么?1 小时后过期”之类的答案是危险的,而且完全是错误的。机器可以在几秒钟内造成很多伤害,您不想让这个差距保持开放。如果您懒得保护您的应用程序,也许您想使用现有的解决方案而不是构建自己的解决方案。

也就是说,JWT/令牌身份验证对游戏来说是相当新的(几年,但不如开发中的其他主题成熟)。找到可行的解决方案需要一些时间和精力,但让我们继续构建安全的软件,而不是让网络泛滥成灾。

最好的,快乐的编码。

Michael Washburn在他的网页上有一篇关于如何使用 redux 保持状态的非常好的文章

在文章中,他提供了一个非常具有描述性的视频教程的链接,该教程由 Redux 的合著者之一 Dan Abramov 创建,我跟随他将其添加到我的项目中。这是我用来使其工作的代码:

商店.js

import { createStore, combineReducers } from "redux";
import { UserReducer, CopyReducer } from "../reducers";
import { loadState, saveState } from "../utils/localStorage";

export const giveMeStore = () => {
  const reducers = combineReducers({
    copy: CopyReducer,
    user: UserReducer
  });
  const persistedState = loadState();
  const store = createStore(reducers, persistedState);
  //user contains the TOKEN
  store.subscribe(() => {
    saveState({
      user: store.getState().user
    });
  });
  return store;
};

本地存储.js

export const loadState = () => {
  try {
    const serializedState = localStorage.getItem("state");
    if (serializedState === null) {
      return undefined;
    }
    return JSON.parse(serializedState);
  } catch (err) {
    return undefined;
  }
};
export const saveState = state => {
  try {
    const serializedState = JSON.stringify(state);
    localStorage.setItem("state", serializedState);
  } catch (err) {
    //ignoring write erros
  }
};

并将商店添加到提供者:

import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";

import { giveMeStore } from "./store.js";

const Root = () => {
  return (
    <Provider store={giveMeStore()}>
      //... your components
      //...
    </Provider>
  );
};

ReactDOM.render(<Root />, document.querySelector("#root"));