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
├── lib
│ └── use-session.ts
├── next.config.ts // Configure URL rewrites
├── middleware.ts // Intercept requests and inject the Authorization header before rewriting to the destination backend API
├── 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 3 bước:
3.1 Thay đổi endpoit tới /backend thay vì đi trược tiếp
// packages/client/browser.ts
import {baseClient} from './shared';
export const apiClient = baseClient.extend({
prefixUrl: '/backend'
});
3.2 Cấu hình forward request từ /backend đến API backend
// next.config.ts
const nextConfig: NextConfig = {
async rewrites() {
return [
{
source: '/backend/:path*',
destination: `${process.env.NEXT_PUBLIC_API_URL}/:path*`,
}
];
},
};
3.3. Middleware gắn Authorization trước khi forward đến API backend
Đọc cookie và attach Authorization khi gọi đi tiếp:
// app/middleware.ts
export async function middleware(req: NextRequest) {
const path = req.nextUrl.pathname;
const accessToken = req.cookies.get('session')?.value;
const session = await verifySession()
// Forwarding authentication from the client to backend API routes
if (session && /^\/backend/.test(path)) {
const requestHeaders = new Headers(req.headers);
requestHeaders.set('Authorization', `Bearer ${accessToken}`);
return NextResponse.next({
request: {
headers: requestHeaders,
},
});
}
return NextResponse.next();
}
Tạo hàm verifySession để verify token, chú ý KHÔNG verify token bởi database, api sẽ ảnh hưởng đến hiệu năng.
import {cache} from 'react';
export const verifySession = cache(async () => {
const cookie = (await cookies()).get('session')?.value;
const session = await decrypt(cookie);
if (!session?.userId) {
return null;
}
return { isAuth: true, userId: session.userId }
})
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