【问题标题】:How to implement authentication in Next.js如何在 Next.js 中实现身份验证
【发布时间】:2018-09-29 21:46:41
【问题描述】:

我是 Next.js 的新手,我正在努力使用 jwt 令牌的身份验证系统。我想知道使用身份验证系统存储 jwt 令牌和路由的最佳/标准方式是什么。我一直在尝试不同的方法,来自不同的教程/文章,但不太了解。这是我尝试过的。

  1. 当用户登录时,它会将用户名/密码发送到一个分离的 API 服务器(例如处理后端内容的新项目),服务器将响应access-token,然后在下一步。在 Node.js 项目中,我使用接收到的令牌设置 cookie。在 Next.js 项目中,受保护的路由将使用 withAuth hoc 包装,它将检查 cookie 中的令牌。这种方法的问题在于它容易受到 XSS 攻击,因为 cookie 没有 httpOnly 标志。

  2. 这类似于1。)但是使用localStorage,问题是access-token在第一次请求时无法发送到服务器。 (这个我不确定,但据我了解,在每个 HTTP 请求中,我必须手动粘贴我的 access-token,那么我无法控制的请求呢?例如第一个请求或使用 <a> 标签) .

  3. 我在 Next.js 服务器(自定义快速服务器)中编写了身份验证后端。当用户登录时,服务器会对其进行验证,然后设置一个 httpOnly cookie。然后问题是,通过客户端路由(使用 Next.js 路由器转到 URL),它无法检查令牌。例如,如果一个页面使用 withAuth hoc 包装,但它无法使用 javascript 访问 cookie 中的令牌。

而且我见过很多人,在受保护路由的getInitialProps,他们只检查cookie/localStorage中是否存在令牌,那么如果令牌被撤销或列入黑名单怎么办,因为他们没有将令牌发送到服务器?还是我必须在每次客户端页面更改时将令牌发送到服务器?

【问题讨论】:

  • this 线程末尾的一系列示例,包括 JWT、OAuth 等。
  • 作为评论发布,因为我是该项目的维护者,因此可能不适合作为答案(尤其是因为它不是解释),但您可能需要查看 next-auth.js.org,因为它支持JWT 和/或基于数据库的身份验证与 OAuth 提供者和电子邮件非常容易,以安全的方式解决了所描述的一些问题。

标签: reactjs authentication next.js jwt server-side-rendering


【解决方案1】:

这个问题可能需要更新的答案,现在 Next.js 12(2021 年 10 月)中有中间件:https://nextjs.org/docs/middleware

我正在起草一个全面的答案来更深入地解释Next.js中的auth,你可以关注progress there on GitHub

这里我将尝试为 Next.js 提出一个总结,使用中间件。

验证后验证令牌并相应重定向

@Yilmaz 从 2020 年 4 月起的大部分答案仍然具有相关性。但是,以前,我们必须使用_app 中的getInitialProps 来处理请求或自定义服务器。

情况不再如此。。使用中间件可以让您以更简洁的代码实现类似的目的。因为中间件是专门为此类用例设计的。

在这里,我假设您使用 RS256 等非对称算法获得 JWT 访问令牌,与之前的答案完全相同。

这是一个可能的实现:

import { NextFetchEvent, NextRequest, NextResponse } from "next/server";

const removeCookie = (res: NextResponse, cookieName: string) => {
  res.headers.append("Set-Cookie", `${cookieName}=; Max-Age=-1; Path=/`);
  return res;
};

export default async function middleware(
  req: NextRequest,
  ev: NextFetchEvent
) {
  const { pathname } = req.nextUrl;
  const isPublic = isPublicRoute(pathname);

  if (isPublic) {
    return NextResponse.next();
  }

  const accessToken = req.cookies[TOKEN_PATH];
  if (!accessToken) {
    return NextResponse.redirect(LOGIN_HREF);
  }

  const isValidToken = await checkAccessToken(accessToken);

  if (!isValidToken) {
    let res = NextResponse.redirect(LOGIN_HREF);
    res = removeCookie(res, TOKEN_PATH);
    return res;
  }

  return NextResponse.next();
}

如何验证令牌

在我的示例中,checkAccessToken 应该验证令牌(不解码,验证签名)。

这是 imo 最复杂的地方。

使用 RSA256 算法时

您还将获得一个 PUBLIC 证书(除了必须...保密的 SECRET 密钥)。尽管您在 middleware 中进行了检查,这是私有且仅限服务器的代码,但这是个好消息,因为理论上您甚至可以在浏览器中使用它。

因此,您可以fetch 身份验证服务器提供的令牌验证端点,也可以自己验证令牌。 不推荐使用获取选项,因为它可能会破坏 Vercel/Next 边缘功能并增加延迟,according to the documentation

我必须承认,我尚未使用 Next.js 成功验证令牌:) 如果我设法获得一个有效的代码示例,我将更新此答案。

使用对称加密时

您只有一个私人密码。这意味着解码必须在服务器端进行(好消息,您正在编写中间件)。

登录/注销

这不会因中间件而改变。您将访问令牌存储为 httpOnly cookie。注销时,您取消设置此 cookie。

管理这些 Set-Cookies 标头是您的身份验证服务器的责任。

这是一个基本的工作流程,但它应该可以工作。然后,您可以使用类似的方法在混合中添加刷新令牌。

关于令牌撤销

  • 如果您在中间件中验证令牌,访问令牌没有立即撤销机制。因为没有调用数据库。

因此,在这种情况下,您需要选择加入短期访问令牌(例如 5 分钟)和刷新令牌。您可以撤销刷新令牌,所以基本上撤销工作但需要几分钟。

  • 如果第 3 方服务器验证令牌:则它可以检查列入黑名单的令牌。

注意事项

此外,还有一些建议:大多数在线文章、教程等都侧重于服务器到服务器的通信。或客户端到 API。在访问网页之前检查身份验证时,它们完全烂透了。

例如,无法在浏览器中设置 Authorization 标头。它仅在与 API 通信时有效。网页必须使用 Cookie。

即便如此,如果要从浏览器调用此 API,它最好接受 cookie。

与该领域的专家讨论时,您需要始终阐明 Next.js 用例。

开放式问题:关于基于会话的身份验证

一些框架似乎更喜欢依赖数据库。他们将哈希令牌存储在数据库中,充当会话。如果要检查身份验证,则需要一个服务器来检查用户的令牌与存储的令牌(= 检查是否存在与此令牌的活动会话)。

例如,我想到 Meteor。

但是,我找不到这种机制的名称以及它与 JWT 的实际关系。它们只是 JWT 方法的变体吗?

Next.js official authentication doc 在撰写本文时并未显示中间件,而是使用getServerSideProps。我真的不喜欢这种模式。 它使用一种会话系统,但我不清楚它的内部结构,我什至不确定名称(那是基于会话的身份验证吗?)。

Vercel edge handles examples 展示了如何保护 API 路由,而不是页面(在撰写本文时)

