Nodemailer 提供者
概述
Nodemailer 提供者使用电子邮件发送包含验证令牌的 URL 的“魔法链接”,这些令牌可用于登录。
除了一个或多个 OAuth 服务之外,还支持通过电子邮件登录,为用户提供了一种在丢失 OAuth 帐户访问权限(例如,如果帐户被锁定或删除)时登录的方法。
Nodemailer 提供者可以与一个或多个 OAuth 提供者结合使用(或代替)。
工作原理
在首次登录时,将向提供的电子邮件地址发送验证令牌。默认情况下,此令牌有效期为 24 小时。如果验证令牌在该时间内使用(即通过单击电子邮件中的链接),将为用户创建一个帐户,并将其登录。
如果有人在登录时提供了现有帐户的电子邮件地址,则会发送一封电子邮件,并在他们点击电子邮件中的链接后将其登录到与该电子邮件地址关联的帐户。
Nodemailer 提供者可以与 JSON Web Token 和数据库管理的会话一起使用,但是必须配置数据库才能使用它。没有使用数据库,就不可能启用电子邮件登录。
配置
- Auth.js 不包含
nodemailer
作为依赖项,因此如果您想使用 Nodemailer 提供者,则需要自己安装它。
npm install nodemailer
-
您将需要一个 SMTP 帐户;理想情况下,是用于已知可与
nodemailer
一起使用的服务之一。Nodemailer 也适用于其他传输方式,但是,如果您想使用基于 HTTP 的电子邮件服务,我们建议使用为那些服务设计的其他 Auth.js 提供者,例如Resend或Sendgrid. -
有两种方法可以配置 SMTP 服务器连接。
您可以使用连接字符串或nodemailer
配置对象。
EMAIL_SERVER=smtp://username:[email protected]:587
EMAIL_FROM=[email protected]
import NextAuth from "next-auth"
import Nodemailer from "next-auth/providers/nodemailer"
export const { handlers, auth, signIn, signOut } = NextAuth({
adapter: ...,
providers: [
Nodemailer({
server: process.env.EMAIL_SERVER,
from: process.env.EMAIL_FROM,
}),
],
})
-
不要忘记为存储电子邮件验证令牌设置一个数据库适配器。
-
您现在可以使用电子邮件地址在
/api/auth/signin
处启动登录过程。
直到用户首次验证其电子邮件地址,用户帐户(即Users
表中的条目)才会为该用户创建。如果电子邮件地址已与帐户关联,则用户在使用电子邮件中的链接时将登录到该帐户。
自定义
电子邮件正文
您可以通过将自定义函数作为sendVerificationRequest
选项传递给Nodemailer()
来完全自定义发送的登录电子邮件。
import NextAuth from "next-auth"
import Nodemailer from "next-auth/providers/nodemailer"
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [
Nodemailer({
server: process.env.EMAIL_SERVER,
from: process.env.EMAIL_FROM,
sendVerificationRequest({
identifier: email,
url,
provider: { server, from },
}) {
// your function
},
}),
],
})
例如,以下显示了我们内置的sendVerificationRequest()
方法的源代码。请注意,我们正在渲染 HTML(html()
)并向电子邮件提供者进行网络调用(transport.sendMail()
)以在此方法中实际执行发送操作。
import { createTransport } from "nodemailer"
export async function sendVerificationRequest(params) {
const { identifier, url, provider, theme } = params
const { host } = new URL(url)
// NOTE: You are not required to use `nodemailer`, use whatever you want.
const transport = createTransport(provider.server)
const result = await transport.sendMail({
to: identifier,
from: provider.from,
subject: `Sign in to ${host}`,
text: text({ url, host }),
html: html({ url, host, theme }),
})
const failed = result.rejected.concat(result.pending).filter(Boolean)
if (failed.length) {
throw new Error(`Email(s) (${failed.join(", ")}) could not be sent`)
}
}
function html(params: { url: string; host: string; theme: Theme }) {
const { url, host, theme } = params
const escapedHost = host.replace(/\./g, "​.")
const brandColor = theme.brandColor || "#346df1"
const color = {
background: "#f9f9f9",
text: "#444",
mainBackground: "#fff",
buttonBackground: brandColor,
buttonBorder: brandColor,
buttonText: theme.buttonText || "#fff",
}
return `
<body style="background: ${color.background};">
<table width="100%" border="0" cellspacing="20" cellpadding="0"
style="background: ${color.mainBackground}; max-width: 600px; margin: auto; border-radius: 10px;">
<tr>
<td align="center"
style="padding: 10px 0px; font-size: 22px; font-family: Helvetica, Arial, sans-serif; color: ${color.text};">
Sign in to <strong>${escapedHost}</strong>
</td>
</tr>
<tr>
<td align="center" style="padding: 20px 0;">
<table border="0" cellspacing="0" cellpadding="0">
<tr>
<td align="center" style="border-radius: 5px;" bgcolor="${color.buttonBackground}"><a href="${url}"
target="_blank"
style="font-size: 18px; font-family: Helvetica, Arial, sans-serif; color: ${color.buttonText}; text-decoration: none; border-radius: 5px; padding: 10px 20px; border: 1px solid ${color.buttonBorder}; display: inline-block; font-weight: bold;">Sign
in</a></td>
</tr>
</table>
</td>
</tr>
<tr>
<td align="center"
style="padding: 0px 0px 10px 0px; font-size: 16px; line-height: 22px; font-family: Helvetica, Arial, sans-serif; color: ${color.text};">
If you did not request this email you can safely ignore it.
</td>
</tr>
</table>
</body>
`
}
// Email Text body (fallback for email clients that don't render HTML, e.g. feature phones)
function text({ url, host }: { url: string; host: string }) {
return `Sign in to ${host}\n${url}\n\n`
}
如果您想使用 React 生成与许多电子邮件客户端兼容的精美外观的电子邮件,请查看mjml或react-email
验证令牌
默认情况下,我们正在生成一个随机验证令牌。如果您想覆盖它,可以在您的提供者选项中定义一个generateVerificationToken
方法
import NextAuth from "next-auth"
import Nodemailer from "next-auth/providers/nodemailer"
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [
Nodemailer({
async generateVerificationToken() {
return crypto.randomUUID()
},
}),
],
})
规范化电子邮件地址
默认情况下,Auth.js 会规范化电子邮件地址。它将地址视为不区分大小写(从技术上讲,这与RFC 2821 规范不兼容,但在实践中,这会导致比解决的问题更多,例如,从数据库中查找用户时)。并删除可能作为逗号分隔列表传递的任何辅助电子邮件地址。您可以通过Nodemailer
提供者上的normalizeIdentifier
方法应用自己的规范化。以下示例显示了默认行为
import NextAuth from "next-auth"
import Nodemailer from "next-auth/providers/nodemailer"
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [
Nodemailer({
normalizeIdentifier(identifier: string): string {
// Get the first two elements only,
// separated by `@` from user input.
let [local, domain] = identifier.toLowerCase().trim().split("@")
// The part before "@" can contain a ","
// but we remove it on the domain part
domain = domain.split(",")[0]
return `${local}@${domain}`
// You can also throw an error, which will redirect the user
// to the sign-in page with error=EmailSignin in the URL
// if (identifier.split("@").length > 2) {
// throw new Error("Only one email allowed")
// }
},
}),
],
})
始终确保这返回单个电子邮件地址,即使传递了多个地址。