+2

Gắn Authorization header trong Next.js (App Router)

Khi làm việc với JWT trong Next.js (App Router), việc tự động gắn Authorization header vào mọi API request là cực kỳ quan trọng. Bài viết này sẽ hướng dẫn một trong những cách triển khai tính năng với kiến trúc phân tách môi trường rõ ràng.
Tham khảo source code mẫu ở Github: nghiepdev/nextjs-authentication

1. Thiết lập cấu trúc project với pnpm workspace

Sử dụng monorepo pattern để quản lý các môi trường khác nhau:

/app
├── page.tsx
├── layout.tsx
/app/api/[…backend]
├── route.ts  // Proxy route forward request đã xác 
/packages/client
├── browser.ts    // Client component on Browser
├── node.ts       // Client component on Server-side rendering (SSR)
├── package.json 
├── rsc.ts        // React Server Components (RSC), Server Actions, Route Handlers
└── shared.ts

Chỉ định exports:

// packages/client/package.json
{
  "name": "@app/client",
  "type": "module",
  "exports": {
    ".": {
      "react-server": "./rsc.ts",
      "node": "./node.ts",
      "browser": "./browser.ts",
      "default": "./browser.ts",
      "types": "./browser.ts"
    }
  }
}

Import linh hoạt:

import { apiClient } from '@app/client';

2. Xử lý cho React Server Components (RSC)

Với RSC, chúng ta có thể truy cập cookies trực tiếp:

// packages/client/rsc.ts
import {cookies} from 'next/headers';
import {baseClient} from './shared';

export const apiClient = baseClient.extend({
  prefixUrl: process.env.NEXT_PUBLIC_API_URL,
  hooks: {
    beforeRequest: [
      async (request) => {
        const accessToken = (await cookies()).get('session')?.value;
        if (accessToken) {
          request.headers.set('Authorization', `Bearer ${accessToken}`);
        }
      },
    ],
  },
});

3. Xử lý cho môi trường Browser

Do hạn chế của HTTP-only cookies, chúng ta cần 2 bước:

3.1 Thay đổi endpoit /api/backend

// packages/client/browser.ts
import {baseClient} from './shared';

export const apiClient = baseClient.extend({
  prefixUrl: '/api/backend',
});

3.2. Route Handler proxy: gắn Authorization và forward

Đọc cookie trong Route Handler và attach Authorization khi gọi đến backend:
Xem thêm: https://nextjs.org/blog/building-apis-with-nextjs#6-using-nextjs-as-a-proxy-or-forwarding-layer

// app/api/[…backend]/route.ts
import {type NextRequest, NextResponse} from 'next/server';

async function handle(
  req: NextRequest,
  ctx: RouteContext<'/api/[...backend]'>
) {
  const accessToken = req.cookies.get('session')?.value;
  if (accessToken) {
    const {
      backend: [, ...paths],
    } = await ctx.params;
    const nextUrl = new URL(paths.join('/'), process.env.NEXT_PUBLIC_API_URL);
    const response = await fetch(nextUrl, {
      headers: {
        Authorization: `Bearer ${accessToken}`,
      },
    });

    return new NextResponse(response.body, {
      status: response.status,
      statusText: response.statusText,
      // Do not forward all headers to avoid CORS issues
      // headers: response.headers,
    });
  }

  return NextResponse.json(
    {
      message: 'Unauthorized',
    },
    {status: 401}
  );
}

export {
  handle as GET,
  handle as POST,
  handle as PUT,
  handle as PATCH,
  handle as DELETE,
  handle as HEAD,
  handle as OPTIONS,
};

4. Tại sao không dùng Server-Side Rendering (SSR)?

  • Không truy cập được cookies/headers
  • Nếu tới đây bạn chưa hiểu tại sao rất có thể bạn đang nhầm lẫn giữa SSRRSC
import {baseClient} from './shared';

export const client = baseClient.extend({
  hooks: {
    beforeRequest: [
      () => {
        throw new Error(
          `⚠️ You're trying to make a request from a client component on the server side, please check your code.`
        );
      },
    ],
  },
});

5. Cách sử dụng thống nhất

5.1 Dynamic: RSC, Server Actions, Route Handlers

import { apiClient } from '@app/client';

export default async function Page() {
   const user = await apiClient.get('/user');
   return <div>{user.name}</div>;
}

5.2 Client Components: 'use client'

Ví dụ sử dụng với swr, react-query hoặc useEffect, đảm bảo rẳng Api chỉ được gọi ở Browser:

// use-session.ts
'use client';
import useSWR from 'swr';
import {apiClient} from '@app/client';

const fetcher = () => {
  return apiClient.get('/user').json();
};

export function useSession() {
  return useSWR('session', fetcher);
}

5.3 Client Components trong Server-side rendering: 'use client'

Như đã đề cập ở trên nếu gọi trực tiếp trong Client Component trên môi trường Server-side rending sẽ bắn ra lỗi, đây hoàn toàn là điều mong đợi.

'use client'; // Client Component
function ClientComponent() {
   // Please DON'T DO THIS
   const data = apiClient.get('/random').json();
   return <></>
}

Lợi ích chính:

✅ Bảo mật với HTTP-only cookies
✅ Tách biệt logic cho từng môi trường
✅ Dễ dàng maintain và mở rộng

Bài viết chỉ đưa ra ý chính cách tiếp cận, trên thực tế áp dụng vào dự án cần xử lý tối ưu cho các trường hợp, tham khảo đầy đủ ví dụ ở Github: nghiepdev/nextjs-authentication


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí