在Next.js中设置身份验证时的注意事项

在Next.js中设置身份验证时的注意事项

在过去几年中,为应用程序添加身份验证功能已经从晦涩难懂的复杂功能变成了只需使用 API 即可实现的功能。

关于如何在 Next.js 中实现特定身份验证方案的示例软件仓库和教程并不缺乏,但关于为什么要选择哪些方案、工具和权衡的内容却较少。

本篇文章将介绍在 Next.js 中进行身份验证时需要考虑的事项,从选择提供商到构建登录路由,以及在服务器端和客户端之间做出选择。

选择身份验证方法/提供商

基本上有 1000 种方法可以在应用程序中建立身份验证。与其在这里关注特定的提供商(这是另一篇博文的主题),不如让我们来看看认证解决方案的类型以及每种解决方案的几个示例。在实施方面,next-auth 正迅速成为将 Next.js 应用程序与多个提供商集成、添加 SSO 等功能的热门选择。

传统数据库

这种方法非常简单:将用户名和密码存储在关系数据库中。当用户首次注册时,您会在 “users” 表中插入一行新记录,其中包含所提供的信息。当用户登录时,将根据表中存储的信息检查其证书。当用户要更改密码时,就需要更新表中的值。

纵观现有的所有应用程序,传统的数据库验证无疑是最流行的验证方案,而且基本上一直存在。它非常灵活、便宜,而且不会将你锁定在任何特定的供应商。但你确实需要自己构建它,尤其要担心加密问题,并确保那些可爱的密码不会落入坏人之手。

数据库供应商提供的身份验证解决方案

在过去几年里(Firebase 的功劳不只几年前),托管数据库提供商提供某种托管身份验证解决方案已成为相对标准的做法。FirebaseSupabaseAWS 都通过一套 API 提供托管数据库和托管身份验证服务,这套 API 可以轻松抽象用户创建和会话管理(稍后详述)。

使用 Supabase 身份验证登录用户非常简单

async function signInWithEmail() {
const { data, error } = await supabase.auth.signInWithPassword({
email: 'example@email.com',
password: 'example-password',
})
}

非数据库提供商提供的身份验证解决方案

也许比 DBaaS 提供的身份验证服务更常见的是整个公司或产品提供的身份验证服务。Auth0 早在 2013 年就已出现(现在归 Okta 所有),最近又增加了 Stytch 等产品,这些产品都优先考虑开发人员的体验,并获得了一定的市场份额。

Auth0 用于身份验证

Auth0 用于身份验证

单点登录

单点登录让你可以将身份 “外包 “给外部提供商,外部提供商可以是像 Okta 这样注重安全的企业级供应商,也可以是像 Google 或 GitHub 这样更广泛采用的供应商。Google SSO 在 SaaS 领域无处不在,而一些以开发人员为重点的工具只能通过 GitHub 进行身份验证。

无论你选择哪种供应商,SSO 通常都是上述其他类型身份验证的附加组件,在与外部平台集成方面有自己的特殊性(警告:SAML 使用 XML)。

好吧,哪一种适合我?

这里没有 “正确 “的选择–什么适合你的项目取决于你的优先事项。如果您希望快速开展工作,而不需要大量的前期配置,那么将认证外包(甚至将包括用户界面在内的全部工作外包给 Auth0 这样的公司)是合理的。如果您预计会有更复杂的设置,那么建立自己的验证后端是合理的。如果你希望支持更大的客户,就需要在某些时候添加 SSO。

Next.js 目前非常流行,大多数身份验证提供商都有 Next.js 专用文档和集成指南。

构建注册和登录路径,以及进行额外身份验证的技巧

一些身份验证提供商(如 Auth0)实际上提供了用于注册和登录的整个托管网页。但是,如果你要从头开始创建这些页面,我发现在创建过程中尽早创建这些页面是非常有用的,因为当你真正实施身份验证时,你需要这些页面作为重定向。

因此,先创建这些页面的结构,然后再将请求添加到后台,这样做才有意义。实现身份验证的最直接方法就是拥有两个这样的路由:

  • 一个用于注册
  • 另一个用于用户已有账户后的登录