【讨论】:

    【解决方案2】:

    随着Next.JS v8的引入,有例子放在NextJS example page。要遵循的基本思想是:

    JWT

    • 使用 cookie 存储令牌(您可以选择是否进一步加密)
    • 将 cookie 作为授权标头发送

    OAuth

    • 使用第三方身份验证服务,例如 OAuth2.0
    • 使用护照

    【讨论】:

      【解决方案3】:

      由于我们正在隔离,我有足够的时间来回答这个问题。这将是一个很长的答案。

      Next.js 使用 App 组件来初始化页面。 _app page 负责渲染我们的页面。我们在 _app.js 上对用户进行身份验证,因为我们从 getInitialProps 返回的任何内容都可以被所有其他页面访问。我们在这里对用户进行身份验证,身份验证决策将传递到页面,从页面到标题,因此每个页面都可以决定用户是否通过身份验证。 (请注意,它可以使用 redux 来完成而不用 prop 钻孔,但它会使答案更复杂)

        static async getInitialProps({ Component, router, ctx }) {
          let pageProps = {};
          const user = process.browser
            ? await auth0.clientAuth()
            : await auth0.serverAuth(ctx.req); // I explain down below
      
          //this will be sent to all the components
          const auth = { user, isAuthenticated: !!user };
          if (Component.getInitialProps) {
            pageProps = await Component.getInitialProps(ctx);
          }
      
          return { pageProps, auth };
        }
      
        render() {
          const { Component, pageProps, auth } = this.props;
          return <Component {...pageProps} auth={auth} />;
        }
      }
      

      如果我们在浏览器上并且需要检查用户是否经过身份验证,我们只需从浏览器中检索 cookie,这很容易。但是我们总是要验证令牌。它与浏览器和服务器使用的过程相同。我将在下面解释。但是如果我们在服务器上。我们无法访问浏览器中的 cookie。但是我们可以从“req”对象中读取,因为 cookie 附加到 req.header.cookie。 这是我们访问服务器上 cookie 的方式。

      async serverAuth(req) {
          // console.log(req.headers.cookie) to check
          if (req.headers.cookie) {
            const token = getCookieFromReq(req, "jwt");
            const verifiedToken = await this.verifyToken(token);
            return verifiedToken;
          }
          return undefined;
        }
      

      这里是 getCookieFromReq()。请记住,我们必须考虑功能性。

      const getCookieFromReq = (req, cookieKey) => {
        const cookie = req.headers.cookie
          .split(";")
          .find((c) => c.trim().startsWith(`${cookieKey}=`));
      
        if (!cookie) return undefined;
        return cookie.split("=")[1];
      };
      

      一旦我们得到cookie,我们必须对其进行解码,提取过期时间以查看它是否有效。这部分很简单。我们必须检查的另一件事是 jwt 的签名是否有效。对称或非对称算法用于签署 jwt。您必须使用私钥来验证对称算法的签名。 RS256 是 API 的默认非对称算法。使用 RS256 的服务器为您提供了一个链接,以获取 jwt 以使用密钥来验证签名。你可以使用 [jwks-rsa][1] 也可以自己做。您必须创建一个证书,然后验证令牌是否有效。

      假设我们的用户现在通过了身份验证。你说,“而且我见过很多人,在受保护路由的 getInitialProps 中,他们只检查 cookie / localStorage 中的存在令牌”。我们使用受保护的路由仅向授权用户提供访问权限。为了访问这些路由,用户必须显示他们的 jwt 令牌,并且 express.js 使用中间件来检查用户的令牌是否有效。由于您已经看过很多示例,因此我将跳过这一部分。

      “那么如果令牌被撤销或列入黑名单怎么办,因为他们没有将令牌发送到服务器,他们如何处理?或者我必须在每个客户端页面更改时将令牌发送到服务器? "

      通过验证令牌过程,我们 100% 确定令牌是否有效。当客户端要求服务器访问一些秘密数据时,客户端必须将令牌发送到服务器。想象一下,当您挂载组件时,组件要求服务器从受保护的路由中获取一些数据。服务器将提取 req 对象,获取 jwt 并使用它从受保护的路由中获取数据。浏览器和服务器获取数据的实现方式不同。如果浏览器发出请求,它只需要相对路径,但服务器需要绝对路径。如您所知,获取数据是通过组件的 getInitialProps() 完成的,并且该函数在客户端和服务器上都执行。这里是你应该如何实现它。我刚刚附上了 getInitialProps() 部分。

      MyComponent.getInitialProps = async (ctx) => {
        const another = await getSecretData(ctx.req);
       //reuslt of fetching data is passed to component as props
        return { superValue: another };
      };
      
      
      
          const getCookieFromReq = (req, cookieKey) => {
            const cookie = req.headers.cookie
              .split(";")
              .find((c) => c.trim().startsWith(`${cookieKey}=`));
      
            if (!cookie) return undefined;
            return cookie.split("=")[1];
          };
      
         
          const setAuthHeader = (req) => {
            const token = req ? getCookieFromReq(req, "jwt") : Cookies.getJSON("jwt");
      
            if (token) {
              return {
                headers: { authorization: `Bearer ${token}` },
              };
            }
            return undefined;
          };
      
          
          export const getSecretData = async (req) => {
            const url = req ? "http://localhost:3000/api/v1/secret" : "/api/v1/secret";
            return await axios.get(url, setAuthHeader(req)).then((res) => res.data);
          };
      
      
      
        [1]: https://www.npmjs.com/package/jwks-rsa
      

      【讨论】:

      • 隔离万岁,谢谢!救生员!我不知道我可以在getInitialProps() 中访问_app.js 中的组件。对于我的用例,我需要将身份验证传递给组件,以防它们对另一台服务器的 API 请求需要令牌访问:pageProps = await Component.getInitialProps(ctx);。我绝对更喜欢全局完成并且可以在特殊情况下被覆盖的方法,比如继承。
      • 我的意思是pageProps = await Component.getInitialProps(ctx, auth);,将auth 作为第二个参数传递。
      猜你喜欢
      • 1970-01-01
      • 2017-10-26
      • 1970-01-01
      • 2020-07-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-01-16
      • 1970-01-01
      相关资源
      最近更新 更多