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 SSR và RSC
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