iT邦幫忙

2023 iThome 鐵人賽

DAY 25
0
Modern Web

深入淺出,完整認識 Next.js 13 !系列 第 25

Day 25 - Next.js 13.5:Server Actions 原理與現行版本使用考量

  • 分享至 

  • xImage
  •  

了解如何搭配表單,和在表單以外觸發 Server Actions 後,我們來嘗試理解它背後的原理,以及目前版本,使用 Server Actions 可能存在的資安風險,以及幾個在斟酌要不要使用 Server Actions 時,可以思考的方向。

假如還不知道 Server Actions 是什麼的讀者,建議先閱讀 Day 23Day 24 文章。

瀏覽器如何與 Server 溝通

我們打開瀏覽器 DevTools,觀察一下觸發 Server Actions 時 Network 的狀況:
server action api check

可以發現,當我們按下「提交表單後」,Next 以目前路由 ( /users ) 為 endpoint,打了一支 POST request。而使用者輸入的內容則是帶在 request payload 裡:
https://ithelp.ithome.com.tw/upload/images/20230924/20161853KMgt5R7wHM.png

假如看 source code,可以在 server-action-reducer.ts 檔案中看到,Next 定義了一個 fetchServerAction 的 function,負責發送 POST request,並 return一個 object,儲存要不要 redirect、action 處理結果、revalidation 細節等等資訊:

async function fetchServerAction(
 state: ReadonlyReducerState,
 { actionId, actionArgs }: ServerActionAction
): Promise<FetchServerActionResult> {
 const body = await encodeReply(actionArgs)

 const res = await fetch('', {
   method: 'POST',
   headers: {
     Accept: RSC_CONTENT_TYPE_HEADER,
     [ACTION]: actionId,
     [NEXT_ROUTER_STATE_TREE]: encodeURIComponent(JSON.stringify(state.tree)),
     ...(process.env.__NEXT_ACTIONS_DEPLOYMENT_ID &&
     process.env.NEXT_DEPLOYMENT_ID
       ? {
           'x-deployment-id': process.env.NEXT_DEPLOYMENT_ID,
         }
       : {}),
     ...(state.nextUrl
       ? {
           [NEXT_URL]: state.nextUrl,
         }
       : {}),
   },
   body,
 })
…


}
…
 return {
   redirectLocation,
   revalidatedParts,
 }

接著看到 186 行,ServerActionReducer 執行的事件。ServerActionReducer根據官方註解,負責呼叫 Server Actions 與處理過程產生的 side effects。當中呼叫了 fetchServerAction,並根據 return 的 object 來處理後續 redirection 和 revalidation。

/*
 * This reducer is responsible for calling the server action and processing any side-effects from the server action.
 * It does not mutate the state by itself but rather delegates to other reducers to do the actual mutation.
 */
export function serverActionReducer(
  state: ReadonlyReducerState,
  action: ServerActionAction
): ReducerState {
...
    mutable.inFlightServerAction = createRecordFromThenable(
      fetchServerAction(state, action)
    )
...
}

所以 Server Actions 還是透過 API 來處理 client 與 server 溝通,只是 Next 底層幫你寫好了這支 API。

不用 JavaScript 也能執行 - Progressive Enhancement

Next 在設計 Server Actions 時,採納了 Progressive Enhancement 的策略。讓 <form> 中的 Server Actions,在沒有 JavaScript 的環境也可以執行。

什麼是 Progressive Enhancement 呢?簡單來說,是盡可能讓不同瀏覽器的使用者都能使用網頁的基本功能,再視瀏覽器的情況,決定功能體驗的完整度。

所以當我們禁用瀏覽器 JavaScript,可以發現,仍然可以在 <form> 中觸發 Server Actions:
server actions without javascript

但從影片可以看到,禁用 JavaScript 後,按下提交表單,瀏覽器就沒有發出 Fetch/XHR 請求。那瀏覽器是怎麼跟 Server 達成溝通的呢?

我們切到 DevTools 的 Elements,看一下這份表單的 HTML tag,會發現 <form> 上有三個 attributes:

<form action enctype="multipart/form-data" method="POST">...</form>
  • action:
    指定表單提交後,要將 FormData 傳送到的地方。比方說:<form action="/action_page.php">...</form>,就是將 FormData 傳送到 action_page.php 這個檔案。

    假如沒有帶任何 URL,則傳送到表單所在的檔案。所以我們表單的 FormData 會被傳到表單所在的 html 檔案中。

  • enctype:指定 FormData 加密的方式

  • method:指定表單提交後,送出的 HTTP 請求類型。假如 method 為 POST,則表單提交後,FormData 會被夾在 request body 中。

所以透過 <form> 原生的 attributes,瀏覽器可以在 JavaScript 載入失敗或禁用的情況下,將 FormData 傳給 server 執行 Server Actions。

Progressive Enhencement 只適用於以 action 和 formAction prop 觸發 Server Actions 的情況,假如使用 useTransition 則不適用 Progressive Enhencement。

Request Body 大小限制

為了防止 server 負擔過重,Next 預設 request body 的大小最多為 1MB。假如 request body 需要超過 1MB,可以修改 next.config.js

