2022年4月26日

使用 Remix、Prisma 和 MongoDB 構建全棧應用:身份驗證

閱讀約15分鐘

歡迎閱讀本系列的第二篇文章,您將學習如何使用 MongoDB、Prisma 和 Remix 從零開始構建一個全棧應用程式!在本部分中,您將為您的 Remix 應用程式設定基於會話的身份驗證。

Build A Fullstack App with Remix, Prisma & MongoDB: Authentication

目錄

簡介

在本系列的上一部分中,您設定了 Remix 專案並啟動了 MongoDB 資料庫。您還配置了 TailwindCSS 和 Prisma,並開始在 schema.prisma 檔案中建模 User 集合。

在本部分中,您將在應用程式中實現身份驗證,允許使用者透過登入和登錄檔單建立帳戶並登入。

注意:此專案的起點可在 GitHub 倉庫的 part-1 分支中找到。如果您想檢視本部分的最終結果,請前往 part-2 分支。

開發環境

為了跟著提供的示例操作,您需要...

注意:這些可選擴充套件為 Tailwind 和 Prisma 提供了非常好的智慧感知和語法高亮。

設定登入路由

您首先需要做的是設定一個 /login 路由,您的登入和登錄檔單將在此處顯示。

要在 Remix 框架中建立路由,請在 app/routes 資料夾中新增一個檔案。該檔案的名稱將用作路由的名稱。有關 Remix 中路由工作原理的更多資訊,請檢視其文件

app/routes 中建立名為 login.tsx 的新檔案,內容如下:

路由檔案的預設匯出是 Remix 渲染到瀏覽器中的元件。

使用 npm run dev 啟動開發伺服器並導航到 https://:3000/login,您應該會看到路由已渲染。

這可以正常工作,但看起來還不夠美觀... 接下來您將透過新增實際的登入表單來美化它。

建立可重用佈局元件

首先,建立一個元件,您將把您的路由包裝在其中以提供一些共享的格式和樣式。您將使用組合模式來建立這個 Layout 元件。

組合

組合是一種模式,您透過元件的 props 向其提供一組子元素。children prop 表示在父元件的開始和結束標籤之間定義的元素。例如,考慮一個名為 Parent 的元件的這種用法:

在這種情況下,<p> 標籤是 Parent 元件的子元素,並將渲染到 Parent 元件中,無論您決定在何處渲染 children prop 值。

要實際操作,請在 app 資料夾內建立一個名為 components 的新資料夾。在該資料夾內建立一個名為 layout.tsx 的新檔案。

在該檔案中,匯出以下函式元件

該元件使用 Tailwind 類來指定您希望元件中包裹的任何內容佔據螢幕的全部寬度和高度,使用等寬字型,並顯示適中的深藍色作為背景。

請注意,children prop 渲染在 <div> 內部。要了解它在實際使用時如何渲染,請檢視下面的程式碼片段:

建立登入表單

現在,您可以將該元件匯入到 app/routes/login.tsx 檔案中,並將您的 <h2> 標籤包裝在新 Layout 元件內,而不是它目前所在的 <div>

構建表單

接下來新增一個登入表單,它包含 emailpassword 輸入框,並顯示一個提交按鈕。在頂部新增一個友好的歡迎資訊,以便使用者進入您的網站時歡迎他們,並使用 Tailwind 的 flex 類將整個表單居中顯示在螢幕上。

此時,您無需擔心 <form> 的 action 指向何處,只需確保其 method 值為 "post" 即可。稍後您將看到 Remix 為我們設定 action 的一些酷炫魔法!

建立表單欄位元件

隨著您新增更多表單,輸入欄位及其標籤將在整個應用程式中進行大量重寫,因此將它們拆分成一個名為 FormField受控元件,以避免程式碼重複。

app/components 中建立一個名為 form-field.tsx 的新檔案,您將在其中構建 FormField 元件。然後新增以下程式碼以開始使用:

這將定義並匯出與您之前在登入表單中相同的標籤和輸入組合,只是此元件將具有可配置選項:

  • htmlFor:用於輸入欄位的 idname 屬性,以及標籤的 htmlFor 屬性的值。
  • label:在標籤中顯示的文字。
  • value:輸入欄位當前的受控值。
  • type可選 允許您設定輸入欄位的 type 屬性,但預設值為 'text'
  • onChange可選 允許您提供一個函式,當輸入欄位的值發生變化時執行。預設為空函式呼叫。

您現在可以用這個元件替換現有的標籤和輸入框。

這將匯入新的 FormField 元件,其狀態將由父元件(在本例中為登入表單)管理。任何對值的更新都將使用 handleInputChange 函式進行跟蹤。

稍後您將回到 FormField 元件來新增錯誤訊息處理,但目前這就足夠了!

添加註冊表單

您還需要一種讓使用者註冊帳戶的方式,這意味著您需要另一個表單。此表單將接收四個值:

  • 電子郵件
  • 密碼
  • 名字
  • 姓氏

為了避免建立與 /login 路由幾乎完全相同的新 /signup 路由,我們將登入表單進行改造,使其可以在兩種不同的操作之間切換:登入和註冊。

在狀態中儲存表單操作

首先,您需要一種方式讓使用者能夠在表單之間切換,並讓您的程式碼能夠區分這些表單。

Login 元件的頂部,在狀態中建立另一個變數來儲存您的 action

注意:預設狀態將是登入介面。

接下來,您需要一種方法來切換要檢視的狀態。在“歡迎來到 Kudos”訊息上方,新增以下按鈕:

此按鈕的文字將根據 action 狀態而變化。onClick 方法將在 'login' 和 'register' 值之間切換狀態。

此頁面上有一些靜態文字,您需要根據正在檢視的表單進行調整。特別是“登入以點贊!”副標題和表單內的“登入”按鈕。

更改表單的副標題,使其在每個表單上顯示不同的訊息。

完全移除登入按鈕,並替換為以下 <button>

這個新按鈕有一個 name 屬性和一個 value 屬性。該值設定為狀態的 action。當您的表單提交時,此值將與表單資料一起作為 _action 傳遞。

注意:此技巧僅在 <button>name 屬性以下劃線開頭時才有效。

根據您選擇的表單,您現在應該會看到更新的訊息。嘗試點選“註冊”和“登入”按鈕幾次。

新增可切換欄位

此頁面上的文字看起來很棒,但兩個表單都顯示相同的輸入欄位。您最後需要做的是在顯示登錄檔單時新增更多欄位。

password 欄位之後新增以下欄位,並確保將新欄位新增到 formData 物件中。

這裡進行了兩處更改:

  1. 您向 formData 狀態添加了兩個新鍵。
  2. 您添加了兩個欄位,它們會根據您正在檢視的是登入表單還是登錄檔單而有條件地渲染

您的登入和登錄檔單現在在視覺上已完成!現在是時候進入下一個部分:使表單功能化。

身份驗證流程

本節是有趣的部分,您將讓您一直設計和構建的一切真正發揮作用!

然而,在繼續之前,您需要在專案中新增一個新的依賴項。執行以下命令:

這將安裝 bcryptjs 庫及其型別定義。稍後您將使用它來雜湊和比較密碼。

身份驗證將基於會話,遵循 Remix Jokes App 教程中身份驗證所使用的模式。

為了更好地瞭解您應用程式的身份驗證流程,請檢視下圖。

為了驗證使用者身份,需要採取一系列步驟,其中有兩種潛在途徑(登入註冊):

  1. 使用者將嘗試登入或註冊。
  2. 表單將被驗證。
  3. 將呼叫 loginregister 函式。
  4. 如果正在登入,伺服器端程式碼將確保存在具有所提供登入詳細資訊的使用者。如果正在註冊帳戶,它將確保不存在具有所提供電子郵件的帳戶。
  5. 如果上述步驟透過,將建立一個新的 cookie 會話,使用者將被重定向到主頁。
  6. 如果某個步驟未透過並且出現問題,使用者將被送回登入或註冊螢幕並顯示錯誤。

