最近使用
remix-auth
和 prisma
实现了一个简单的登录功能,但是......以一种奇怪的方式,在我的操作函数中,真正的凭据去 catch 块并抛出 Response
错误。不过一切都很好。当输入不正确的凭据或 csrf 出现错误(使用 remix-utils
)时,我会看到该消息。
实施:
/服务/session.server.ts
import { createCookieSessionStorage } from "@remix-run/node";
if (!process.env.SESSION_SECRET) {
throw new Error("SESSION_SECRET is not set");
}
// export the whole sessionStorage object
export const sessionStorage = createCookieSessionStorage({
cookie: {
name: "_vSession",
sameSite: "lax",
path: "/",
httpOnly: true,
secrets: [process.env.SESSION_SECRET],
secure: process.env.NODE_ENV === "production",
},
});
export const { getSession, commitSession, destroySession } = sessionStorage;
/services/auth-strategies/form.strategy.server.ts
import type { UserSession } from "types/auth";
import { FormStrategy } from "remix-auth-form";
import prisma from "lib/prisma";
import { AuthorizationError } from "remix-auth";
import { compare } from "bcrypt";
export const formStrategy = new FormStrategy<UserSession>(async ({ form }) => {
const username = form.get("username") as string;
const password = form.get("password") as string;
const user = await prisma.user.findUnique({
where: { username },
select: {
id: true,
username: true,
role: true,
password: true,
},
});
if (!user)
throw new AuthorizationError("user doesn't exist", {
name: "userNotFound",
message: "user doesn't exist",
cause: "userNotFound",
});
const passwordMatch = await compare(password, user.password as string);
if (!passwordMatch)
throw new AuthorizationError("invalid username or password", {
name: "invalidCredentials",
message: "invalid username or password",
cause: "invalidCredentials",
});
const { password: userPassword, ...userWithoutPassword } = user;
return userWithoutPassword;
});
我的表单策略名称位于此导出的对象内
/services/auth-strategies/index.ts
export const AuthStrategies = {
FORM: "form"
} as const;
这里一切都照常使用:
/服务/auth.server.ts
import { Authenticator } from "remix-auth";
import { sessionStorage } from "~/services/session.server";
import { AuthStrategies } from "~/services/auth_strategies";
import { formStrategy } from "./auth_strategies/form.strategy.server";
import { UserSession } from "types/auth";
export type AuthStrategy = (typeof AuthStrategies)[keyof typeof AuthStrategies];
export const authenticator = new Authenticator<UserSession | Error | null>(
sessionStorage,
{
sessionErrorKey: "vSessionError",
sessionKey: "vSession",
throwOnError: true,
}
);
authenticator.use(formStrategy, AuthStrategies.FORM);
root.tsx
:
import { authenticator } from "~/services/auth.server";
import { MetaFunction } from "@remix-run/react";
...
export const loader = async ({ request }: LoaderFunctionArgs) => {
const headers = new Headers();
// i18n
const locale = await i18next.getLocale(request);
// CSRF
const [token, cookieHeader] = await csrf.commitToken();
// Setting headers
headers.append("Set-Cookie", await i18nCookie.serialize(locale));
headers.append("Set-Cookie", cookieHeader as string);
return json({ locale, csrf: token }, { headers });
};
...
并且在
_index.tsx
中我只是重定向到仪表板:
import type { LoaderFunctionArgs } from "@remix-run/node";
import { redirect } from "@remix-run/react";
import { authenticator } from "~/services/auth.server";
export const loader = async ({ request }: LoaderFunctionArgs) => {
return await authenticator.isAuthenticated(request, {
failureRedirect: "/login",
successRedirect: "/dashboard",
});
};
现在在我的
login
页面:
...
// Loader
export async function loader({ request }: LoaderFunctionArgs) {
return await authenticator.isAuthenticated(request, {
successRedirect: "/dashboard",
});
}
// Page
export default function Login() {
const { t } = useTranslation("login");
return (
<Container className="sm:px-20 sm:py-10">
<Title className="text-center">{t("title")}</Title>
<Space h="xs" />
<LoginForm />
</Container>
);
}
// Action
export async function action({ request }: ActionFunctionArgs) {
try {
await csrf.validate(request);
return await authenticator.authenticate("form", request, {
successRedirect: "/dashboard",
});
} catch (error) {
if (error instanceof CSRFError)
return json(
{ error: "error with authenticating, please refresh" },
{ status: 403 }
);
if (error instanceof AuthorizationError)
return json({ error: error.message }, { status: 401 });
if (error instanceof Response) {
// @!@ FLOWS HERE @!@
return null;
}
}
}
// Error Boundary
export function ErrorBoundary() {
...
}
Form
组件——虽然不是真正必要的:
...
return (
<Form
method="post"
onSubmit={loginForm.onSubmit((_values, event) =>
submit((event as React.FormEvent<HTMLFormElement>).currentTarget)
)}
>
<AuthenticityTokenInput name="_vCsrf" />
<TextInput.../>
<Space h={"sm"} />
<PasswordInput.../>
...
<Button
type="submit" ... >
{t("login")}
</Button>
</Form>
);
}
我再次对登录页面中代码所在的部分进行了评论:
@!@ FLOWS HERE @!@
。为什么会出现这样的行为?如果我不将验证器放在 try/catch 中,我会成功重定向,尽管凭据错误,ErrorBoundry
会被渲染!
所有配置均正确。由于 remix-auth 在成功验证时抛出响应,因此它流向 catch 块。重新混合应用程序的预期行为是抛出重定向以阻止加载器和操作进一步执行。您可以重新抛出或返回响应:
if (error instanceof Response) {
// @!@ FLOWS HERE @!@
return error;
}
然后就认证成功了