/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    serverActions: true,
    serverActionsBodySizeLimit: '2mb'
  },
};

module.exports = nextConfig;

大致理解 Server Actions 的原理後,接下來想跟大家分享一個,我在網路上看到關於現行版本 (v13.5),Server Actions 可能存在的資安疑慮,使用上必須務必注意的地方。

將機密資料放在 Server Actions 中

試想一個情境:假如我要拿到用戶資料,需經過以下步驟:

  1. 向 secret manager 取得 DB 連線密碼
  2. 以取得的密碼連線資料庫
  3. 連線成功後,當用戶點擊「取得用戶資料」後,會到 DB 撈用戶資料

所以我寫了一段程式碼:

// 假設密碼是 THIS IS SECRET
const getSecret = async () => {
  ...
  return 'THIS IS SECRET';
};

...

export default async function Page() {
  const secret = await getSecret();

  // 以密碼連線資料庫,並撈取用戶資料
  const getData = async () => {
    'use server';
    const status = await connectToDb(secret);
    status === 'success' && getProfile();
    ...
  };

  return (
    // 提交表單後會撈取用戶資料
    <form
      action={getData}
    >
      <button
        type='submit'
      >
        取得用戶資料
      </button>
    </form>
  );
}

運作起來沒問題,但假如打開 DevTools 的 Network,查看 Next 幫我們寫的 API 的 payload,會發現一件令人害怕的事:
https://ithelp.ithome.com.tw/upload/images/20230924/20161853FYfqaPzU3Q.png

連線資料庫的密碼也被夾在 payload 中。假如打開網頁原始碼,也會發現 <form> 裡面被塞了幾個隱形的 <input>,其中一個的 value 還帶有密碼資訊:

    <form action='' encType='multipart/form-data' method='POST'>
      ...
      <input type='hidden' name='$ACTION_1:1' value='["THIS IS SECRET"]' />
      <button type='submit'>提交表單</button>
    </form>

具體原因還不清楚,可能跟目前 Server Actions 和 components 溝通的機制有關,假如之後有時間我會再試著從從 source code 中找答案。

總之,目前假如在 closure 中去使用外層的變數,以上述例子來說,我在 getData() 去使用 secret 變數,似乎會讓變數也暴露在 request payload 裡,Next 渲染時也會在 HTML 中塞入幾個隱形的 <input>,其中一個 <input> 的 value 會是該變數的值。

所以目前相對安全的做法,是將 secret 宣告在 getData 中:

const getSecret = async () => {
  return 'THIS IS SECRET';
};

export default async function Page() {
  const getData = async () => {
    'use server';
    // 將 secret 宣告在 server action 中
    const secret = await getSecret();
    const status = await connectToDb(secret);
    status === 'success' && getProfile();
  };

  return (
    <form
      action={getData}
    >
      <button
        type='submit'
      >
        取得用戶資料
      </button>
    </form>
  );
}

這樣當 getData()觸發後,secret 就不會出現在 request payload 中:
https://ithelp.ithome.com.tw/upload/images/20230924/20161853p0arCvzvy2.png
<form> 中也不會出現 value 為 secret 的隱藏 <input>

參考資料:
Web Dev Cody: Next's Server Actions Might Not Be That Safe...
Theo-t3.gg: I Fixed Next.js Server Actions

補充:假如有碰到 Jest 無法針對 Server Actions 跑 testing 的問題,可以將 Next 版本升到 v13.5.4 以上,詳情可以參考這個 Pull Request


Server Actions 的介紹就到這邊。這幾天在讀 Server Actions 的內容時,也有找幾個前後端工程師分享這個概念,但大部分的人都不覺得 Server Actions 讓 client 與 server 溝通變得更簡單😆

主要有幾個因素:

  1. 只能支援 Web App:假如 server 不只服務網頁,還有桌面應用程式、iOS/Android App 等等,可能還是需要自行開發 API。
  2. 專案管理問題:表面上是少開發一支 API,實質上只是把前後端程式碼放到同個專案中,底層還是依賴 API 進行 client 和 server 的溝通。當專案規模較大時,可能反而造成專案管理不易,沒有優化開發者體驗。
  3. 安全疑慮:如上一部分所提,目前版本的 Server Actions 觸發邏輯,隱含一些資安疑慮。

總結來說,目前 Server Actions 建議可以在規模較小、後端邏輯較單純的專案上嘗試,不建議使用在 production grade 的產品上。

儘管 Server Actions 目前似乎還存在頗多可討論的空間,但它的確提供了開發個人全端專案上,一個便利的選擇。而且它也還在 alpha 階段,我們就靜待 Vercel 之後的調整吧!

謝謝大家耐心的閱讀,我們明天見!


上一篇
Day 24 - 如何在表單外使用 Server Actions - formAction & useTransition
下一篇
Day 26 - 圖片優化:next/image
系列文
深入淺出,完整認識 Next.js 13 !30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
Jim
iT邦新手 4 級 ‧ 2024-03-13 23:55:50

這一篇寫的好詳細,推推!

S.C iT邦新手 4 級 ‧ 2024-03-14 22:07:39 檢舉

感謝大大支持!希望有幫助到你~

我要留言

立即登入留言