首先,在 app 目錄中建立一個名為 utils 的資料夾。您將在此處儲存所有輔助函式、服務和配置檔案。

在該新資料夾內,建立一個名為 auth.server.ts 的檔案,您將在其中編寫身份驗證和會話相關的方法。

注意:Remix 不會將檔案型別前帶有 .server 的檔案與傳送到瀏覽器的程式碼捆綁在一起。

構建註冊函式

您將構建的第一個函式是註冊函式,它將允許使用者建立新帳戶。

app/utils/auth.server.ts 匯出一個名為 register 的非同步函式:

app/utils 中建立另一個新檔案,名為 types.server.ts,並在此檔案中建立並匯出一個 type,定義登錄檔單將提供的欄位。

將該 type 匯入到 app/utils/auth.server.ts 中,並在 register 函式中使用它來描述 user 引數,該引數將包含登錄檔單的資料。

當呼叫此 register 函式並提供一個 user 時,您需要檢查的第一件事是是否已存在具有所提供電子郵件的使用者。

注意:請記住,email 欄位在您的模式中定義為唯一。

建立 PrismaClient 例項

您將使用 PrismaClient 來執行資料庫查詢,但是您的應用程式中還沒有它的例項。

app/utils 資料夾中建立一個名為 prisma.server.ts 的新檔案,您將在其中建立並匯出一個 Prisma Client 例項。

注意:上面已採取預防措施,防止在開發過程中即時過載導致資料庫連線飽和。

您現在有了訪問資料庫的方法。在 app/utils/auth.server.ts 中,匯入例項化的 PrismaClient 並將以下內容新增到 register 函式中:

註冊函式現在將查詢資料庫中具有所提供電子郵件的任何使用者。

這裡使用 count 函式是因為它返回一個數值。如果沒有匹配查詢的記錄,它將返回 0,其求值為 false。否則,將返回大於 0 的值,其求值為 true

如果找到使用者,該函式將返回一個狀態碼為 400json 響應。

更新您的資料模型

現在您可以確保當用戶嘗試註冊時,不會存在另一個使用相同電子郵件的使用者。接下來,register 函式應該建立一個新使用者。但是,我們將儲存一些欄位,它們在 Prisma 模式中尚不存在(firstNamelastName)。

您將把這些資料儲存在 User 模型中一個名為 profile 的欄位中,該欄位包含一個嵌入式文件

開啟您的 prisma/schema.prisma 檔案並新增以下 type 塊:

type 關鍵字用於定義複合型別——允許您在文件內部定義文件。使用複合型別而不是 JSON 型別的好處是,在查詢文件時可以獲得型別安全。

非常有用,因為它使您能夠明確定義資料的形狀,而這些資料本來由於 MongoDB 的靈活特性而可能是流動的,並且能夠包含任何內容。

您尚未將此新的複合型別(嵌入式文件的另一個名稱)用於描述欄位。在您的 User 模型中,新增一個新的 profile 欄位,並使用 Profile 型別作為其資料型別。

太棒了,您的 User 模型現在將包含一個 profile 嵌入式文件。重新生成 Prisma Client 以考慮這些新更改:

注意:您不需要執行 prisma db push,因為您沒有新增任何新的集合或索引。

新增使用者服務

app/utils 中建立另一個名為 user.server.ts 的檔案,所有與使用者相關的函式都將在此處編寫。在該檔案中新增以下函式和匯入:

這個 createUser 函式做了幾件事:

  1. 它對登錄檔單中提供的密碼進行雜湊處理,因為您不應該將其以明文形式儲存。
  2. 它使用 Prisma 儲存新的 User 文件。
  3. 它返回新使用者的 idemail

