Remix Auth
Remix 的简单身份验证
适用于功能
- 完整的**服务器端**身份验证
- 完整的**TypeScript** 支持
- 基于**策略**的身份验证
- 轻松处理**成功和失败**
- 实现**自定义**策略
- 支持持久**会话**
概述
Remix Auth 是 Remix.run 应用程序的完整开源身份验证解决方案。
它深受 Passport.js 的启发,但从头开始重写以在 Web Fetch API 之上运行。Remix Auth 可以以最少的设置添加到任何基于 Remix 的应用程序。
与 Passport.js 一样,它使用策略模式来支持不同的身份验证流程。每个策略都作为单独的 npm 包单独发布。
安装
要使用它,请从 npm(或 yarn)安装它
npm install remix-auth
此外,安装其中一种策略。可以在 社区策略讨论 中找到策略列表。
用法
Remix Auth 需要一个会话存储对象来存储用户会话。它可以是任何实现 Remix 的 SessionStorage 接口 的对象。
在这个例子中,我使用的是 createCookieSessionStorage 函数。
// app/services/session.server.ts
import { createCookieSessionStorage } from "@remix-run/node";
// export the whole sessionStorage object
export let sessionStorage = createCookieSessionStorage({
cookie: {
name: "_session", // use any name you want here
sameSite: "lax", // this helps with CSRF
path: "/", // remember to add this so the cookie will work in all routes
httpOnly: true, // for security reasons, make this cookie http only
secrets: ["s3cr3t"], // replace this with an actual secret
secure: process.env.NODE_ENV === "production", // enable this in prod only
},
});
// you can also export the methods individually for your own usage
export let { getSession, commitSession, destroySession } = sessionStorage;
现在,为 Remix Auth 配置创建一个文件。在这里导入 Authenticator
类和你的 sessionStorage
对象。
// app/services/auth.server.ts
import { Authenticator } from "remix-auth";
import { sessionStorage } from "~/services/session.server";
// Create an instance of the authenticator, pass a generic with what
// strategies will return and will store in the session
export let authenticator = new Authenticator<User>(sessionStorage);
User
类型是你将存储在会话存储中以识别已认证用户的任何东西。它可以是完整的用户数据或包含令牌的字符串。它完全可配置。
之后,注册策略。在这个例子中,我们将使用 FormStrategy 检查你想要使用的策略的文档,以查看你可能需要的任何配置。
import { FormStrategy } from "remix-auth-form";
// Tell the Authenticator to use the form strategy
authenticator.use(
new FormStrategy(async ({ form }) => {
let email = form.get("email");
let password = form.get("password");
let user = await login(email, password);
// the type of this user must match the type you pass to the Authenticator
// the strategy will automatically inherit the type if you instantiate
// directly inside the `use` method
return user;
}),
// each strategy has a name and can be changed to use another one
// same strategy multiple times, especially useful for the OAuth2 strategy.
"user-pass"
);
现在至少注册了一种策略,是时候设置路由了。
首先,创建一个 /login
页面。在这里,我们将渲染一个表单以获取用户的电子邮件和密码,并使用 Remix Auth 来认证用户。
// app/routes/login.tsx
import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node";
import { Form } from "@remix-run/react";
import { authenticator } from "~/services/auth.server";
// First we create our UI with the form doing a POST and the inputs with the
// names we are going to use in the strategy
export default function Screen() {
return (
<Form method="post">
<input type="email" name="email" required />
<input
type="password"
name="password"
autoComplete="current-password"
required
/>
<button>Sign In</button>
</Form>
);
}
// Second, we need to export an action function, here we will use the
// `authenticator.authenticate method`
export async function action({ request }: ActionFunctionArgs) {
// we call the method with the name of the strategy we want to use and the
// request object, optionally we pass an object with the URLs we want the user
// to be redirected to after a success or a failure
return await authenticator.authenticate("user-pass", request, {
successRedirect: "/dashboard",
failureRedirect: "/login",
});
};
// Finally, we can export a loader function where we check if the user is
// authenticated with `authenticator.isAuthenticated` and redirect to the
// dashboard if it is or return null if it's not
export async function loader({ request }: LoaderFunctionArgs) {
// If the user is already authenticated redirect to /dashboard directly
return await authenticator.isAuthenticated(request, {
successRedirect: "/dashboard",
});
};
有了它,我们就有了一个登录页面。如果我们需要在应用程序的另一个路由中获取用户数据,我们可以使用 authenticator.isAuthenticated
方法,将请求传递给它,如下所示
// get the user data or redirect to /login if it failed
let user = await authenticator.isAuthenticated(request, {
failureRedirect: "/login",
});
// if the user is authenticated, redirect to /dashboard
await authenticator.isAuthenticated(request, {
successRedirect: "/dashboard",
});
// get the user or null, and do different things in your loader/action based on
// the result
let user = await authenticator.isAuthenticated(request);
if (user) {
// here the user is authenticated
} else {
// here the user is not authenticated
}
一旦用户准备离开应用程序,我们可以在一个操作中调用 logout
方法。
export async function action({ request }: ActionFunctionArgs) {
await authenticator.logout(request, { redirectTo: "/login" });
};
高级用法
基于用户的自定义重定向 URL
假设我们有 /dashboard
和 /onboarding
路由,并且在用户认证后,你需要检查他们数据中的某个值以确定他们是否已完成 onboarding。
如果我们没有将 successRedirect
选项传递给 authenticator.authenticate
方法,它将返回用户数据。
请注意,我们需要以这种方式将用户数据存储在会话中。为了确保我们使用正确的会话密钥,authenticator 具有一个 sessionKey
属性。
export async function action({ request }: ActionFunctionArgs) {
let user = await authenticator.authenticate("user-pass", request, {
failureRedirect: "/login",
});
// manually get the session
let session = await getSession(request.headers.get("cookie"));
// and store the user data
session.set(authenticator.sessionKey, user);
// commit the session
let headers = new Headers({ "Set-Cookie": await commitSession(session) });
// and do your validation to know where to redirect the user
if (isOnboarded(user)) return redirect("/dashboard", { headers });
return redirect("/onboarding", { headers });
};
更改会话密钥
如果我们想更改 Remix Auth 用于存储用户数据的会话密钥,我们可以在创建 Authenticator
实例时进行自定义。
export let authenticator = new Authenticator<AccessToken>(sessionStorage, {
sessionKey: "accessToken",
});
有了它,authenticate
和 isAuthenticated
都将使用该密钥来读写用户数据(在本例中为访问令牌)。
如果我们需要手动从会话中读写数据,请记住始终使用 authenticator.sessionKey
属性。如果我们在 Authenticator
实例中更改了密钥,则无需在代码中更改它。
读取身份验证错误
当用户无法认证时,错误将使用 authenticator.sessionErrorKey
属性设置在会话中。
我们可以在创建 Authenticator
实例时自定义密钥的名称。
export let authenticator = new Authenticator<User>(sessionStorage, {
sessionErrorKey: "my-error-key",
});
此外,我们可以在身份验证失败后使用该密钥读取错误。
// in the loader of the login route
export async function loader({ request }: LoaderFunctionArgs) {
await authenticator.isAuthenticated(request, {
successRedirect: "/dashboard",
});
let session = await getSession(request.headers.get("cookie"));
let error = session.get(authenticator.sessionErrorKey);
return json({ error }, {
headers:{
'Set-Cookie': await commitSession(session) // You must commit the session whenever you read a flash
}
});
};
请记住始终使用 authenticator.sessionErrorKey
属性。如果我们在 Authenticator
实例中更改了密钥,则无需在代码中更改它。
错误处理
默认情况下,身份验证过程中的任何错误都将抛出一个 Response 对象。如果指定了 failureRedirect
,这将始终是一个重定向响应,其错误消息位于 sessionErrorKey
上。
如果未定义 failureRedirect
,Remix Auth 将抛出一个包含错误消息的 JSON 主体的 401 Unauthorized 响应。这样,我们可以使用路由的 CatchBoundary 组件来渲染任何错误消息。
如果我们想在操作中获取错误对象而不是抛出 Response,我们可以将 throwOnError
选项配置为 true
。我们可以在实例化 Authenticator
或调用 authenticate
时这样做。
如果我们在 Authenticator
中这样做,它将成为所有 authenticate
调用的默认行为。
export let authenticator = new Authenticator<User>(sessionStorage, {
throwOnError: true,
});
或者,我们可以在操作本身中执行此操作。
import { AuthorizationError } from "remix-auth";
export async function action({ request }: ActionFunctionArgs) {
try {
return await authenticator.authenticate("user-pass", request, {
successRedirect: "/dashboard",
throwOnError: true,
});
} catch (error) {
// Because redirects work by throwing a Response, you need to check if the
// caught error is a response and return it or throw it again
if (error instanceof Response) return error;
if (error instanceof AuthorizationError) {
// here the error is related to the authentication process
}
// here the error is a generic error that another reason may throw
}
};
如果我们同时定义了 failureRedirect
和 throwOnError
,则将发生重定向而不是抛出错误。