顯示具有 session 標籤的文章。 顯示所有文章
顯示具有 session 標籤的文章。 顯示所有文章

2023-10-26

RemixJS 2 保有登入狀態

RemixJS 2 保有登入狀態

呈上篇 RemixJS 2 以 session 實作登入機制,session 的資料存放在 cookie 裡,那要如何在頁面內判斷及保有登入的狀態?

在前端可以用原本 react 的處理方式,以 context 來保有狀態。在後端必須每個 request 都要檢查 cookie。

get-auth.ts 是將解讀 cookie 的功能寫成模組,用來判斷是否為登入狀態。

// app/modules/get-auth.ts import { getMySession } from "~/modules/sessions"; async function getAuth(request: Request) { const session = await getMySession(request); const userId = session.get("userId") || ""; const nickname = session.get("nickname") || ""; return { userId, nickname, auth: !!userId }; // auth 屬性用來判斷是否登入 } export default getAuth;

AuthContextProvider

// app/contexts/AuthContext.tsx import React, { createContext } from "react"; export type AuthDataType = { userId: string | undefined; nickname: string | undefined; }; export const AuthContext = createContext<AuthDataType>({ userId: "", nickname: "", }); type PropsType = { userId: string | undefined; nickname: string | undefined; children: React.ReactNode; }; export function AuthContextProvider({ userId, nickname, children }: PropsType) { return ( <AuthContext.Provider value={{ userId, nickname }}> {children} </AuthContext.Provider> ); }

將 <AuthContextProvider> 包住 <Outlet />。

// app/root.tsx 部份內容 export async function loader({ request, params }: LoaderFunctionArgs) { console.log("App loader"); return await getAuth(request); } export default function App() { const loaderData = useLoaderData<typeof loader>() return ( <html lang="en"> <head> <meta charSet="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <Meta /> <Links /> </head> <body> <AuthContextProvider userId={loaderData.userId} nickname={loaderData.nickname}> <Outlet /> </AuthContextProvider> <ScrollRestoration /> <Scripts /> <LiveReload /> </body> </html> ); }

防止未授權而使用功能

這裡要分兩個部份 loader 和 action。loader 因為有階層關係,可以在 Layout 的 loader 做阻擋的動作。

// app/routes/address-book.tsx 的 loader 部份 export async function loader({ request, params }: LoaderFunctionArgs) { console.log("address-book loader"); const myAuth = await getAuth(request); if (!myAuth.auth) { // 沒有登入,轉到登入頁面 return redirect(`/login?u=${request.url}`); } return null; }

action 就必須在每個頁面處理。