注意:您可以透過傳入 JSON 物件,在此查詢中直接填充 profile 嵌入式文件的詳細資訊,並且由於 Prisma 生成的型別,您將看到不錯的自動補全功能。

此函式將用於您的 register 函式中,以處理使用者的實際建立。在 app/utils/auth.server.ts 中匯入新的 createUser 函式,並在 register 函式中呼叫它。

現在,當用戶註冊時,如果不存在使用所提供電子郵件的其他使用者,則會建立一個新使用者。如果在使用者建立過程中出現問題,則會將錯誤以及為 emailpassword 傳遞的值返回給客戶端。

構建登入函式

login 函式將接收 emailpassword,因此要開始此函式,請在 app/utils/types.server.ts 中建立一個新的 LoginForm 型別來描述該資料。

然後透過將以下內容新增到 app/utils/auth.server.ts 來建立 login 函式:

上面的程式碼...

  1. ... 匯入新的 typebcryptjs 庫。
  2. ... 查詢具有匹配電子郵件的使用者。
  3. ... 如果未找到使用者或提供的密碼與資料庫中的雜湊值不匹配,則返回 null 值。
  4. ... 如果一切順利,則返回使用者的 idemail

這將確保提供了正確的憑據,併為您提供建立新 cookie 會話所需的資料。

新增會話管理

您現在需要一種方法在使用者登入或註冊帳戶時為其生成一個 cookie 會話。Remix 提供了一種透過其 createCookieSessionStorage 函式來儲存這些 cookie 會話的簡便方法。

將該函式匯入到 app/utils/auth.server.ts 中,並在匯入語句之後直接新增一個新的 cookie 會話儲存:

上面的程式碼建立了一個會話儲存,包含以下幾個設定:

  • name:Cookie 的名稱。
  • secure:如果為 true,則只允許透過 HTTPS 傳送 cookie。
  • secrets:會話金鑰。
  • sameSite:指定是否可以透過跨站點請求傳送 cookie。
  • path:URL 中必須存在的路徑才能傳送 cookie。
  • maxAge:定義 cookie 在自動刪除前允許存在的時長。
  • httpOnly:如果為 true,則不允許 JavaScript 訪問 cookie。

注意:在此處瞭解更多關於不同 cookie 選項的資訊這裡

您還需要在 .env 檔案中設定一個會話金鑰。新增一個名為 SESSION_SECRET 的變數,併為其賦予一個秘密值。例如:

會話儲存已設定完畢。在 app/utils/auth.server.ts 中再建立一個函式,它將實際建立 cookie 會話。

此函式...

  • ... 建立一個新會話。
  • ... 將該會話的 userId 設定為已登入使用者的 id
  • ... 將使用者重定向到您在呼叫此函式時可以指定的路由。
  • ... 在設定 cookie 頭部時提交會話。

現在,當用戶成功註冊或登入時,createUserSession 函式可以在 registerlogin 函式中使用。

處理登入和登錄檔單提交

您已建立了所有建立新使用者和登入使用者所需的函式。現在您將把它們應用於您構建的表單中。

app/routes/login.tsx 中,匯出一個 action 函式。

注意:Remix 會查詢名為 action 的匯出函式,以在您定義的路由上設定 POST 請求。

現在,在 app/utils 內建立一個名為 validators.server.ts 的新檔案,其中包含一些驗證器函式,用於驗證表單輸入。

app/routes/login.tsxaction 函式中,從請求中獲取表單資料並驗證其格式是否正確。

上面的程式碼可能看起來有點嚇人,但簡而言之它...

  • ... 從請求物件中提取表單資料。
  • ... 確保提供了 emailpassword
  • ... 確保如果 _action 值為 "register" 則提供了 firstNamelastName
  • ... 如果出現任何問題,將返回錯誤以及表單欄位值,以便您稍後在這些欄位無效時,用使用者的輸入和錯誤訊息重新填充表單。

您最後需要做的是,如果輸入看起來良好,則實際執行您的 registerlogin 函式。

switch 語句將允許您根據表單中 _action 的值有條件地執行 loginregister 函式。

