在 nextjs 13“App Router”版本中存储客户端检索到的令牌的正确方法?

问题描述 投票:0回答:2

我们有一个 nextjs 13.5 网络应用程序。 Nextjs 13 使用新的应用程序路由器范例,其中所有组件(甚至客户端组件)都会在整个页面加载期间(例如刷新后)在服务器上进行首次渲染。 来自文档

为了优化初始页面加载,Next.js 将使用 React 的 API 在服务器上为客户端和服务器组件呈现静态 HTML 预览。

在我们的应用程序中,登录流程是登录到提供令牌的外部服务。然后,该令牌用于验证对不同外部服务发出的其他请求 (graphql)。

我可以使用反应上下文/提供者范例将此令牌存储在内存中。

src/app/login/page.tsx

'use client';

import { useState, useCallback, useContext } from 'react';
import { AuthDispatchContext } from '@/contexts';
import { ActionNames } from '@/reducers';

export default function LoginPage() {
  const loginDispatch = useContext(AuthDispatchContext);

  const handleLogin = useCallback((token: string) => {
    loginDispatch({payload: token, type: ActionNames.LOGIN})
  }, []);


  return (
    <main>
      <button onClick={() => handleLogin('a token we got from external service') > Login</button>
    </main>
  );
}

此上下文是在应用布局级别提供的。

src/app/layout.tsx

import React from 'react';
import { ApolloWrapper} from '@/lib/apollo-wrapper';
import AuthWrapper from '@/lib/auth-wrapper';

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>
        <AuthWrapper>
          <ApolloWrapper>
            {children}
          </ApolloWrapper>
        </AuthWrapper>
      </body>
    </html>
  );
}

登录后,其他组件(应用程序根布局的子组件)能够访问更新后的令牌,或者通过注销等清除令牌。特别是,ApolloWrapper 是一个创建需要访问令牌的 Apollo graphql 客户端的组件,它通过提供的上下文获取:

src/lib/apollo-wrapper.tsx

'use client';

import React, { useContext, useEffect } from 'react';
import { AuthContext } from '@/contexts';
import {
  ApolloLink,
  HttpLink,
  SuspenseCache,
} from '@apollo/client';
import {
  ApolloNextAppProvider,
  NextSSRInMemoryCache,
  SSRMultipartLink,
  NextSSRApolloClient as ApolloClient,
} from '@apollo/experimental-nextjs-app-support/ssr';

import { AuthContextType } from '@/types';
import {onError} from 'apollo-link-error';
import { AuthAction, ActionNames } from '@/reducers';

function makeClient(authToken: AuthContextType) {
  return function() {
    const httpLink = new HttpLink({
      uri: GRAPHQL_ENDPOINT,
      headers: {
        Authorization: `Bearer ${authToken}`
      },
    });

    return new ApolloClient({
      cache: new NextSSRInMemoryCache(),
      link:
        typeof window === 'undefined'
          ? ApolloLink.from([
            new SSRMultipartLink({
              stripDefer: true,
            }),
            logoutLink.concat(httpLink),
          ])
          : logoutLink.concat(httpLink),
    });
  };
}

function makeSuspenseCache() {
  return new SuspenseCache();
}

export function ApolloWrapper({ children }: React.PropsWithChildren) {
  const authToken = useContext(AuthContext)

  return (
    <ApolloNextAppProvider
      makeClient={makeClient(authToken)}
      makeSuspenseCache={makeSuspenseCache}
    >
      {children}
    </ApolloNextAppProvider>
  );
}

这一切都很好……直到刷新。刷新后,显然上下文和提供程序被清除为其默认状态(在本例中为 null)。

刷新时,我需要客户端不需要登录。我选择的机制是localStorage:

src/lib/auth-wrapper.tsx

'use client';

import { reducer, ActionNames } from '@/reducers';
import { useReducer, useEffect } from 'react';
import { AuthContext, AuthDispatchContext } from '@/contexts';

