Remix Auth
Remix 和 React Router 應用程式提供簡易身份驗證。
為功能
- 完整的伺服器端身份驗證
- 完整的 TypeScript 支援
- 基於策略的身份驗證
- 實作自訂策略
概述
Remix Auth 是一個完整的開放原始碼身份驗證解決方案,適用於 Remix 和 React Router 應用程式。
深受 Passport.js 的啟發,但從頭開始完全重寫,以便在 Web Fetch API 之上運作。Remix Auth 可以輕鬆部署到任何基於 Remix 或 React Router 的應用程式中,並且只需最少的設定。
如同 Passport.js,它使用策略模式來支援不同的身份驗證流程。每個策略都以獨立的 npm 套件發佈。
安裝
若要使用它,請從 npm (或 yarn) 安裝:
npm install remix-auth
此外,請安裝其中一個策略。策略列表可在社群策略討論中找到。
[!TIP] 請檢查策略所支援的 Remix Auth 版本,因為它們可能尚未更新到最新版本。
用法
匯入 Authenticator
類別並使用泛型型別進行實例化,該泛型型別將是您從策略取得的使用者資料的型別。
// Create an instance of the authenticator, pass a generic with what
// strategies will return
export let authenticator = new Authenticator<User>();
User
型別是策略在識別已驗證使用者後將提供給您的任何內容。它可以是完整的使用者資料,或是包含 Token 的字串。這完全取決於您。
之後,請註冊策略。在此範例中,我們將使用 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");
// 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 await login(email, password);
}),
// 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 來驗證使用者。
import { Form } from "react-router";
import { authenticator } from "~/services/auth.server";
// Import this from correct place for your route
import type { Route } from "./+types";
// 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 }: Route.ActionArgs) {
// we call the method with the name of the strategy we want to use and the
// request object
let user = await authenticator.authenticate("user-pass", request);
let session = await sessionStorage.getSession(request.headers.get("cookie"));
session.set("user", user);
throw redirect("/", {
headers: { "Set-Cookie": await sessionStorage.commitSession(session) },
});
}
// Finally, we need to export a loader function to check if the user is already
// authenticated and redirect them to the dashboard
export async function loader({ request }: Route.LoaderArgs) {
let session = await sessionStorage.getSession(request.headers.get("cookie"));
let user = session.get("user");
if (user) throw redirect("/dashboard");
return data(null);
}
可以使用 React Router 的 session storage helper 建立 sessionStorage,由您決定想要使用哪個 session storage 機制,或者您計劃在身份驗證後如何保留使用者資料,也許您只需要一個普通的 Cookie。
進階用法
根據使用者的資料將其重新導向到不同的路由
假設我們有 /dashboard
和 /onboarding
路由,而且使用者驗證後,您需要檢查他們資料中的某些值,以了解他們是否已加入。
export async function action({ request }: Route.ActionArgs) {
let user = await authenticator.authenticate("user-pass", request);
let session = await sessionStorage.getSession(request.headers.get("cookie"));
session.set("user", 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 });
}
處理錯誤
發生錯誤時,驗證器和策略只會拋出錯誤。您可以捕獲它並根據需要進行處理。
export async function action({ request }: Route.ActionArgs) {
try {
return await authenticator.authenticate("user-pass", request);
} catch (error) {
if (error instanceof Error) {
// here the error related to the authentication process
}
throw error; // Re-throw other values or unhandled errors
}
}
[!TIP] 某些策略可能會拋出重新導向回應,這在 OAuth2/OIDC 流程中很常見,因為它們需要將使用者重新導向到身份提供者,然後再返回到應用程式,請確保重新拋出任何未處理的錯誤。在 catch 區塊的開頭使用
if (error instanceof Response) throw error;
來重新拋出任何回應,以防您想要以不同的方式處理它。
登出使用者
因為您負責在登入後保留使用者資料,所以如何處理登出將取決於此。您可以簡單地從 Session 中移除使用者資料,或者您可以建立新的 Session,甚至可以使 Session 無效。
export async function action({ request }: Route.ActionArgs) {
let session = await sessionStorage.getSession(request.headers.get("cookie"));
return redirect("/login", {
headers: { "Set-Cookie": await sessionStorage.destroySession(session) },
});
}
保護路由
若要保護路由,您可以使用 loader
函式來檢查使用者是否已驗證。如果沒有,您可以將其重新導向到登入頁面。
export async function loader({ request }: Route.LoaderArgs) {
let session = await sessionStorage.getSession(request.headers.get("cookie"));
let user = session.get("user");
if (!user) throw redirect("/login");
return null;
}
這不屬於 Remix Auth 的範圍,因為您儲存使用者資料的位置取決於您的應用程式。
一個簡單的方法是建立一個 authenticate
helper。
export async function authenticate(request: Request, returnTo?: string) {
let session = await sessionStorage.getSession(request.headers.get("cookie"));
let user = session.get("user");
if (user) return user;
if (returnTo) session.set("returnTo", returnTo);
throw redirect("/login", {
headers: { "Set-Cookie": await sessionStorage.commitSession(session) },
});
}
然後在您的 loaders 和 actions 中呼叫它:
export async function loader({ request }: Route.LoaderArgs) {
let user = await authenticate(request, "/dashboard");
// use the user data here
}
建立策略
所有策略都擴充了 Remix Auth 所匯出的 Strategy
抽象類別。您可以透過擴充此類別並實作 authenticate
方法來建立自己的策略。
import { Strategy } from "remix-auth/strategy";
export namespace MyStrategy {
export interface VerifyOptions {
// The values you will pass to the verify function
}
}
export class MyStrategy<User> extends Strategy<User, MyStrategy.VerifyOptions> {
name = "my-strategy";
async authenticate(
request: Request,
options: Strategy.AuthenticateOptions
): Promise<User> {
// Your logic here
}
}
在您的 authenticate
方法的某個時刻,您需要呼叫 this.verify(options)
來呼叫應用程式定義的 verify
函式。
export class MyStrategy<User> extends Strategy<User, MyStrategy.VerifyOptions> {
name = "my-strategy";
async authenticate(
request: Request,
options: Strategy.AuthenticateOptions
): Promise<User> {
return await this.verify({
/* your options here */
});
}
}
選項將取決於您傳遞給 Strategy
類別的第二個泛型。
您想要傳遞給 verify
方法的內容取決於您和您的身份驗證流程的需求。
儲存中間狀態
如果您的策略需要儲存中間狀態,您可以覆寫 contructor
方法,以預期一個 Cookie
物件,甚至是一個 SessionStorage
物件。
import { SetCookie } from "@mjackson/headers";
export class MyStrategy<User> extends Strategy<User, MyStrategy.VerifyOptions> {
name = "my-strategy";
constructor(
protected cookieName: string,
verify: Strategy.VerifyFunction<User, MyStrategy.VerifyOptions>
) {
super(verify);
}
async authenticate(
request: Request,
options: Strategy.AuthenticateOptions
): Promise<User> {
let header = new SetCookie({
name: this.cookieName,
value: "some value",
// more options
});
// More code
}
}
header.toString()
的結果將是一個您必須使用 Set-Cookie
標頭傳送到瀏覽器的字串,這可以透過使用標頭拋出重新導向來完成。
export class MyStrategy<User> extends Strategy<User, MyStrategy.VerifyOptions> {
name = "my-strategy";
constructor(
protected cookieName: string,
verify: Strategy.VerifyFunction<User, MyStrategy.VerifyOptions>
) {
super(verify);
}
async authenticate(
request: Request,
options: Strategy.AuthenticateOptions
): Promise<User> {
let header = new SetCookie({
name: this.cookieName,
value: "some value",
// more options
});
throw redirect("/some-route", {
headers: { "Set-Cookie": header.toString() },
});
}
}
然後,您可以使用 @mjackson/headers
套件中的 Cookie
物件來讀取下一個請求中的值。
import { Cookie } from "@mjackson/headers";
export class MyStrategy<User> extends Strategy<User, MyStrategy.VerifyOptions> {
name = "my-strategy";
constructor(
protected cookieName: string,
verify: Strategy.VerifyFunction<User, MyStrategy.VerifyOptions>
) {
super(verify);
}
async authenticate(
request: Request,
options: Strategy.AuthenticateOptions
): Promise<User> {
let cookie = new Cookie(request.headers.get("cookie") ?? "");
let value = cookie.get(this.cookieName);
// More code
}
}
授權
請參閱 LICENSE。