為了實際觸發此操作,表單需要將資料提交到此路由。幸運的是,Remix 會處理這個問題,因為它在識別到匯出的 action 函式時,會自動將 POST 請求配置到 /login 路由。

如果您嘗試登入或建立帳戶,您應該會看到之後被髮送到主螢幕。成功!🎉

在私有路由上授權使用者

為了提升使用者體驗,您接下來要做的就是根據使用者是否擁有有效會話,自動將其重定向到主頁或登入頁面。

app/utils/auth.server.ts 中,您需要新增一些輔助函式。

這是許多新功能。以上函式將執行以下操作:

  • requireUserId 檢查使用者的會話。如果存在,則成功並僅返回 userId。但是,如果失敗,它會將使用者重定向到登入螢幕。
  • getUserSession 根據請求的 cookie 獲取當前使用者的會話。
  • getUserId 從會話儲存中返回當前使用者的 id
  • getUser 返回與當前會話關聯的整個 user 文件。如果未找到,使用者將被登出。
  • logout 銷燬當前會話並將使用者重定向到登入螢幕。

有了這些,您就可以在您的私有路由上實現一些不錯的授權。

app/routes/index.tsx 中,如果使用者未登入,則透過新增以下內容將其返回到登入螢幕:

注意:Remix 在提供頁面之前執行 loader 函式。這意味著載入器中的任何重定向都會在您的頁面提供之前觸發。

如果您在未登入的情況下嘗試導航到應用程式的基礎路由 (/),您應該會被重定向到登入螢幕,並且 URL 中會帶有一個 redirectTo 引數。

注意:如果您已經登入,可能需要清除您的 cookie。

接下來,做相反的事情。如果已登入使用者嘗試訪問登入頁面,他們應該被重定向到主頁,因為他們已經登入。將以下程式碼新增到 app/routes/login.tsx 中:

新增表單驗證

太棒了!您的登入和登錄檔單均已正常工作,並且您已在私有路由上設定了授權和重定向。您即將達到終點!

最後一件事是新增表單驗證,並顯示從 action 函式返回的錯誤訊息。

更新 FormField 元件,使其能夠處理錯誤訊息。

此元件現在將接收一個錯誤訊息。當用戶開始在該欄位中輸入時,如果顯示了任何錯誤訊息,它將被清除。

在登入表單中,您需要使用 Remix 的 useActionData Hook 來訪問從 action 返回的資料,以便提取錯誤訊息。

此程式碼添加了以下內容:

  1. 捕獲從 action 函式返回的資料。
  2. 設定一個 errors 變數,該變數將以物件形式儲存特定於欄位的錯誤,例如“無效電子郵件”。它還設定了一個 formError 變數,該變數將儲存用於顯示錶單訊息的錯誤訊息,例如“登入不正確”。
  3. 更新 formData 狀態變數,如果可用,則預設使用 action 函式返回的任何值。

如果使用者看到錯誤並切換表單,您需要清除表單和所有顯示的錯誤。使用這些 effects 來實現此目的:

有了這些,您終於可以讓您的表單和欄位知道要顯示哪些錯誤了。

現在,您應該會在註冊和登入表單上看到錯誤訊息和表單重置功能正常工作!

總結與展望

感謝您一直堅持到本節結束,(😉)!雖然內容很多,但希望您能理解以下內容:

  • 如何在 Remix 中設定路由。
  • 如何構建帶驗證的登入和登錄檔單。
  • 基於會話的身份驗證如何工作。
  • 如何透過實現授權來保護私有路由。
  • 在建立和驗證使用者時,如何使用 Prisma 儲存和查詢您的資料。

在本系列的下一部分中,您將構建 Kudos 的主頁和點贊分享功能。您還將為點贊動態新增搜尋和過濾功能。

不要錯過下一篇文章!

訂閱 Prisma 新聞通訊

© . This site is unofficial and not affiliated with Prisma Data, Inc.