除了基本功能外,您还需要处理边缘情况,例如用户忘记密码时。有些团队喜欢在单独的路径上设置密码重置流程,而有些团队则喜欢在常规登录页面上添加动态用户界面元素。

一个漂亮的注册页面可能并不意味着成功与失败的区别,但一些小细节却能给人留下好印象,并在整体上提供更好的用户体验。以下是从网络上一些网站整理出来的,它们在认证流程中加入了一些额外的爱。

1. 如果有活动会话,更新导航栏

Stripe 导航栏中的行动号召会根据您是否有认证会话而改变。下面是未通过身份验证时营销网站的样子。请注意登录的操作提示:

Stripe 主页会根据您是否通过身份验证来更改 CTA

Stripe 主页会根据您是否通过身份验证来更改 CTA

下面是通过身份验证后的页面。请注意,行动呼吁会将用户带到仪表板,而不是登录:

Stripe 主页更改

Stripe 主页更改

虽然没有从根本上改变我的 Stripe 体验,但还是不错的。

一个有趣的技术问题:大多数公司都不会让营销网站的导航栏 “依赖” 身份验证,这是有原因的,因为这意味着每次页面加载时都要额外请求 API 检查身份验证状态,而大多数页面的访问者可能都没有通过身份验证。

2. 在注册表单旁添加一些有用的内容

在过去几年中,尤其是在 SaaS 领域,公司开始在注册页面添加内容,以 “鼓励” 用户完成注册。这有助于提高页面的转化率,至少可以逐步提高。

下面是 Retool 的一个注册页面,边上有一个动画和一些标识:

在注册表单旁添加一些有用的内容

如果要这样做,请确保两侧的字体相匹配。

这些额外的小内容可以帮助提醒用户注册的目的以及为什么需要注册。

3. 如果使用密码:建议或强制使用强密码

我可以肯定地说,密码本身就不安全,这是开发人员的常识,但这并不是所有注册你产品的人的常识。鼓励用户创建安全密码对你和他们都有好处。

Coinbase 对注册要求非常严格,要求你使用比名字更复杂的安全密码:

 

Coinbase 上的弱密码

Coinbase 上的弱密码

用密码管理器生成一个密码后,我就可以使用了:

 

Coinbase 上的强密码

Coinbase 上的强密码

不过,用户界面并没有告诉我密码不够安全的原因,也没有告诉我除了数字之外的其他要求。在产品文案中加入这些要求,会让用户使用起来更顺畅,也有助于避免重试密码时的挫败感。

4. 为输入内容贴标签,让它们与密码管理器

