Remix Run:验证失败却仍跳转至Dash路由的认证问题求助
问题根源分析
你的代码存在两个核心问题,导致登录失败时仍跳转到 /dash:
Authenticator 错误处理逻辑冲突
Remix Auth 的authenticate方法默认不会抛出验证失败的错误(throwOnError默认值为false),而是直接返回failureRedirect对应的重定向响应。但你在外层添加了try/catch捕获自定义错误并返回 JSON,这会干扰 Authenticator 的默认失败重定向逻辑——如果策略抛出的错误被你的catch块捕获并返回 JSON,而非让 Authenticator 处理为重定向,前端可能因未正确处理 JSON 响应而错误跳转。策略回调的错误类型不匹配
你在策略中抛出的是AuthorizationError,但catch块判断的是CustomAuthorizationError,如果这两个不是同一个类(比如你自定义了CustomAuthorizationError但策略里用的是 Remix Auth 自带的AuthorizationError),catch条件不会命中,此时会重新抛出错误,导致 Remix 返回 500 错误而非预期的重定向。
另外还有一个潜在逻辑漏洞:当验证通过(validationResult.success 为 true)但最终的 if (passwordMatch && email === user.email) 条件不满足时,策略回调会返回 undefined,虽然 Authenticator 会将其视为验证失败,但这种冗余判断可能引发不可预期的行为。
修复方案
方案1:移除外层 try/catch,依赖 Authenticator 原生重定向逻辑
既然你已经配置了 failureRedirect: "/login",让 Authenticator 自行处理验证失败的重定向即可,无需手动捕获错误:
// Login route action method: export async function action({ request }: ActionFunctionArgs) { return await authenticator.authenticate("form", request, { successRedirect: "/dash", failureRedirect: "/login", }); }
方案2:保留错误捕获并正确配置 throwOnError
如果你需要返回错误信息给前端(用于在登录页显示字段错误),需要开启 throwOnError: true,让 authenticate 抛出验证失败的错误,同时确保错误类型匹配:
首先,修改策略中抛出的错误类型为自定义的 CustomAuthorizationError:
// 在策略中替换原有 AuthorizationError 为 CustomAuthorizationError throw new CustomAuthorizationError("Invalid email", "", { email: "Please enter a valid email", password: ""});
然后修改 action 方法,开启 throwOnError 并正确处理错误:
export async function action({ request }: ActionFunctionArgs) { try { return await authenticator.authenticate("form", request, { successRedirect: "/dash", failureRedirect: "/login", throwOnError: true, // 开启抛出错误,让外层 catch 能捕获 }); } catch (error) { if (error instanceof CustomAuthorizationError) { // 返回错误信息,前端可通过 useActionData 获取并显示 return json({ errors: error.errors, fields: error.fields }, { status: 401 }); } // 其他错误重新抛出,交由 Remix 处理 throw error; } }
方案3:优化策略中的冗余逻辑
移除策略中最后冗余的条件判断,因为前面已经验证了密码匹配且用户存在,直接返回用户即可:
// 替换原有的条件判断代码 return user;
前端错误信息显示(补充)
如果选择方案2返回错误 JSON,你需要在登录页组件中通过 useActionData 获取错误信息并渲染:
import { useActionData } from "@remix-run/react"; export default function LoginPage() { const actionData = useActionData<typeof action>(); return ( <form method="post"> <div> <label>Email</label> <input type="email" name="email" defaultValue={actionData?.fields?.email} /> {actionData?.errors?.email && <span className="error">{actionData.errors.email}</span>} </div> <div> <label>Password</label> <input type="password" name="password" /> {actionData?.errors?.password && <span className="error">{actionData.errors.password}</span>} </div> <button type="submit">Login</button> </form> ); }
内容的提问来源于stack exchange,提问作者Ahmed