export default function AuthWrapper({ children }: React.PropsWithChildren) {
  const [authToken, dispatch] = useReducer(reducer, null);

  // Check if user is already logged in from a previous reload
  useEffect(() => {
    if (localStorage.getItem('authToken') !== ''){
      dispatch({
          type: ActionNames.LOGIN,
          payload: localStorage.getItem('authToken')
      });
    }
  }, []);

  return (
    <AuthContext.Provider value={authToken}>
      <AuthDispatchContext.Provider value={dispatch}>
        {children}
      </AuthDispatchContext.Provider>
    </AuthContext.Provider>
  );
}

这可以工作,但在该组件的实际第一次渲染时,localStorage 不可用,因为第一次渲染发生在服务器端。因此,调用必须存储在 useEffect 中。因此,在可以从 localStorage 获取令牌并将其设置为所有组件使用之前,整个组件树都会获得一次完整渲染。因此,该组件呈现:

/src/app/achildcomponent/page.tsx

'use client';

import { useContext, useEffect } from 'react';
import {
  getMeQuery,
  getMeResponse,
} from '@/lib/queries';
import { useSuspenseQuery } from '@apollo/experimental-nextjs-app-support/ssr';
import { AuthContext } from '@/contexts';

export default function Landing() {
// The following query attempts to run from the server, and thus fails
  const { data, error } = useSuspenseQuery<getMeResponse>(getMeQuery);
  return (
    <main>
      {error ? (
        <p>Error {error.cause as string}</p>
      ) : !data ? (
        <p>Loading...</p>
      ) : data ? (
        <div>
          <ul>
            <li>{data.me.id}</li>
          </ul>
        </div>
      ) : null
      }
    </main>
  );
}

它尝试从客户端进行 graphql 查询减去令牌。

我的第一个想法:没问题,重定向到没有身份验证令牌的登录。但是等等...有一个身份验证令牌,我不希望用户重新登录,这意味着,哦不,我必须检查是否没有身份验证令牌,因为我们没有登录,或者是因为应用程序处于第一个服务器端渲染模式。

第二个想法:至少可以,只需使 graphql 依赖组件和登录组件成为同级组件,以及 authcontext 的两个子组件,并且如果 authtoken 不可用,则不显示 graphql 组件。我走这条路线,但因为我的产品要求是让第一个根页面成为经过身份验证的页面,并在

/login
登录路线,如果未登录,则自动重定向设置为
/login
,我的代码开始变得草率超级快,我有一种直觉,我正走在一条糟糕的道路上。

所以现在我在这里问:我错过了什么?这肯定是一个已解决的问题,我只是对新的 App Router 范式不够熟悉,无法理解。

在 nextjs 13 App Router 应用程序中存储客户端组件获取的令牌的最佳机制是什么?

javascript reactjs authentication next.js next.js13
2个回答
1
投票

不要对应用程序路由器感到困惑。

token
用于识别客户端,因此这里仍然有客户端-服务器架构。

您可以将令牌存储在 cookie 中,该令牌将自动附加到对与 cookie 关联的同一域和路径发出的每个 HTTP 请求。您可以指定其他属性,例如过期时间、安全标志以及是否应通过 JavaScript 访问(HttpOnly 标志)。

现在令牌将附加到每个请求。您可以编写中间件来检查是否有有效令牌或不处理请求。


0
投票

您可以做的是以下

  • 当您通过 (yournextjsapp)/api/login 登录时,您使用 jwt 设置了 cookie
  • 在 NextJS 应用程序中为后端 api 调用创建代理端点
    例如/api/服务
  • 然后您向此端点发出请求,请求将自动发送 cookie,您唯一要做的就是获取 cookie 中的 jwt 并使用该 jwt 令牌将其转发到您的实际后端服务

因此您无需担心客户端组件中的令牌。只需向您的 NextJS 应用程序发出外部服务请求即可。 这是我现在自己的解决方案。

© www.soinside.com 2019 - 2024. All rights reserved.