export async function action({ request, params }: ActionFunctionArgs) { const myAuth = await getAuth(request); if (!myAuth.auth) { // 沒有登入,轉到登入頁面 return redirect(`/login?u=${request.url}`); } // 略 }

2023-10-23

RemixJS 2 以 session 實作登入機制

RemixJS 2 以 session 實作登入機制

一般是以 cookie 存放 session id,session 資料存放在後端,可以是記憶體、檔案或資料庫。 但如果考慮到分散式架構,記憶體和檔案都是不可行的。

以識別用戶而言,其實資料很少一個變數值即可,其餘想放的資料頂多三四個變數值。此時使用官方範例的 createCookieSessionStorage 是個不錯的作法,將資料直接放在 cookie 並加密即可,當然除了好的加密密碼,cookie 最好設定為 httpOnly。

session 工具

使用官方的範例,同時包裝兩個方法 getMySession 和 getHeadersWithSetCookie 省點囉唆的動作。

// app/modules/sessions.ts import { Session, createCookieSessionStorage } from "@remix-run/node"; type SessionData = { userId: string; nickname: string; }; type SessionFlashData = { error: string; }; const { getSession, commitSession, destroySession } = createCookieSessionStorage<SessionData, SessionFlashData>({ cookie: { name: "__session", // domain: "remix.run", // expires: new Date(Date.now() + 60_000), httpOnly: true, // maxAge: 60, path: "/", sameSite: "lax", secrets: ["your_secret_key"], // secure: true, }, }); const getMySession = async (request: Request) => { return await getSession(request.headers.get("Cookie")); }; const getHeadersWithSetCookie = async (session: Session) => { return { "Set-Cookie": await commitSession(session), }; }; export { getSession, commitSession, destroySession, getMySession, getHeadersWithSetCookie, };

登入頁

關於登入頁幾點請注意:

  1. 進入登入頁前,先判斷用戶是否已經登入,若是則轉向別的頁面。
  2. 表單有兩個欄位 email 和 password,email 為 userId。
  3. 帳號和密碼直接寫在程式碼裡是不好的作法,這只是個 demo。
  4. 確定登入後,將兩個值寫入 cookie,分別是 userId 和 nickname。
// app/routes/login.tsx // 略 export async function loader({ request, params }: LoaderFunctionArgs) { const session = await getMySession(request); const userId = session.get("userId"); if (userId) { // 如果已經登入 const uStr = getQueryStringObject(request)["u"]; if (uStr) { return redirect(uStr); } else { return redirect("/"); } } return null; } export default function LoginPage() { // 略 return ( <> <Form method="post" encType="multipart/form-data"> {/* 略 */} <button type="submit" className="btn btn-primary"> 登入 </button> </Form> </> ); } type loginUser = { email: string; nickname: string; password: string }; export async function action({ request, params }: ActionFunctionArgs) { const users: loginUser[] = [ { email: "lsd0125@gmail.com", password: "12345678", nickname: "小嘟" }, { email: "shin@gmail.com", password: "345678", nickname: "肥肥" }, ]; const session = await getMySession(request); const body = await getBodyObject(request); const { email, password } = body; const theUser = users.find((u) => { return u.email === email && u.password === password; }); const output = { success: !!theUser, bodyData: body, }; if (theUser) { session.set("userId", email); session.set("nickname", theUser.nickname); const headers = await getHeadersWithSetCookie(session); return json(output, { headers }); } return output; }

登出功能

登出是直接把兩個值從 cookie 中移除。

// app/routes/logout.tsx import { type LoaderFunctionArgs, redirect } from "@remix-run/node"; import { getMySession, getHeadersWithSetCookie } from "~/modules/sessions"; import { getQueryStringObject } from "~/modules/handle-request-data"; export async function loader({ request, params }: LoaderFunctionArgs) { const session = await getMySession(request); session.unset("userId"); session.unset("nickname"); const headers = await getHeadersWithSetCookie(session); const uStr = getQueryStringObject(request)["u"]; if(uStr){ return redirect(uStr, { headers }); } else { return redirect("/", { headers }); } }

有 userId 後,其它要存放在後端的資料,就可以存到資料庫裡了,不要把太多資料放在 cookie 裡,通常不要超過 4K。

2023-09-03

NextJS app router 實作 session 功能

NextJS app router 實作 session 功能

NextJS 的 app router 所使用的 request 和 response 分別是 NextRequest 和 NextResponse 類型;又分別是 JS 裡 Request 和 Response 類型的延伸。而不像 NodeJS 裡的 IncomingMessage 和 ServerResponse,有著彈性的功能。

比較大的問題是,ExpressJS 裡的 request 和 response 是從頭「流」到尾的機制。我們可以很方便在 middlewares 處理完資料,掛載資料到 request 或 response.locals 的屬性上。

然而 NextJS 的 middleware 卻不是這樣運作的,request 物件可能是透過 clone 的方式再往下一個階段傳遞。所以在 middleware 取得的 request 和在 route.js 裡拿到的 request 是不相同(不同參照)的物件。如此就無法藉由 request 物件來傳遞資料。沒有接收 response 物件的情況下,更難使用 response 物件。

後來才找到 Passing data from middleware to API route,利用 middleware 的 response.rewrite(),將資料放在 cookie 裡,往 end-point 傳遞。

shin-session-obj.js 為實作取得 session 物件的 getSessionObj() 函式。回傳值中若 new_session_id 不為空字串,表示要設定新的 session-id。為了簡明,裡面用 Object 來實作 session 的功能,當然你也可以 DB 或 files 來實作,不過就不在此說明。

// modules/shin-session-obj.js import { validate, v4 as uuidV4 } from "uuid"; let sessions; export function getSessionObj(request) { let new_session_id = ""; if (!request) return {}; // 沒有 request 時 let sid = request.cookies.get("shin-session-id")?.value; // 沒有 session_id 時, 或 session_id 不是 uuid 時, 設定新的 session-id if (!sid || !validate(sid)) { new_session_id = uuidV4(); sid = new_session_id; console.log({new_session_id}) } // 初始化 sessions if (!sessions) { sessions = {}; } // 初始化 session if (!sessions[sid]) { sessions[sid] = {}; } return { session: sessions[sid], new_session_id, }; }

在 middleware 會呼叫 getSessionObj() 以測試是否取得 new_session_id。若取得,則在複製的 request 物件裡設定 cookie 內容為我們要傳下傳遞的 session-id;同時,在 response 物件內設定要用戶端儲存的 session-id(儲存於 cookie)。

import { NextResponse } from "next/server"; import { getSessionObj } from "@/modules/shin-session-obj"; export async function middleware(nextRequest, nextFetchEvent) { const {session, new_session_id} = getSessionObj(nextRequest); if(new_session_id) { const clonedRequest = nextRequest.clone(); clonedRequest.headers.append("Cookie", `shin-session-id=${new_session_id}`); const response = NextResponse.rewrite(nextRequest.url.toString(), { request: clonedRequest, }); response.cookies.set({ name: "shin-session-id", value: new_session_id, path: "/", }); return response; } }

在任何的 route.js 就可以直接使用 getSessionObj() 取得對應的 session 物件。

import { getSessionObj } from "@/modules/shin-session-obj"; export async function GET(request) { const {session} = getSessionObj(request); if(session){ session.count = session.count || 0; session.count++; } const response = new Response(JSON.stringify({session}), { status: 200, headers: { "Content-Type": "application/json", }, }); return response; }

要注意的是此實作,主要是為了說明實作方式,在正式環境使用記憶體存放 session 資料有許多問題。最好還是以 DB 或檔案來存放 session 資料。

2020-05-05

NodeJS 將 session 資料存入 MySQL

NodeJS 將 session 資料存入 MySQL

一般使用 express.js 時,使用的 session 套件為 express-session。使用記憶體存放 session 資料的做法:

const session = require('express-session'); app.use(session({ saveUninitialized: false, resave: false, secret: '你的 cookie 加密字串', cookie: { maxAge: 1200000 // 單位為毫秒 } }));

若要將 session 存入資料庫,需要先安裝 express-mysql-session 套件。設定方式如下,其中的 db_connect2.js 請看 上篇:

const session = require('express-session'); const MysqlStore = require('express-mysql-session')(session); const db = require(__dirname + '/db_connect2'); const sessionStore = new MysqlStore({}, db); app.use(session({ saveUninitialized: false, resave: false, secret: '你的 cookie 加密字串', store: sessionStore, cookie: { maxAge: 1200000 } }));

若使用 session 可以在資料庫看到這樣的資料:

CREATE TABLE IF NOT EXISTS `sessions` ( `session_id` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, `expires` int(11) unsigned NOT NULL, `data` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin ) ENGINE=InnoDB DEFAULT CHARSET=utf8; INSERT INTO `sessions` (`session_id`, `expires`, `data`) VALUES ('8CDH6O91CkkY_1DpJs7h3YmzbqgQeqrF', 1588332706, '{"cookie":{"originalMaxAge":1200000,"expires":"2020-05-01T11:31:42.263Z","httpOnly":true,"path":"/"},"hello":"shinder"}'); ALTER TABLE `sessions` ADD PRIMARY KEY (`session_id`);

FB 留言