Open4

Next.jsにおけるデータ処理モデルの選択

goggle555goggle555
  • データ処理モデルの選択
    • React Serverコンポーネントは、サーバーとクライアントの境界を曖昧にする
    • データ処理は、情報が処理され利用可能になる場所を理解する上で重要
    • プロジェクトに適したデータ処理方法を選択する
      • HTTP API: 既存の大規模プロジェクトや組織に推奨
      • Data Access Layer: 新規プロジェクトに推奨
      • Component Level Data Access: プロトタイピングや学習に推奨
goggle555goggle555
  • HTTP API
    • 既存のプロジェクトでサーバーコンポーネントを導入する際の推奨事項:
      • サーバーコンポーネントを既定で安全でない/信頼できないものとして処理する
      • ゼロトラストの概念を適用し、内部ネットワークや信頼ゾーンを想定しない
      • サーバーコンポーネントからクライアントと同様に fetch() を使用してRESTやGraphQLのAPIエンドポイントを呼び出す
      • getStaticProps/getServerSideProps がデータベースに接続している場合は、モデルを統合してAPIエンドポイントに移動
      • 内部ネットワークからのフェッチが安全であることを前提としたアクセス制御に注意が必要
      • 既存のセキュリティプラクティスを維持し、バックエンドチームがJavaScript以外の言語を使用している場合でも機能
    • 利点:
      • クライアントに送信するコードが少なくなる
      • サーバーコンポーネントの利点を活用し、低待機時間でデータウォーターフォールを実行可能
goggle555goggle555
  • Data Access Layer (データアクセス層)
    • 新しいプロジェクトに推奨されるアプローチ:
      • JavaScriptコードベース内に個別のデータアクセス層を作成し、すべてのデータアクセスを統合
      • 一貫したデータアクセスを保証し、承認バグの発生可能性を低減
      • 1つのライブラリに統合することでメンテナンスが容易になる
      • チームの結束を高め、1つのプログラミング言語で統一
      • 実行時のオーバーヘッドが低く、要求のさまざまな部分でメモリ内キャッシュを共有することでパフォーマンスが向上
    • カスタムのデータアクセスチェック:
      • 呼び出し元に渡す前に、カスタムのデータアクセスチェックを提供する内部JavaScriptライブラリを構築
      • HTTPエンドポイントに似ているが、同じメモリモデルを使用
      • すべてのAPIは、現在のユーザーがデータを表示できるか確認してから返す
    • 原則:
      • サーバーコンポーネント関数本体は、要求を発行している現在のユーザーがアクセスを許可されたデータのみを参照すること
data/auth.tsx
import { cache } from 'react';
import { cookies } from 'next/headers';
 
// キャッシュされたヘルパーメソッドを使用すると、手動で渡すことなく、
// 多くの場所で同じ値を簡単に取得できます。
// これにより、サーバーコンポーネントからサーバーコンポーネントに渡すことがなくなり、
// クライアントコンポーネントに渡すリスクが最小限に抑えられます。
export const getCurrentUser = cache(async () => {
  const token = cookies().get('AUTH_TOKEN');
  const decodedToken = await decryptAndValidate(token);
  // 機密トークンやプライベート情報をパブリックフィールドとして含めないでください。
  // クラスを使用して、誤ってオブジェクト全体をクライアントに渡さないようにします。
  return new User(decodedToken.id);
});
data/user-dto.tsx
import 'server-only';
import { getCurrentUser } from './auth';
 
function canSeeUsername(viewer: User) {
  // 現時点では公開情報ですが、変更される可能性があります
  return true;
}
 
function canSeePhoneNumber(viewer: User, team: string) {
  // プライバシールール
  return viewer.isAdmin || team === viewer.team;
}
 
export async function getProfileDTO(slug: string) {
  // 値を渡さず、キャッシュされた値を読み戻し、コンテキストも解決し、遅延を容易にします
 
  // クエリの安全なテンプレート化をサポートするデータベースAPIを使用します
  const [rows] = await sql`SELECT * FROM user WHERE slug = ${slug}`;
  const userData = rows[0];
 
  const currentUser = await getCurrentUser();
 
  // すべてではなく、このクエリに関連するデータのみを返します
  // <https://www.w3.org/2001/tag/doc/APIMinimization>
  return {
    username: canSeeUsername(currentUser) ? userData.username : null,
    phonenumber: canSeePhoneNumber(currentUser, userData.team)
      ? userData.phonenumber
      : null,
  };
}
  • データ転送オブジェクト (DTO)

    • メソッドは、クライアントに転送しても安全なオブジェクトを公開する必要がある
    • これらのオブジェクトを データ転送オブジェクト (DTO) と呼び、クライアントで使用する準備ができていることを明確にする
    • 実際には、サーバーコンポーネントによってのみ使用される可能性がある
  • セキュリティ監査の利点

    • セキュリティ監査では主にデータアクセス層に焦点を当てることができる
    • UIは迅速に反復処理可能
    • 表面領域が小さく、対象となるコードが少ないため、セキュリティの問題を簡単にキャッチできる
import {getProfile} from '../../data/user'
export async function Page({ params: { slug } }) {
  // このページは、機密情報が含まれていないことがわかっているため、
  // このプロファイルを安全に渡すことができます
  const profile = await getProfile(slug);
  ...
}

秘密キーは環境変数に格納できますが、この方法でprocess.envにアクセスできるのはデータアクセス層だけです。

goggle555goggle555
  • Component Level Data Access
    • データベースクエリをサーバーコンポーネントに直接配置するアプローチ:

      • 迅速なイテレーションやプロトタイピングに適している
      • 小規模なチームやリスクを認識し、その監視方法を理解している小規模な製品に最適
    • 監査ポイント:

      • "use client" ファイルを慎重に監査
      • PRを確認する際には、エクスポートされたすべての関数を確認
      • 型シグネチャが広範なオブジェクト (例: User) を受け入れていないかをチェック
      • tokencreditCard などの機密propsが含まれていないか確認
      • phoneNumber のようなプライバシーに関わるフィールドも特に注意
      • クライアントコンポーネントは、ジョブを実行するために必要な最小限のデータのみを受け入れるべき
import Profile from './components/profile.tsx';
 
export async function Page({ params: { slug } }) {
  const [rows] = await sql`SELECT * FROM user WHERE slug = ${slug}`;
  const userData = rows[0];
  // サーバーコンポーネントからクライアントにデータを渡すため、
  // userDataのすべてのフィールドがクライアントに公開されます。
  // これは、`getServerSideProps`で`userData`を返すことと似ています。
  return <Profile user={userData} />;
}
'use client';
// これは、クライアントコンポーネントが必要とするよりもはるかに多くのデータを受け取り、
// サーバーコンポーネントがすべてのデータを渡すことを奨励するため、
// 不適切なpropsインターフェイスです。
// より良い解決策は、プロファイルのレンダリングに必要なフィールドだけを持つ
// 限定されたオブジェクトを受け入れることです。
export default async function Profile({ user }: { user: User }) {
  return (
    <div>
      <h1>{user.name}</h1>
      ...
    </div>
  );
}
  • SQLインジェクション攻撃を避けるために、パラメータ化されたクエリ、またはそれを行うdbライブラリを常に使用する