Changer le thème

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

Easton editorial illustration: cache waterfall instrument

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 :

  1. matcher définit les chemins protégés, avec wildcard :path*
  2. authorized fait le contrôle de connexion minimal
  3. La fonction middleware affine selon les rôles

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 :

  1. authOptions mal configuré ou non passé
  2. Problème de cookies (cross-domain, HTTPS)
  3. Usage incorrect de getServerSession dans 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 :

  1. 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
    }
  }
}
  1. 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

DimensionMiddleware seulDéfense en profondeur
SécuritéFaible, contournableÉlevée, plusieurs filets
Coût devFaible, un seul endroitMoyen, contrôles multiples
MaintenabilitéFaible, logique disperséeBonne, config centralisée
UXMoyenne, erreur après clicBonne, fonctions masquées à l’avance
AuditFaible, point uniqueBon, 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 :

  1. Contrôle d’accès uniquement au Middleware ?
  2. API Routes et Server Actions avec validation indépendante ?
  3. 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. 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. 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. 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. 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 ?
Raison : le Middleware peut être contourné.

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 ?
Middleware :
• 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 ?
RBAC = Role-Based Access Control (contrôle d'accès basé sur les rôles)

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 ?
Trois 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 ?
Faille : l'attaquant ajoute l'en-tête x-middleware-subrequest pour contourner le Middleware.

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 ?
Trois couches :
• 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

Commentaires

Connectez-vous avec GitHub pour laisser un commentaire

Easton BlogEaston Blog