如何将NextAuth V5/Auth.js登录错误信息持久化到前端?
问题描述
正在将应用从PHP迁移到Next.js,使用Auth.js并参考CodeWithAntonio的教程,采用mysql2而非Prisma的实现方式,对React/Next.js/Auth.js不熟悉。目前登录功能可正常工作,但当凭证不匹配时,无法将指定错误信息传递到前端。使用的是next-auth 5.0.0-beta.25,同时想了解是否必须使用react-hook-forms。
相关代码文件
log-in-form.tsx
'use client' import { useForm } from 'react-hook-form' import * as z from 'zod' import { zodResolver } from '@hookform/resolvers/zod' import { LogInSchema } from '../../../../../../constants/validation-schemas' import { logIn } from '../../../../../../actions/users/log-in' export default function LogInForm() { const { register, handleSubmit, formState: { errors }, } = useForm<z.infer<typeof LogInSchema>>({ resolver: zodResolver(LogInSchema), }) async function onSubmit(credentials: z.infer<typeof LogInSchema>) { logIn(credentials) } return ( <> <div id="log-in-container"> <form id="log-in-form" onSubmit={handleSubmit(onSubmit)}> <div className="label"> <label htmlFor="username">username</label> </div> <div className="field"> <input {...register('username')} type="text" id="username" maxLength={15} /> {errors.username && ( <div className="error-message"> {errors.username.message} </div> )} </div> <div className="label"> <label htmlFor="password">password</label> </div> <div className="field"> <input {...register('password')} type="password" id="password" maxLength={16} /> {errors.password && ( <div className="error-message"> {errors.password.message} </div> )} </div> <button type="submit" id="log-in-button"> log in </button> {errors.root && ( <div className="error-message"> {errors.root.message} </div> )} </form> </div> </> ) }
auth.ts
import NextAuth from 'next-auth' import Credentials from 'next-auth/providers/credentials' import { LogInSchema } from './constants/validation-schemas' import bcryptjs from 'bcryptjs' import { getPasswordByUsername } from './actions/users/get-password-by-username' import { getUserIdByUsername } from './actions/users/get-user-id-by-username' import { getSuspensionStatusByUsername } from './actions/users/get-suspension-status-by-username' export const { auth, handlers, signIn, signOut } = NextAuth({ providers: [ Credentials({ async authorize(credentials) { const validatedFields = LogInSchema.safeParse(credentials) if (validatedFields.success) { const { username, password } = validatedFields.data const user = await getUserIdByUsername(username) if (!user) { return null } const suspended = await getSuspensionStatusByUsername( username ) if (suspended) { return null } const storedPassword = await getPasswordByUsername(username) const passwordsMatch = await bcryptjs.compare( password, storedPassword ) if (passwordsMatch) { return await getUserIdByUsername(username) } return null } return null }, }), ], session: { strategy: 'jwt' }, callbacks: { async session({ session, user, token }) { return session }, async jwt({ token, user, account, profile, isNewUser }) { return token }, }, })
log-in.ts(服务器操作)
'use server' import * as z from 'zod' import { LogInSchema } from '../../constants/validation-schemas' import { signIn } from '../../auth' import { DEFAULT_LOGIN_REDIRECT } from '../../constants/routes' import { AuthError } from 'next-auth' export async function logIn(values: z.infer<typeof LogInSchema>) { const validatedFields = LogInSchema.safeParse(values) const { username, password } = validatedFields.data try { await signIn('credentials', { username, password, redirectTo: DEFAULT_LOGIN_REDIRECT, }) } catch (error) { if (error instanceof AuthError) { switch (error.type) { case 'CredentialsSignin': return { error: 'Invalid credentials.' } default: return { error: 'Something went wrong.' } } } throw error } }
终端错误日志
[auth][error] CredentialsSignin: Read more at https://errors.authjs.dev#credentialssignin at Module.callback (webpack-internal:///(action-browser)/./node_modules/@auth/core/lib/actions/callback/index.js:246:23) at process.processTicksAndRejections (node:internal/process/task_queues:95:5) at async AuthInternal (webpack-internal:///(action-browser)/./node_modules/@auth/core/lib/index.js:70:24) at async Auth (webpack-internal:///(action-browser)/./node_modules/@auth/core/index.js:130:34) at async signIn (webpack-internal:///(action-browser)/./node_modules/next-auth/lib/actions.js:53:17) at async logIn (webpack-internal:///(action-browser)/./actions/users/log-in.ts:23:9)
解决方案
1. 修复错误信息传递到前端的问题
当前核心问题是onSubmit函数未接收服务器操作返回的错误,也未将错误同步到react-hook-form的表单状态中,需做以下修改:
步骤1:更新log-in-form.tsx的onSubmit逻辑
首先在useForm解构中添加setError方法,用于手动设置表单错误:
const { register, handleSubmit, formState: { errors }, setError, // 添加这个方法 } = useForm<z.infer<typeof LogInSchema>>({ resolver: zodResolver(LogInSchema), })
然后修改onSubmit函数,接收logIn的返回结果并设置错误:
async function onSubmit(credentials: z.infer<typeof LogInSchema>) { const result = await logIn(credentials) if (result?.error) { // 将错误设置为root级别的表单错误,对应页面中已有的errors.root渲染逻辑 setError('root', { message: result.error }) } }
步骤2:(可选)区分不同错误类型
当前authorize函数所有失败场景都返回null,导致统一抛出CredentialsSignin错误。若需要区分“用户不存在”“账号封禁”“密码错误”等场景,可修改auth.ts的authorize函数抛出自定义错误:
async authorize(credentials) { const validatedFields = LogInSchema.safeParse(credentials) if (validatedFields.success) { const { username, password } = validatedFields.data const user = await getUserIdByUsername(username) if (!user) { throw new Error('用户不存在') } const suspended = await getSuspensionStatusByUsername(username) if (suspended) { throw new Error('账号已被封禁') } const storedPassword = await getPasswordByUsername(username) const passwordsMatch = await bcryptjs.compare(password, storedPassword) if (passwordsMatch) { return await getUserIdByUsername(username) } throw new Error('密码错误') } throw new Error('输入格式不正确') }
然后在log-in.ts中捕获自定义错误信息:
try { await signIn('credentials', { username, password, redirectTo: DEFAULT_LOGIN_REDIRECT, }) } catch (error) { if (error instanceof AuthError) { const errorMessage = (error.cause as Error)?.message || '凭证无效' return { error: errorMessage } } throw error }
2. 是否必须使用react-hook-form?
不是必须的。react-hook-form是一款提升表单开发效率的工具库,提供了状态管理、验证、提交处理等便捷能力,但你完全可以用原生HTML表单结合React的useState来实现登录功能,手动管理输入值和错误状态。
内容的提问来源于stack exchange,提问作者Zuno
相关产品推荐
相关产品推荐

