Protection des routes Next.js et contrôle d'accès : guide Middleware et défense en profondeur

Lors d’un audit de sécurité client le mois dernier, l’auditeur a froncé les sourcils en regardant mon code de contrôle d’accès : « C’est tout ? Vous n’avez filtré qu’au Middleware ? »
J’étais un peu vexé. Le Middleware n’est-il pas fait pour ça ? Utilisateur non connecté → redirection vers la connexion ; connecté → accès autorisé. Tout semblait hermétique.
L’auditeur a ouvert les outils de développement du navigateur et m’a montré : contourner le routage frontend, appeler l’API backend directement avec Postman — et récupérer facilement des données qui devraient être protégées.
J’étais un peu abasourdi.
Après quelques recherches, j’ai compris : le contrôle d’accès Next.js ne se résume jamais au Middleware seul. Il faut une architecture de défense en profondeur complète.
Cet article aborde : comment utiliser le Middleware ? Quelle relation avec getServerSession ? Comment concevoir un RBAC pour un back-office ? Et des modèles de code prêts à l’emploi.
Si vous travaillez sur le contrôle d’accès Next.js ou si la combinaison Middleware / getServerSession vous pose question, cet article devrait vous aider.
Pourquoi le contrôle d’accès ne peut pas reposer sur le Middleware seul
À quoi sert le Middleware
Commençons par clarifier son rôle.
Il s’exécute sur Edge Runtime, la première couche que touche une requête utilisateur. Vous y faites un filtrage grossier : l’utilisateur est-il connecté ? Admin ou utilisateur standard ? Autoriser ou rediriger ?
Rapide et en amont — idéal pour le contrôle d’accès, non ?
Oui, mais ce n’est pas suffisant seul.
Un cas de faille réel
En janvier, des chercheurs ont divulgué CVE-2025-29927 : l’attaquant ajoute l’en-tête spécial x-middleware-subrequest pour contourner directement le Middleware.
Toute la logique écrite dans le Middleware devient alors inutile face à cette attaque.
Ce n’est pas un cas isolé. En première ligne de défense, le Middleware peut être contourné, mal configuré, ou limité par l’environnement edge pour des jugements de permissions complexes.
La documentation Next.js insiste : « While Middleware can be useful for initial checks, it should not be your only line of defense. »
En clair : ne mettez pas tous vos espoirs dans le Middleware.
Le frontend est bloqué, et le backend ?
J’avais un projet de back-office. Dans le Middleware, contrôle de connexion : accès à /admin sans session → redirection vers la connexion.
Ça paraissait sûr. Clics menu, changements de route — tout passait par le Middleware.
Où était le problème ? L’API n’était pas protégée.
Un collègue QA curieux a ouvert l’onglet Network, repéré l’endpoint de suppression POST /api/users/delete, et l’a appelé avec curl, paramètres au hasard.
La suppression a réussi.
Parce qu’il n’y avait aucun contrôle d’accès dans l’API Route. J’avais bloqué les routes frontend au Middleware, sans penser à quelqu’un qui appellerait l’API directement.
C’est le piège de ne compter que sur le Middleware : il garde la porte d’entrée frontend, pas les fenêtres backend.
L’approche recommandée par Next.js
La doc évoque le « Proximity Principle » — placer le contrôle d’accès au plus près des données.
Concrètement ?
Les données sont en base. Pour les protéger, vérifiez les permissions avant d’y accéder — pas seulement au niveau route ou page, mais au niveau données.
Ce n’est pas dire que le Middleware est inutile. Il peut faire la première interception : redirection si non connecté, refus des rôles manifestement non autorisés. Mais une seule couche ne suffit pas. Il vous faut aussi :
- Contrôles dans les Server Components : avant le rendu, vérifier l’accès à la page
- Contrôles dans les API Routes et Server Actions : avant chaque opération sur les données
- Éventuellement au niveau requêtes base : Row-Level Security ou filtres pour limiter l’accès aux données autorisées
Défense en profondeur : une couche contournée, la suivante reste.
Au début, je trouvais ça lourd. Après avoir pris quelques claques, j’ai compris : là où la sécurité vous semble pénible, les attaquants s’y intéressent.
Middleware et getServerSession : la bonne combinaison
Pourquoi getServerSession ne fonctionne pas dans le Middleware
Au début avec NextAuth, cette question m’a bloqué.
La doc dit d’utiliser getServerSession(authOptions) dans les Server Components. Naturellement, j’ai voulu en faire autant dans le Middleware.
Erreur.
Après investigation : le Middleware tourne sur Edge Runtime, getServerSession exige Node.js Runtime. Incompatibles.
Edge Runtime est l’environnement léger de Vercel, sans l’API Node.js complète, mais rapide et distribué mondialement. Le Middleware privilégie la performance au prix de l’impossibilité d’utiliser tout l’écosystème Node.js.
Comment obtenir la session dans le Middleware alors ?
La bonne approche : getToken ou withAuth
NextAuth propose deux API dédiées au Middleware :
Option 1 : getToken
// middleware.ts
import { getToken } from "next-auth/jwt"
import { NextResponse } from "next/server"
export async function middleware(req) {
const token = await getToken({ req, secret: process.env.NEXTAUTH_SECRET })
if (!token) {
return NextResponse.redirect(new URL('/login', req.url))
}
// 检查角色
if (req.nextUrl.pathname.startsWith('/admin') && token.role !== 'admin') {
return NextResponse.redirect(new URL('/403', req.url))
}
return NextResponse.next()
}
export const config = {
matcher: ['/admin/:path*', '/dashboard/:path*']
}
getToken parse le JWT depuis les cookies. Attention : stratégie session JWT uniquement ; avec session en base, cette approche ne convient pas.
Option 2 : fonction d’ordre supérieur withAuth
// middleware.ts
import { withAuth } from "next-auth/middleware"
export default withAuth({
callbacks: {
authorized: ({ token, req }) => {
// 未登录
if (!token) return false
// admin 路由只允许 admin 角色访问
if (req.nextUrl.pathname.startsWith('/admin')) {
return token.role === 'admin'
}
return true
}
}
})
export const config = {
matcher: ['/admin/:path*', '/dashboard/:path*']
}
withAuth encapsule la logique de redirection. Dans authorized, retournez true ou false ; la redirection vers la connexion est automatique.
Je préfère withAuth : code plus concis.
Où utiliser getServerSession ?
Dans les Server Components, API Routes et Server Actions.
Dans un Server Component :
// app/admin/page.tsx
import { getServerSession } from "next-auth/next"
import { authOptions } from "@/lib/auth"
import { redirect } from "next/navigation"
export default async function AdminPage() {
const session = await getServerSession(authOptions)
if (!session) {
redirect('/login')
}
if (session.user.role !== 'admin') {
redirect('/403')
}
// 渲染页面
return <UsersList />
}
Cette couche vérifie l’accès à la page. Sans permission, pas d’affichage.
Dans une API Route :
// app/api/users/route.ts
import { getServerSession } from "next-auth/next"
import { authOptions } from "@/lib/auth"
import { NextResponse } from "next/server"
export async function DELETE(req: Request) {
const session = await getServerSession(authOptions)
if (!session || session.user.role !== 'admin') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 403 })
}
// 执行删除操作
// ...
return NextResponse.json({ success: true })
}
Dans une Server Action :
// app/actions.ts
'use server'
import { getServerSession } from "next-auth/next"
import { authOptions } from "@/lib/auth"
export async function deleteUser(userId: string) {
const session = await getServerSession(authOptions)
if (!session || session.user.role !== 'admin') {
throw new Error('Unauthorized')
}
// 执行删除
// ...
}
Répartition des rôles
En résumé :
- Middleware (avec getToken) : interception grossière des routes — redirection si non connecté, contrôle de rôle de base
- getServerSession : contrôle fin, dernière vérification avant manipulation des données
Image : le Middleware est le gardien à l’entrée du quartier ; getServerSession, la serrure de votre porte — gardien ou non, sans clé, pas d’entrée.
C’est ça, la défense en profondeur.
Conception RBAC complète pour un back-office
Après Middleware et getServerSession, passons à un RBAC (contrôle d’accès basé sur les rôles) complet pour un vrai back-office.
Modèle RBAC
Principe : Utilisateur → Rôle → Permission.
- Utilisateur (User) : Alice, Bob
- Rôle (Role) : administrateur, éditeur, lecteur
- Permission (Permission) : voir la liste des utilisateurs, éditer un article, supprimer un commentaire
Un utilisateur peut avoir plusieurs rôles ; un rôle regroupe plusieurs permissions. Alice admin → toutes les permissions ; Bob éditeur → édition d’articles et consultation des utilisateurs.
Trois niveaux de granularité :
- Page : accès à une URL (ex.
/admin/users) - Fonction : clic sur un bouton (ex. « Supprimer »)
- Données : accès à un enregistrement (ex. ses propres articles uniquement)
Schéma base typique (Prisma) :
model User {
id String @id @default(cuid())
email String @unique
roles Role[]
}
model Role {
id String @id @default(cuid())
name String @unique
permissions Permission[]
users User[]
}
model Permission {
id String @id @default(cuid())
name String @unique // 如 "user:view", "user:edit"
roles Role[]
}
En pratique, si les rôles sont peu nombreux (admin, éditeur, lecteur), une définition en code peut suffire.
Architecture à quatre couches
Comment déployer le contrôle à chaque niveau.
Couche 1 : Middleware — interception grossière
// middleware.ts
import { withAuth } from "next-auth/middleware"
export default withAuth({
callbacks: {
authorized: ({ token, req }) => {
if (!token) return false
const path = req.nextUrl.pathname
// admin 路径只允许 admin 角色
if (path.startsWith('/admin')) {
return token.role === 'admin'
}
// dashboard 路径要求登录即可
if (path.startsWith('/dashboard')) {
return true
}
return false
}
}
})
export const config = {
matcher: ['/admin/:path*', '/dashboard/:path*']
}
Contrôle de rôle de base uniquement, pas de permissions fonctionnelles détaillées.
Couche 2 : Server Component — permissions au niveau page
// app/admin/users/page.tsx
import { getServerSession } from "next-auth/next"
import { authOptions } from "@/lib/auth"
import { checkPermission } from "@/lib/permissions"
import { redirect } from "next/navigation"
export default async function UsersPage() {
const session = await getServerSession(authOptions)
if (!session) {
redirect('/login')
}
// 检查是否有查看用户列表的权限
const hasPermission = await checkPermission(session.user.id, 'user:view')
if (!hasPermission) {
redirect('/403')
}
// 渲染页面
return <UsersList />
}
Vérifie l’accès à la page. Sans permission, pas d’affichage.
Couche 3 : rendu conditionnel UI — permissions fonctionnelles
// components/UsersList.tsx
'use client'
import { useSession } from "next-auth/react"
import { hasPermission } from "@/lib/permissions-client"
export function UsersList() {
const { data: session } = useSession()
const canEdit = hasPermission(session, 'user:edit')
const canDelete = hasPermission(session, 'user:delete')
return (
<div>
{users.map(user => (
<div key={user.id}>
<span>{user.name}</span>
{canEdit && <button>编辑</button>}
{canDelete && <button>删除</button>}
</div>
))}
</div>
)
}
Masque ou affiche les boutons selon les permissions. Bouton invisible → pas de clic.
Attention : optimisation UX, pas mesure de sécurité. Quelqu’un de technique peut réafficher les boutons dans la console. La vraie sécurité est à la couche suivante.
Couche 4 : Server Action / API — validation avant opération
// app/actions.ts
'use server'
import { getServerSession } from "next-auth/next"
import { authOptions } from "@/lib/auth"
import { checkPermission } from "@/lib/permissions"
export async function deleteUser(userId: string) {
const session = await getServerSession(authOptions)
if (!session) {
throw new Error('Unauthorized')
}
// 必须有删除权限
const hasPermission = await checkPermission(session.user.id, 'user:delete')
if (!hasPermission) {
throw new Error('Forbidden')
}
// 执行删除
await db.user.delete({ where: { id: userId } })
return { success: true }
}
Dernière ligne de défense : avant toute opération sur les données, revérifier les permissions.
Centraliser la configuration des permissions
Répéter les contrôles partout est pénible. Je centralise définitions et logique dans un fichier.
// lib/permissions.ts
export const PERMISSIONS = {
USER_VIEW: 'user:view',
USER_EDIT: 'user:edit',
USER_DELETE: 'user:delete',
POST_VIEW: 'post:view',
POST_EDIT: 'post:edit',
POST_DELETE: 'post:delete',
} as const
export const ROLES = {
ADMIN: {
name: 'admin',
permissions: Object.values(PERMISSIONS) // 管理员有所有权限
},
EDITOR: {
name: 'editor',
permissions: [
PERMISSIONS.USER_VIEW,
PERMISSIONS.POST_VIEW,
PERMISSIONS.POST_EDIT,
]
},
VIEWER: {
name: 'viewer',
permissions: [
PERMISSIONS.USER_VIEW,
PERMISSIONS.POST_VIEW,
]
}
} as const
// 服务端权限检查
export async function checkPermission(userId: string, permission: string) {
const user = await db.user.findUnique({
where: { id: userId },
include: { roles: true }
})
if (!user) return false
// 检查用户的角色是否包含所需权限
const userRole = ROLES[user.role as keyof typeof ROLES]
return userRole?.permissions.includes(permission) ?? false
}
// lib/permissions-client.ts (客户端版本)
import { Session } from "next-auth"
export function hasPermission(session: Session | null, permission: string) {
if (!session?.user) return false
const userRole = ROLES[session.user.role as keyof typeof ROLES]
return userRole?.permissions.includes(permission) ?? false
}
Ajout, suppression, modification des permissions : tout se fait dans ce fichier.
Avantages de cette architecture
Plus de code, mais ça vaut le coup.
- Sécurité : une couche contournée, les autres restent
- Maintenabilité : configuration centralisée, modifications faciles
- UX : boutons masqués quand il faut, pas de message d’erreur après clic
- Audit : contrôles explicites à chaque couche, audits facilités
La mise en place demande du temps au départ ; ensuite, nouvelles fonctionnalités et règles de permissions deviennent bien plus simples.
Cas pratiques et modèles de code
Principes et architecture vus ; voici des modèles prêts à l’emploi et du dépannage.
Modèle Middleware complet
Configuration Middleware multi-rôles avec routes dynamiques :
// middleware.ts
import { withAuth } from "next-auth/middleware"
import { NextResponse } from "next/server"
export default withAuth(
function middleware(req) {
const token = req.nextauth.token
const path = req.nextUrl.pathname
// 根据路径和角色进行细粒度控制
if (path.startsWith('/admin') && token?.role !== 'admin') {
return NextResponse.redirect(new URL('/403', req.url))
}
if (path.startsWith('/editor') && !['admin', 'editor'].includes(token?.role as string)) {
return NextResponse.redirect(new URL('/403', req.url))
}
return NextResponse.next()
},
{
callbacks: {
authorized: ({ token }) => !!token
}
}
)
export const config = {
matcher: [
'/admin/:path*',
'/editor/:path*',
'/dashboard/:path*',
'/api/admin/:path*',
'/api/editor/:path*'
]
}
Points clés :
matcherdéfinit les chemins protégés, avec wildcard:path*authorizedfait le contrôle de connexion minimal- La fonction
middlewareaffine selon les rôles
Menu dynamique
Générer le menu selon les permissions — besoin courant en back-office :
// components/Sidebar.tsx
'use client'
import { useSession } from "next-auth/react"
import Link from "next/link"
import { hasPermission } from "@/lib/permissions-client"
const menuItems = [
{
label: '用户管理',
href: '/admin/users',
permission: 'user:view'
},
{
label: '文章管理',
href: '/admin/posts',
permission: 'post:view'
},
{
label: '系统设置',
href: '/admin/settings',
permission: 'setting:manage'
}
]
export function Sidebar() {
const { data: session } = useSession()
// 根据权限过滤菜单项
const visibleItems = menuItems.filter(item =>
hasPermission(session, item.permission)
)
return (
<nav>
{visibleItems.map(item => (
<Link key={item.href} href={item.href}>
{item.label}
</Link>
))}
</nav>
)
}
Menu et permissions liés clairement. Nouvel item → ajout au tableau menuItems.
Hook réutilisable pour les permissions
// hooks/usePermission.ts
import { useSession } from "next-auth/react"
import { hasPermission } from "@/lib/permissions-client"
export function usePermission(permission: string) {
const { data: session, status } = useSession()
const isLoading = status === 'loading'
const isAllowed = hasPermission(session, permission)
return { isAllowed, isLoading }
}
Usage :
// components/DeleteButton.tsx
'use client'
import { usePermission } from "@/hooks/usePermission"
export function DeleteButton({ userId }: { userId: string }) {
const { isAllowed, isLoading } = usePermission('user:delete')
if (isLoading) return <div>Loading...</div>
if (!isAllowed) return null
return (
<button onClick={() => deleteUser(userId)}>
删除
</button>
)
}
Dépannage courant
Problème 1 : boucle de redirection Middleware
Symptôme : « Trop de redirections » dans le navigateur.
Cause : la page de connexion est aussi interceptée par le Middleware.
Solution : exclure connexion et pages publiques du matcher.
export const config = {
matcher: [
/*
* 匹配所有路径,除了:
* - /login (登录页)
* - /api/auth (NextAuth API)
* - /_next (Next.js 内部)
* - /favicon.ico, /robots.txt (静态文件)
*/
'/((?!login|api/auth|_next|favicon.ico|robots.txt).*)',
]
}
Problème 2 : getServerSession retourne null
Symptôme : connecté pourtant getServerSession renvoie null.
Causes :
authOptionsmal configuré ou non passé- Problème de cookies (cross-domain, HTTPS)
- Usage incorrect de
getServerSessiondans le Middleware
Solutions :
- Vérifier l’import de
authOptions - Utiliser dans Server Component ou API Route, pas dans le Middleware
- En dev, vérifier
NEXTAUTH_URL
// 正确的用法
import { getServerSession } from "next-auth/next"
import { authOptions } from "@/app/api/auth/[...nextauth]/route"
const session = await getServerSession(authOptions)
Problème 3 : performance des contrôles de permissions
Symptôme : pages lentes à chaque chargement à cause des requêtes base.
Cause : pas de cache, requête base à chaque fois.
Solutions :
- Mettre rôle et permissions dans le JWT
// app/api/auth/[...nextauth]/route.ts
export const authOptions: NextAuthOptions = {
callbacks: {
async jwt({ token, user }) {
if (user) {
token.role = user.role
token.permissions = user.permissions
}
return token
},
async session({ session, token }) {
session.user.role = token.role
session.user.permissions = token.permissions
return session
}
}
}
- Cacher côté client avec React Query ou SWR
// hooks/usePermissions.ts
import useSWR from 'swr'
export function usePermissions() {
const { data: permissions } = useSWR('/api/me/permissions', {
revalidateOnFocus: false,
dedupingInterval: 60000 // 1分钟内不重复请求
})
return permissions
}
Checklist rapide
Avant mise en production :
- Le Middleware ne fait-il que du filtrage grossier ?
- Toutes les pages Server Component ont-elles une vérification ?
- Toutes les API Routes sont-elles protégées ?
- Toutes les Server Actions sont-elles protégées ?
- Les boutons sensibles sont-ils masqués selon les permissions ?
- La configuration des permissions est-elle centralisée ?
- Le JWT contient-il le rôle ?
- Connexion et pages publiques exclues du Middleware ?
Tout coché → contrôle d’accès solide.
Conclusion
Retour à la question initiale : comment faire le contrôle d’accès Next.js ?
Réponse : pas de solution unique.
Le Middleware compte : première ligne, bloque la majorité des accès non autorisés. Ce n’est pas tout. Il faut aussi vérifier les pages (Server Components), les opérations (Server Actions, API Routes), et optimiser l’UX côté UI.
L’architecture multi-couches paraît lourde. Mais la sécurité n’a pas de silver bullet. Une couche contournée, les autres tiennent — approche fiable.
Middleware seul vs défense en profondeur
| Dimension | Middleware seul | Défense en profondeur |
|---|---|---|
| Sécurité | Faible, contournable | Élevée, plusieurs filets |
| Coût dev | Faible, un seul endroit | Moyen, contrôles multiples |
| Maintenabilité | Faible, logique dispersée | Bonne, config centralisée |
| UX | Moyenne, erreur après clic | Bonne, fonctions masquées à l’avance |
| Audit | Faible, point unique | Bon, trace à chaque couche |
La défense en profondeur demande plus d’effort, mais les bénéfices sont réels.
Passez à l’action
Sur votre projet Next.js, vérifiez :
- Contrôle d’accès uniquement au Middleware ?
- API Routes et Server Actions avec validation indépendante ?
- Configuration des permissions éparpillée ?
Si oui à l’une de ces questions, migrez vers une architecture multi-couches. Pas besoin de tout d’un coup : commencez par la couche opérations sur les données, puis complétez.
En sécurité, corriger tôt vaut mieux que tard.
Des questions ?
Cet article couvre l’architecture et des cas pratiques, pas tous les scénarios. En projet, vous pourriez rencontrer :
- Permissions au niveau données (voir uniquement ses propres enregistrements)
- Row-Level Security avec Prisma
- Isolation des permissions multi-tenant
Laissez un commentaire — on en discute.
Le contrôle d’accès ne se démode jamais. J’espère que cet article vous évitera quelques pièges.
Configuration complète de la protection multi-couches Next.js
Étapes complètes du contrôle Middleware de base à getServerSession et à la vérification des permissions en base
⏱️ Estimated time: 3 hr
- 1
Step 1: Couche 1 : contrôle de base Middleware
Créer middleware.ts :
```ts
import { withAuth } from 'next-auth/middleware'
export default withAuth({
pages: {
signIn: '/login'
}
})
export const config = {
matcher: ['/dashboard/:path*', '/admin/:path*']
}
```
Rôle :
• Vérifier si l'utilisateur est connecté
• Rediriger vers la page de connexion si non connecté
• Rapide et en amont
Limites :
• Contournement possible (faille x-middleware-subrequest)
• Filtrage grossier uniquement
• Pas de contrôle détaillé des permissions
Point clé : le Middleware est la première couche, pas la seule. - 2
Step 2: Couche 2 : validation détaillée avec getServerSession
Dans la page :
```tsx
import { getServerSession } from 'next-auth'
import { authOptions } from '@/app/api/auth/[...nextauth]/route'
export default async function DashboardPage() {
const session = await getServerSession(authOptions)
if (!session) {
redirect('/login')
}
// 检查用户角色
if (session.user.role !== 'admin') {
redirect('/unauthorized')
}
return <div>Dashboard</div>
}
```
Rôle :
• Validation détaillée de l'identité
• Vérification du rôle utilisateur
• Contrôle fin des permissions
Avantages :
• Plus sûr que le Middleware seul
• Contrôles détaillés possibles
• Non contournable
Point clé : getServerSession est la deuxième couche, validation détaillée. - 3
Step 3: Couche 3 : vérification des permissions en base
Dans l'API Route :
```ts
import { getServerSession } from 'next-auth'
import { db } from '@/lib/db'
export async function GET(request: Request) {
const session = await getServerSession(authOptions)
if (!session) {
return new Response('Unauthorized', { status: 401 })
}
// 数据库权限检查
const user = await db.user.findUnique({
where: { id: session.user.id },
include: { role: { include: { permissions: true } } }
})
const hasPermission = user.role.permissions.some(
p => p.resource === 'users' && p.action === 'read'
)
if (!hasPermission) {
return new Response('Forbidden', { status: 403 })
}
// 返回数据
return Response.json({ users: [...] })
}
```
Rôle :
• Validation finale des permissions
• Vérification en base de données
• Sécurité des données
Point clé : la base est la troisième couche, validation finale. - 4
Step 4: Implémenter le système RBAC
Schéma base de données :
```prisma
model User {
id String @id @default(cuid())
email String @unique
role Role @relation(fields: [roleId], references: [id])
roleId String
}
model Role {
id String @id @default(cuid())
name String @unique
permissions Permission[]
users User[]
}
model Permission {
id String @id @default(cuid())
resource String
action String
roles Role[]
}
```
Vérifier les permissions :
```ts
async function checkPermission(
userId: string,
resource: string,
action: string
) {
const user = await db.user.findUnique({
where: { id: userId },
include: { role: { include: { permissions: true } } }
})
return user.role.permissions.some(
p => p.resource === resource && p.action === action
)
}
```
Points clés :
• L'utilisateur a des rôles
• Les rôles ont des permissions
• Les permissions contrôlent l'accès aux ressources
FAQ
Pourquoi le contrôle d'accès ne peut-il pas reposer sur le Middleware seul ?
Cas de faille :
• CVE-2025-29927 : l'attaquant ajoute l'en-tête x-middleware-subrequest pour contourner le Middleware
• Appel direct de l'API backend avec Postman pour accéder aux données protégées
Rôle du Middleware :
• Edge Runtime, filtrage grossier
• Vérifier connexion et rôle (admin ou utilisateur)
• Rapide et en amont, mais insuffisant seul
Solution : défense en profondeur
• Couche 1 : Middleware (contrôle de base)
• Couche 2 : getServerSession (validation détaillée)
• Couche 3 : vérification en base (validation finale)
Point clé : trois couches pour la sécurité, aucune ne doit manquer.
Quelle différence entre Middleware et getServerSession ?
• Edge Runtime
• Rapide et en amont
• Filtrage grossier (connexion, rôle)
• Contournement possible (x-middleware-subrequest)
getServerSession :
• Node.js Runtime
• Validation détaillée
• Vérification rôle et permissions
• Non contournable
Usage combiné :
• Middleware : contrôle de base (couche 1)
• getServerSession : validation détaillée (couche 2)
• Base : vérification finale (couche 3)
Exemple de code :
```ts
// middleware.ts(第一层)
export default withAuth({
pages: { signIn: '/login' }
})
// page.tsx(第二层)
const session = await getServerSession(authOptions)
if (session.user.role !== 'admin') {
redirect('/unauthorized')
}
// api/route.ts(第三层)
const hasPermission = await checkPermission(userId, 'users', 'read')
if (!hasPermission) {
return new Response('Forbidden', { status: 403 })
}
```
Comment implémenter un système RBAC ?
Schéma base :
```prisma
model User {
id String @id
email String @unique
role Role @relation(fields: [roleId], references: [id])
roleId String
}
model Role {
id String @id
name String @unique
permissions Permission[]
users User[]
}
model Permission {
id String @id
resource String // 资源:users, posts, etc.
action String // 操作:read, write, delete
roles Role[]
}
```
Vérifier les permissions :
```ts
async function checkPermission(
userId: string,
resource: string,
action: string
) {
const user = await db.user.findUnique({
where: { id: userId },
include: { role: { include: { permissions: true } } }
})
return user.role.permissions.some(
p => p.resource === resource && p.action === action
)
}
```
Points clés :
• L'utilisateur a des rôles
• Les rôles ont des permissions
• Les permissions contrôlent l'accès aux ressources
Comment mettre en place une architecture multi-couches ?
Couche 1 : Middleware (contrôle de base)
```ts
// middleware.ts
export default withAuth({
pages: { signIn: '/login' }
})
```
Couche 2 : getServerSession (validation détaillée)
```tsx
// page.tsx
const session = await getServerSession(authOptions)
if (session.user.role !== 'admin') {
redirect('/unauthorized')
}
```
Couche 3 : vérification en base (validation finale)
```ts
// api/route.ts
const hasPermission = await checkPermission(userId, 'users', 'read')
if (!hasPermission) {
return new Response('Forbidden', { status: 403 })
}
```
Points clés :
• Trois couches pour la sécurité
• Aucune couche ne doit manquer
• Middleware filtre grossièrement, getServerSession finement, base valide en dernier
Comment se protéger de la faille x-middleware-subrequest ?
Mesures :
• Ne pas s'appuyer sur le Middleware seul
• Vérifier aussi les permissions dans les API Routes
• Utiliser getServerSession
• Validation finale en base
Exemple :
```ts
// api/route.ts
export async function GET(request: Request) {
// 即使Middleware被绕过,这里也会检查
const session = await getServerSession(authOptions)
if (!session) {
return new Response('Unauthorized', { status: 401 })
}
// 数据库权限检查
const hasPermission = await checkPermission(session.user.id, 'users', 'read')
if (!hasPermission) {
return new Response('Forbidden', { status: 403 })
}
return Response.json({ users: [...] })
}
```
Points clés :
• Défense en profondeur
• Vérifier aussi dans les API Routes
• Validation finale en base
Conseil : ne jamais s'appuyer sur le Middleware seul ; vérifier aussi dans les API Routes.
Quelles sont les bonnes pratiques pour le contrôle d'accès ?
• Middleware : contrôle de base (couche 1)
• getServerSession : validation détaillée (couche 2)
• Base : vérification finale (couche 3)
Système RBAC :
• L'utilisateur a des rôles
• Les rôles ont des permissions
• Les permissions contrôlent l'accès aux ressources
Organisation du code :
• Créer des fonctions utilitaires de vérification
• Les utiliser uniformément dans les API Routes
• Éviter la duplication
Conseils sécurité :
• Ne jamais s'appuyer sur le Middleware seul
• Vérifier aussi dans les API Routes
• Validation finale en base
• Auditer régulièrement la configuration
Points clés :
• Défense en profondeur pour la sécurité
• Aucune couche ne doit manquer
• Améliorer continuellement le système
Rappel : le contrôle d'accès est la base de la sécurité ; ne pas le négliger.
14 min de lecture · Publié le: 19 déc. 2025 · Mis à jour le: 27 juil. 2026
Guide complet Next.js
Si vous arrivez depuis la recherche, le plus rapide est de passer à l’article précédent ou suivant de cette série.
Précédent
Middleware Next.js : guide pratique — matcher, limites Edge Runtime et pièges courants
Du bug en production à la solution complète : matcher, limites Edge Runtime et trois scénarios concrets pour éviter les pièges les plus fréquents avec Next.js Middleware.
Partie 11 sur 51
Suivant
Guide complet OAuth Next.js : Google, GitHub, WeChat — configuration et bonnes pratiques
Comprenez OAuth 2.0 simplement et configurez pas à pas Google, GitHub et WeChat dans Next.js. Résolvez redirect_uri_mismatch, failles de sécurité et autres pièges courants.
Partie 13 sur 51



Commentaires
Connectez-vous avec GitHub pour laisser un commentaire