如何在 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 中实现身份验证的主要内容,如果未能解决你的问题,请参考以下文章