发布于 2026-01-06 3 阅读
0

Next.js 应用中的身份验证

Next.js 应用中的身份验证

喜欢Next.js。

每当我用 React 构建生产级网站时,我都会选择 Next.js 作为首选框架。
它之所以如此出色,是因为它抽象化了所有耗时的繁琐工作,例如服务器端渲染 (SSR)、
Webpack 和 TypeScript 配置、路由等等,让我们能够专注于真正独特的功能,而不是把时间浪费在这些琐碎的任务上。
而且它在这方面做得非常出色,甚至比大多数人自己实现这些功能还要好。除非你是某种受虐狂,否则我强烈建议你不要在 2020 年尝试自己构建服务器端渲染的 React 应用。

Next.js 10

Next.js 一直在进步。最近发布的10 版本带来了大量改进,
简直太棒了!它现在拥有next/image类似 Gatsby 的 Gatsby Image 辅助工具,默认支持 i18n 路由,以及许多其他实用功能。如果你还没看过更新日志,赶紧去看看吧。
不过这篇文章主要讲的是身份验证,所以我们直接进入正题!

Next.js 中的身份验证

Next.js 中处理身份验证并没有唯一的方法。
理想情况下,你无需重复造轮子,而是应该选择标准的身份验证提供程序,例如FirebaseAuth0Cognito
你可以在 Next.js 的 GitHub 代码库中找到使用这些提供程序的示例
。 我们在 Crowdcast 使用的是 Firebase 身份验证,但这些都是有效的选择。
此外,还有一个名为next-auth 的相对较新的项目,我们对其进行了简单的测试,它看起来非常有前景。这些应该可以满足 Next.js 应用中大部分的身份验证需求。

如何构建你的身份验证

我首先要做的是将页面分为三类:

  1. 所有人都可以查看的页面(公开页面)
    这些页面不需要任何身份验证

  2. 当用户未获得授权时,这些页面将完全无法访问。这些页面通常非常适合采用服务器端身份验证。

  3. 根据用户状态显示不同视图的页面和组件。这些页面通常适合采用客户端身份验证。

让我们更详细地了解一下最后两点:

服务器端身份验证

假设您的网站上有一些敏感的用户数据,例如用户的个人电子邮件地址和支付信息。这些数据可以在/account页面上访问。现在,当用户未登录时,渲染此页面上的任何内容都是没有意义的,我们应该将他们重定向到其他页面/login
因此,我们需要使用服务器端身份验证。这意味着,我们在服务器上检查用户是否已登录,如果未登录,则使用 HTTP 重定向将他们重定向到其他页面。如果用户已登录,我们将他们的用户数据直接传递到我们的帐户页面,这样我们就可以立即开始渲染,从而避免一次往返请求。
为此,我们使用一个名为 `Authorization` 的方法getServerSideProps文档)。该方法是在 Next.js 9.3 版本中引入的,在此之前它被称为 `Authorization` getInitialProps
需要注意的是,此函数必须从 Next.js 页面中单独导出。这意味着我们不能使用高阶组件来包装所有使用服务器端身份验证的页面,而必须从每个页面中导出它。

该函数的逻辑取决于你的具体使用场景。在我的示例中,它非常简单:我们尝试从session请求中存储的对象中读取用户对象。如果找到用户,则将其作为 prop 返回给页面;否则,将用户重定向到 /login 路由。

export const getServerSideProps = ({ req, res }) => {
  try {
    const user = req.session.get('user');
    if (!user) throw new Error('unauthorized');

    return {
      props: {
        user,
      },
    };
  } catch (err) {
    return {
      redirect: {
        permanent: false,
        destination: '/login',
      },
    };
  }
};
Enter fullscreen mode Exit fullscreen mode

一个巧妙的重构技巧是,我们可以创建一个共享的辅助函数,该函数返回一个可以在此导出中使用或别名化的函数,如下所示:

// helper.ts
export const getUserFromServerSession = ({
  redirectToLogin,
}: {
  redirectToLogin?: boolean;
}) =>
  withSession(
    async ({ req }: GetServerSidePropsContextWithSession<{}>) => {
      try {
        const user = req.session.get<User>('user');

        if (!user) throw new Error('unauthorized');

        return {
          props: {
            user,
          },
        };
      } catch (err) {
        if (redirectToLogin) {
          return {
            redirect: {
              permanent: false,
              destination: '/login',
            },
          };
        } else {
          return {
            props: {},
          };
        }
      }
    },
  );
Enter fullscreen mode Exit fullscreen mode

然后,您就可以在所有需要服务器端身份验证的页面中使用该辅助函数:

// some-page.tsx
export const getServerSideProps = getUserFromServerSession({
  redirectToLogin: true,
});
Enter fullscreen mode Exit fullscreen mode

这样我们就无需在所有页面中重复编写相同的代码。

如果你好奇的话,我用next-iron-session它来处理会话。你可以在 Next.js 代码库中找到示例

何时使用 getServerSideProps

Next.js 文档中写道:

只有当需要预渲染页面且数据必须在请求时获取时才应使用 `getServerSideProps`。由于服务器必须在每次请求时都计算结果,并且未经额外配置,CDN 无法缓存结果,因此其首字节到达时间 (TTFB) 将比 `getStaticProps` 更慢。如果不需要预渲染数据,则应考虑在客户端获取数据。

