createSession
isSession
createSessionStorage
createCookieSessionStorage
createMemorySessionStorage
createFileSessionStorage
(node)createWorkersKVSessionStorage
(Cloudflare Workers)createArcTableSessionStorage
(architect, Amazon DynamoDB)session.has(key)
session.set(key, value)
session.flash(key, value)
session.get()
session.unset()
Sessions 是網站的重要組成部分,它允許伺服器識別來自同一個人的請求,尤其是在伺服器端表單驗證或頁面上沒有 JavaScript 時。Sessions 是許多允許使用者「登入」的網站的基本構建模組,包括社交、電子商務、商業和教育網站。
在 Remix 中,sessions 是在每個路由的基礎上(而不是像 express 中間件那樣)在您的 loader
和 action
方法中使用「session 儲存」物件(實現 SessionStorage
介面)來管理的。Session 儲存瞭解如何解析和產生 cookies,以及如何將 session 資料儲存在資料庫或檔案系統中。
Remix 提供了幾種針對常見情境的預建 session 儲存選項,以及一個用於建立您自己的
createCookieSessionStorage
createMemorySessionStorage
createFileSessionStorage
(node)createWorkersKVSessionStorage
(Cloudflare Workers)createArcTableSessionStorage
(architect, Amazon DynamoDB)createSessionStorage
的自訂儲存。這是一個 cookie session 儲存的範例
// app/sessions.ts
import { createCookieSessionStorage } from "@remix-run/node"; // or cloudflare/deno
type SessionData = {
userId: string;
};
type SessionFlashData = {
error: string;
};
const { getSession, commitSession, destroySession } =
createCookieSessionStorage<SessionData, SessionFlashData>(
{
// a Cookie from `createCookie` or the CookieOptions to create one
cookie: {
name: "__session",
// all of these are optional
domain: "remix.run",
// Expires can also be set (although maxAge overrides it when used in combination).
// Note that this method is NOT recommended as `new Date` creates only one date on each server deployment, not a dynamic date in the future!
//
// expires: new Date(Date.now() + 60_000),
httpOnly: true,
maxAge: 60,
path: "/",
sameSite: "lax",
secrets: ["s3cret1"],
secure: true,
},
}
);
export { getSession, commitSession, destroySession };
我們建議在 app/sessions.ts
中設定您的 session 儲存物件,以便所有需要存取 session 資料的路由都可以從同一個位置導入(另請參閱我們的 路由模組限制)。
與會話儲存物件的輸入/輸出是 HTTP Cookie。getSession()
從傳入請求的 Cookie
標頭中檢索目前的會話,而 commitSession()
/destroySession()
則為傳出的響應提供 Set-Cookie
標頭。
您將使用方法來存取您 loader
和 action
函式中的會話。
登入表單可能看起來像這樣
import type {
ActionFunctionArgs,
LoaderFunctionArgs,
} from "@remix-run/node"; // or cloudflare/deno
import { json, redirect } from "@remix-run/node"; // or cloudflare/deno
import { useLoaderData } from "@remix-run/react";
import { getSession, commitSession } from "../sessions";
export async function loader({
request,
}: LoaderFunctionArgs) {
const session = await getSession(
request.headers.get("Cookie")
);
if (session.has("userId")) {
// Redirect to the home page if they are already signed in.
return redirect("/");
}
const data = { error: session.get("error") };
return json(data, {
headers: {
"Set-Cookie": await commitSession(session),
},
});
}
export async function action({
request,
}: ActionFunctionArgs) {
const session = await getSession(
request.headers.get("Cookie")
);
const form = await request.formData();
const username = form.get("username");
const password = form.get("password");
const userId = await validateCredentials(
username,
password
);
if (userId == null) {
session.flash("error", "Invalid username/password");
// Redirect back to the login page with errors.
return redirect("/login", {
headers: {
"Set-Cookie": await commitSession(session),
},
});
}
session.set("userId", userId);
// Login succeeded, send them to the home page.
return redirect("/", {
headers: {
"Set-Cookie": await commitSession(session),
},
});
}
export default function Login() {
const { error } = useLoaderData<typeof loader>();
return (
<div>
{error ? <div className="error">{error}</div> : null}
<form method="POST">
<div>
<p>Please sign in</p>
</div>
<label>
Username: <input type="text" name="username" />
</label>
<label>
Password:{" "}
<input type="password" name="password" />
</label>
</form>
</div>
);
}
然後登出表單可能看起來像這樣
import { getSession, destroySession } from "../sessions";
export const action = async ({
request,
}: ActionFunctionArgs) => {
const session = await getSession(
request.headers.get("Cookie")
);
return redirect("/login", {
headers: {
"Set-Cookie": await destroySession(session),
},
});
};
export default function LogoutRoute() {
return (
<>
<p>Are you sure you want to log out?</p>
<Form method="post">
<button>Logout</button>
</Form>
<Link to="/">Never mind</Link>
</>
);
}
action
中登出(或執行任何變更),而不是在 loader
中。否則,您將使您的使用者容易受到跨站請求偽造攻擊。此外,Remix 僅在呼叫 actions
時才重新呼叫 loaders
。
由於巢狀路由,可能會呼叫多個 loader 來建構單一頁面。當使用 session.flash()
或 session.unset()
時,您需要確保請求中的其他 loader 不會想要讀取它,否則您會遇到競爭條件。通常,如果您使用 flash,您會希望只有一個 loader 讀取它,如果另一個 loader 想要快閃訊息,請為該 loader 使用不同的鍵。
createSession
待辦事項
isSession
如果物件是 Remix 會話,則返回 true
。
import { isSession } from "@remix-run/node"; // or cloudflare/deno
const sessionData = { foo: "bar" };
const session = createSession(sessionData, "remix-session");
console.log(isSession(session));
// true
createSessionStorage
如果需要,Remix 可以輕鬆地將會話儲存在您自己的資料庫中。createSessionStorage()
API 需要一個 cookie
(有關建立 Cookie 的選項,請參閱cookies)以及一組用於管理會話資料的建立、讀取、更新和刪除 (CRUD) 方法。 Cookie 用於持久化會話 ID。
commitSession
呼叫 createData
getSession
呼叫 readData
commitSession
呼叫 updateData
destroySession
呼叫 deleteData
以下範例顯示如何使用一般資料庫客戶端執行此操作
import { createSessionStorage } from "@remix-run/node"; // or cloudflare/deno
function createDatabaseSessionStorage({
cookie,
host,
port,
}) {
// Configure your database client...
const db = createDatabaseClient(host, port);
return createSessionStorage({
cookie,
async createData(data, expires) {
// `expires` is a Date after which the data should be considered
// invalid. You could use it to invalidate the data somehow or
// automatically purge this record from your database.
const id = await db.insert(data);
return id;
},
async readData(id) {
return (await db.select(id)) || null;
},
async updateData(id, data, expires) {
await db.update(id, data);
},
async deleteData(id) {
await db.delete(id);
},
});
}
然後您可以像這樣使用它
const { getSession, commitSession, destroySession } =
createDatabaseSessionStorage({
host: "localhost",
port: 1234,
cookie: {
name: "__session",
sameSite: "lax",
},
});
createData
和 updateData
的 expires
引數是 Cookie 本身過期且不再有效的相同 Date
。 您可以使用此資訊從資料庫中自動清除會話記錄,以節省空間,或確保您不會針對舊的、過期的 Cookie 返回任何資料。
createCookieSessionStorage
對於純粹基於 Cookie 的會話(其中會話資料本身與瀏覽器一起儲存在會話 Cookie 中,請參閱cookies),您可以使用 createCookieSessionStorage()
。
Cookie 會話儲存的主要優點是您不需要任何額外的後端服務或資料庫即可使用它。 它在某些負載平衡的情況下也可能很有用。但是,基於 Cookie 的會話可能不會超過瀏覽器允許的最大 Cookie 長度(通常為 4kb)。
缺點是您幾乎必須在每個 loader 和 action 中執行 commitSession
。 如果您的 loader 或 action 有任何變更會話,則必須提交它。 這表示如果您在 action 中執行 session.flash
,然後在另一個 action 中執行 session.get
,則必須提交它,該快閃訊息才會消失。 使用其他會話儲存策略時,您只需在建立時提交它(瀏覽器 Cookie 不需要變更,因為它不儲存會話資料,只儲存在其他地方找到它的鍵)。
import { createCookieSessionStorage } from "@remix-run/node"; // or cloudflare/deno
const { getSession, commitSession, destroySession } =
createCookieSessionStorage({
// a Cookie from `createCookie` or the same CookieOptions to create one
cookie: {
name: "__session",
secrets: ["r3m1xr0ck5"],
sameSite: "lax",
},
});
請注意,其他會話實作會在 Cookie 中儲存唯一的會話 ID,並使用該 ID 在真實來源(記憶體中、檔案系統、DB 等)中查詢會話。在 Cookie 會話中,Cookie *是*真實來源,因此沒有現成的唯一 ID。如果您需要在 Cookie 會話中追蹤唯一的 ID,您需要透過 session.set()
自己新增一個 ID 值。
createMemorySessionStorage
此儲存將所有 Cookie 資訊保留在您伺服器的記憶體中。
import {
createCookie,
createMemorySessionStorage,
} from "@remix-run/node"; // or cloudflare/deno
// In this example the Cookie is created separately.
const sessionCookie = createCookie("__session", {
secrets: ["r3m1xr0ck5"],
sameSite: true,
});
const { getSession, commitSession, destroySession } =
createMemorySessionStorage({
cookie: sessionCookie,
});
export { getSession, commitSession, destroySession };
createFileSessionStorage
(node)對於檔案備份的會話,請使用 createFileSessionStorage()
。 檔案會話儲存需要檔案系統,但這應該在大多數執行 express 的雲端供應商上都容易取得,可能需要一些額外的設定。
檔案備份會話的優點是只有會話 ID 儲存在 Cookie 中,而其餘資料則儲存在磁碟上的常規檔案中,非常適合超過 4kb 資料的會話。
import {
createCookie,
createFileSessionStorage,
} from "@remix-run/node"; // or cloudflare/deno
// In this example the Cookie is created separately.
const sessionCookie = createCookie("__session", {
secrets: ["r3m1xr0ck5"],
sameSite: true,
});
const { getSession, commitSession, destroySession } =
createFileSessionStorage({
// The root directory where you want to store the files.
// Make sure it's writable!
dir: "/app/sessions",
cookie: sessionCookie,
});
export { getSession, commitSession, destroySession };
createWorkersKVSessionStorage
(Cloudflare Workers)對於Cloudflare Workers KV 備份的會話,請使用 createWorkersKVSessionStorage()
。
KV 備份會話的優點是只有會話 ID 儲存在 Cookie 中,而其餘資料則儲存在一個全域複寫、低延遲的資料儲存中,該資料儲存具有極高的讀取量和低延遲。
import {
createCookie,
createWorkersKVSessionStorage,
} from "@remix-run/cloudflare";
// In this example the Cookie is created separately.
const sessionCookie = createCookie("__session", {
secrets: ["r3m1xr0ck5"],
sameSite: true,
});
const { getSession, commitSession, destroySession } =
createWorkersKVSessionStorage({
// The KV Namespace where you want to store sessions
kv: YOUR_NAMESPACE,
cookie: sessionCookie,
});
export { getSession, commitSession, destroySession };
createArcTableSessionStorage
(架構、Amazon DynamoDB)對於 Amazon DynamoDB 備份的會話,請使用 createArcTableSessionStorage()
。
DynamoDB 備份會話的優點是只有會話 ID 儲存在 Cookie 中,而其餘資料則儲存在一個全域複寫、低延遲的資料儲存中,該資料儲存具有極高的讀取量和低延遲。
# app.arc
sessions
_idx *String
_ttl TTL
import {
createCookie,
createArcTableSessionStorage,
} from "@remix-run/architect";
// In this example the Cookie is created separately.
const sessionCookie = createCookie("__session", {
secrets: ["r3m1xr0ck5"],
maxAge: 3600,
sameSite: true,
});
const { getSession, commitSession, destroySession } =
createArcTableSessionStorage({
// The name of the table (should match app.arc)
table: "sessions",
// The name of the key used to store the session ID (should match app.arc)
idx: "_idx",
// The name of the key used to store the expiration time (should match app.arc)
ttl: "_ttl",
cookie: sessionCookie,
});
export { getSession, commitSession, destroySession };
在使用 getSession
檢索會話後,返回的會話物件具有一些方法和屬性
export async function action({
request,
}: ActionFunctionArgs) {
const session = await getSession(
request.headers.get("Cookie")
);
session.get("foo");
session.has("bar");
// etc.
}
session.has(key)
如果會話具有具有指定 name
的變數,則返回 true
。
session.has("userId");
session.set(key, value)
設定會話值,以供後續請求使用
session.set("userId", "1234");
session.flash(key, value)
設定會話值,該會話值將在第一次讀取時取消設定。 之後,它就消失了。 對於「快閃訊息」和伺服器端表單驗證訊息最有用
import { commitSession, getSession } from "../sessions";
export async function action({
params,
request,
}: ActionFunctionArgs) {
const session = await getSession(
request.headers.get("Cookie")
);
const deletedProject = await archiveProject(
params.projectId
);
session.flash(
"globalMessage",
`Project ${deletedProject.name} successfully archived`
);
return redirect("/dashboard", {
headers: {
"Set-Cookie": await commitSession(session),
},
});
}
現在我們可以在 loader 中讀取訊息。
flash
時,都必須提交會話。 這與您可能習慣的有所不同,在某些情況下,某些類型的 middleware 會自動為您設定 Cookie 標頭。
import { json } from "@remix-run/node"; // or cloudflare/deno
import {
Meta,
Links,
Scripts,
Outlet,
} from "@remix-run/react";
import { getSession, commitSession } from "./sessions";
export async function loader({
request,
}: LoaderFunctionArgs) {
const session = await getSession(
request.headers.get("Cookie")
);
const message = session.get("globalMessage") || null;
return json(
{ message },
{
headers: {
// only necessary with cookieSessionStorage
"Set-Cookie": await commitSession(session),
},
}
);
}
export default function App() {
const { message } = useLoaderData<typeof loader>();
return (
<html>
<head>
<Meta />
<Links />
</head>
<body>
{message ? (
<div className="flash">{message}</div>
) : null}
<Outlet />
<Scripts />
</body>
</html>
);
}
session.get()
從先前的請求存取會話值
session.get("name");
session.unset()
從會話中刪除值。
session.unset("name");
unset
時都必須提交會話
export async function loader({
request,
}: LoaderFunctionArgs) {
// ...
return json(data, {
headers: {
"Set-Cookie": await commitSession(session),
},
});
}