配合默契每三个美国人中就有一个使用密码管理器(如 1Password),但网上仍有许多表单忽略了 HTML 输入中的 `type=` 字样。让您的表单与密码管理器配合默契

  • 将输入元素包含在表单元素中
  • 为输入赋予类型和标签
  • 为输入添加自动完成功能
  • 不要动态添加字段(我看着你呢,Delta

这可以让您在 10 秒钟内顺利完成登录,也可以让您手动完成登录,尤其是在移动设备上。

在会话和 JWT 之间做出选择

用户通过身份验证后,您需要选择一种策略来在后续请求中保持该状态。HTTP 是无状态的,我们当然不想在每次请求时都要求用户提供密码。目前有两种流行的处理方法会话(或 Cookie)和 JWTs(JSON 网络令牌),它们的区别在于由服务器还是客户端来完成这项工作。

会话,又名 Cookie

在基于会话的身份验证中,维护身份验证的逻辑和工作由服务器处理。基本流程如下:

  1. 用户通过登录页面进行身份验证。
  2. 服务器创建一个记录,代表这个特定的浏览 “会话”。该记录通常会被插入数据库,其中包含一个随机标识符和有关会话的详细信息,如开始时间和过期时间。
  3. 该随机标识符(如 “6982e583b1874abf9078e1d1dd5442f1″)会被发送到浏览器,并作为 Cookie 保存
  4. 在客户端的后续请求中,会包含该标识符,并与数据库中的会话表进行核对

在会话持续多长时间、何时撤销会话等方面,这种方法非常简单,而且可以调整。其缺点是,由于要对数据库进行大量写入和读取,因此会有很大的延迟,但这对大多数读者来说可能不是主要的考虑因素。

JSON 网络令牌(JWT)

JWTs 不需要在服务器上处理后续请求的身份验证,而是可以在客户端处理(大部分)。工作原理如下:

  1. 用户通过登录页面进行身份验证。
  2. 服务器会生成一个 JWT,其中包含用户身份、授予的权限和到期日期(以及其他潜在的信息)。
  3. 服务器会对令牌内容进行加密签名,然后将整个内容发送给客户端。
  4. 对于每个请求,客户端都可以解密令牌,并验证用户是否有权限提出请求(所有这些都无需与服务器通信)。

将初始验证后的所有工作都卸载到客户端后,应用程序的加载和运行速度都会大大加快。但有一个主要问题:服务器无法使 JWT 失效。如果用户想注销设备或其授权范围发生变化,就需要等到 JWT 失效。

在服务器端和客户端 Auth 之间做出选择

Next.js 的部分优势在于内置的静态渲染功能–如果你的页面是静态的,即不需要调用任何外部 API,Next.js 就会自动对其进行缓存,并通过 CDN 以极快的速度提供服务。如果文件中不包含任何 `getServerSideProps` 或 `getInitialProps` 内容,Next.js 13 之前的版本就会知道页面是否是静态的,而 Next.js 13 之后的版本则会使用 React Server Components 来实现这一功能。

对于身份验证,你可以选择:渲染一个静态的 “加载” 页面并在客户端执行获取操作,或者在服务器端执行所有操作。对于需要身份验证的页面,你可以呈现一个静态的 “骨架”,然后在客户端发出身份验证请求。理论上,这意味着即使初始内容尚未完全就绪,页面加载速度也会更快。

下面是文档中的一个简化示例,只要用户对象尚未就绪,就会呈现加载状态:

import useUser from '../lib/useUser'
const Profile = () => {
// Fetch the user client-side
const { user } = useUser({ redirectTo: '/login' })
// Server-render loading state
if (!user || user.isLoggedIn === false) {
// Build some sort of loading page here
return <div>Loading...</div>
}
// Once the user request finishes, show the user
return (
<div>
<h1>Your Account</h1>
<p>Username: {JSON.stringify(user.username,null)}</p>
<p>Email: {JSON.stringify(user.email,null)}</p>
<p>Address: {JSON.stringify(user.address,null)}</p>
</div>
)
}
export default Profile

请注意,您需要在此构建某种加载 UI,以便在客户端提出加载后请求时保留空间。

如果想简化操作并在服务器端运行身份验证,可以将身份验证请求添加到 `getServerSideProps` 函数中,Next 会等待请求完成后再渲染页面。与上面代码段中的条件逻辑不同,你可以运行一些更简单的东西,比如 Next 文档中的简化版本:

import withSession from '../lib/session'
export const getServerSideProps = withSession(async function ({ req, res }) {
const { user } = req.session
if (!user) {
return {
redirect: {
destination: '/login',
permanent: false,
},
}
}
return {
props: { user },
}
})
const Profile = ({ user }) => {
// Show the user. No loading state is required
return (
<div>
<h1>Your Account</h1>
<p>Username: {JSON.stringify(user.username,null)}</p>
<p>Email: {JSON.stringify(user.email,null)}</p>
<p>Address: {JSON.stringify(user.address,null)}</p>
</div>
)
}
export default Profile

这里仍然有处理身份验证失败的逻辑,但会重定向到登录,而不是呈现加载状态。

小结

那么,哪种方案适合你的项目呢?首先要评估你对身份验证方案的速度有多大信心。如果您的请求完全不耗费时间,您可以在服务器端运行这些请求,并避免加载状态。如果你想优先立即呈现某些内容,然后等待请求,那就跳过 `getServerSideProps` 并在其他地方运行身份验证。

在使用 Next 时,这是不对每个页面一概要求身份验证的一个很好的理由。这样做更简单,但意味着你首先会错过网络框架的性能优势。

评论留言