因此,请谨慎使用复杂的逻辑或向外部 API 发送请求。在我们的例子中,我们只是检查请求中是否存在会话 cookie,这种方法速度很快,而且可以避免客户端不必要的重新渲染和重定向。

客户端身份验证

假设你有一个用户可以登录的网站。你希望在用户未登录时,在页眉或导航栏中显示“登录”按钮;或者在用户登录时显示头像。此外,
你还希望在用户登录或登出时更新整个应用程序的状态。
无论你选择哪种身份验证提供商,你都需要一个React Hook,以便在任何需要访问用户的页面或组件中使用。

为了创建这个钩子,我强烈建议你使用 React 的context API。你需要的是一个可以从应用内任何位置访问的单一用户对象,而 context 正好满足这个需求。

首先,创建上下文:

// AuthContext.tsx
import React from 'react';
import { Session } from '~/types';

const AuthContext = React.createContext<{
  user: Session | undefined;
  mutateUser: () => Promise<any>;
  logout: () => void;
}>({
  user: undefined,
  mutateUser: async () => null,
  logout: async () => null,
});

export default AuthContext;
Enter fullscreen mode Exit fullscreen mode

这里我们告诉应用程序存在一个上下文,它有三个属性:用户、mutateUser 函数和 logout 函数。不过我们暂时不初始化它们,而是返回空函数和 undefined 类型的用户。

下一步是创建上下文Provider。每个上下文都需要一个提供程序,因为提供程序定义了该上下文的范围

在 Next.js 应用中,通常会将提供程序添加到组件列表_app中(大型应用通常会有多个提供程序)。这样,嵌套在提供程序内的每个组件都可以访问该提供程序的上下文。

// pages/_app.tsx
import { AppProps } from 'next/app';
import { ApolloProvider } from '@apollo/client';
import { CSSReset, ChakraProvider } from '@chakra-ui/core';
import { useApollo } from '~/lib/apolloClient';
import { AuthProvider } from '~/components/auth/AuthProvider';

// eslint-disable-next-line no-console
const onError = (err: Error) => console.log(err);

const App = ({ Component, pageProps }: AppProps) => {
  const apolloClient = useApollo(pageProps.initialApolloState);

  return (
    <ApolloProvider client={apolloClient}>
      <ChakraProvider theme={customTheme}>
        <CSSReset />
        <AuthProvider>
          {/* eslint-disable-next-line react/jsx-props-no-spreading */}
          <Component {...pageProps} />
        </AuthProvider>
      </ChakraProvider>
    </ApolloProvider>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

如您所见,这里我提供了多个提供商,果然,我们也在AuthProvider其中找到了我们自己的提供商。现在我们可以从应用程序中的任何位置访问其上下文!

实际的提供程序可以这样实现:

// AuthProvider.tsx
import React, { useEffect } from 'react';
import { useRouter } from 'next/router';

import useSWR from 'swr';
import { auth } from '~/services/auth';
import { Session } from '~/types';

import { post } from '~/utils/http';
import { jwt } from '~/utils/jwt';
import AuthContext from './AuthContext';

export function AuthProvider({
  children,
}: {
  children: React.ReactNode;
}) {
  const { data: user, mutate: mutateUser } = useSWR<Session>(
    '/api/user',
  );

  const { push, pathname, query } = useRouter();

  const logout = async () => {
    await auth.logout();
    push('/');
  };

  useEffect(() => {
    // eslint-disable-next-line consistent-return
    auth.onIdTokenChanged(async _user => {
      if (!_user) return mutateUser(post('/api/logout'));

      await mutateUser(
        post('/api/session', { accountId: _user.uid! }),
      );

      const token = await auth.getLatestToken(_user);

      await jwt.save({ token });
    });

    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [user, mutateUser]);

  return (
    <AuthContext.Provider value={{ user, mutateUser, logout }}>
      {children}
    </AuthContext.Provider>
  );
}
Enter fullscreen mode Exit fullscreen mode

本示例中我们使用了SWR和 Firebase 身份验证,但具体实现细节并不重要。重要的是,我们将需要关注的功能(用户、mutateUser 和注销)附加到 ` value`属性上AuthContext.Provider,这样就可以在整个应用程序中访问这些功能。

现在这个钩子本身非常简单——它只是返回 AuthContext 中的值:

// hooks/useAuth.ts
import { useContext } from 'react';
import AuthContext from '~/components/auth/AuthContext';

export const useAuth = () => useContext(AuthContext);
Enter fullscreen mode Exit fullscreen mode

现在,无论我们在哪里想要访问用户,我们都可以这样做。

import { useAuth } from '~/hook/useAuth';

const { user } = useAuth();
Enter fullscreen mode Exit fullscreen mode

以下是一些对我的帮助很大的其他博客文章:

希望这对您有所帮助!祝您 Next.js 项目开发顺利!

这篇文章最初撰写并发表在我的博客上。

封面照片由Jonathan Farber拍摄,来自Unsplash。

文章来源:https://dev.to/tmaximini/authentication-in-next-js-apps-3b22