如何在 Next.js 中实现身份验证

Posted

技术标签:

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

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

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

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

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

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

【问题讨论】:

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

以上是关于如何在 Next.js 中实现身份验证的主要内容,如果未能解决你的问题,请参考以下文章

如何在 WCF 服务中实现自定义身份验证

如何在带警卫的流明中实现多重身份验证?

如何在基于 JWT 的单点登录身份验证架构中实现注销?

如何在 Spring Security 中实现基于 JWT 的身份验证和授权

如何在 laravel 护照中实现多身份验证

如何在 node.js 中实现登录身份验证