Proteção de rotas e controle de acesso no Next.js: guia completo de Middleware e defesa em camadas

No mês passado, durante uma auditoria de segurança para um cliente, o auditor olhou para o código de controle de acesso que eu havia escrito, franziu a testa e perguntou: “É só isso? Você bloqueou o acesso apenas no Middleware?”
Na hora, achei a crítica um pouco injusta. O Middleware não existe justamente para isso? Usuários não autenticados são redirecionados para a página de login, enquanto os autenticados seguem em frente. Parecia uma proteção sem brechas.
Então o auditor abriu as ferramentas de desenvolvedor do navegador e fez uma demonstração: ignorou a rota do frontend, chamou diretamente a API do backend com Postman e obteve, sem dificuldade, dados que deveriam estar protegidos.
Fiquei sem reação.
Depois de pesquisar bastante, entendi que o controle de acesso no Next.js nunca deve depender apenas do Middleware. É preciso criar uma arquitetura completa de defesa em camadas.
Neste artigo, vamos ver como usar o Middleware, qual é a relação dele com getServerSession, como projetar um sistema RBAC para um painel administrativo e quais modelos de código podem ser aproveitados diretamente.
Se você está implementando controle de acesso no Next.js ou ainda tem dúvidas sobre como combinar Middleware e getServerSession, este guia deve ajudar.
Por que o controle de acesso não pode depender apenas do Middleware
Para que o Middleware realmente serve
Primeiro, precisamos definir o papel do Middleware.
Ele é executado no Edge Runtime e representa uma das primeiras camadas alcançadas quando a requisição do usuário chega à aplicação. Ali, você pode fazer uma “triagem inicial”: verificar se o usuário está autenticado, identificar se ele é administrador ou usuário comum e decidir se a requisição deve seguir ou ser redirecionada.
É rápido e atua cedo. Parece ideal para controle de acesso, certo?
De fato, ele é útil. O problema é depender somente dele.
Um caso real de vulnerabilidade
Em janeiro deste ano, pesquisadores de segurança divulgaram a vulnerabilidade CVE-2025-29927. Em termos simples, um invasor podia adicionar o campo especial x-middleware-subrequest ao cabeçalho da requisição e contornar diretamente as verificações do Middleware.
Nesse tipo de ataque, toda a lógica escrita no Middleware se torna ineficaz.
Não é um caso isolado. Como camada mais externa, o Middleware pode ser contornado, configurado incorretamente ou limitado pelo ambiente de borda, que não é adequado para decisões complexas de autorização.
A documentação oficial do Next.js também destaca esse ponto: “While Middleware can be useful for initial checks, it should not be your only line of defense.”
Em outras palavras: não aposte toda a segurança no Middleware.
O frontend foi bloqueado, mas e o backend?
Em um painel administrativo que desenvolvi, coloquei uma verificação de login no Middleware. Usuários não autenticados que acessassem a rota /admin eram redirecionados para a página de login.
Parecia seguro. Ao clicar no menu e navegar pelas rotas, o usuário sempre passava pela verificação do Middleware.
Onde estava o problema? A API não tinha proteção.
Um colega de testes, bastante curioso, abriu o painel Network e percebeu que a interface de exclusão de usuários chamava POST /api/users/delete. Ele executou a requisição diretamente com curl e preencheu os parâmetros com valores quaisquer.
O usuário foi excluído.
A API Route simplesmente não verificava permissões. Eu havia protegido a porta do frontend com o Middleware, mas não considerei que alguém poderia entrar pela janela e chamar a API diretamente.
Esse é o problema de depender apenas do Middleware: ele consegue controlar a entrada do frontend, mas não protege sozinho as operações do backend.
A abordagem recomendada pelo Next.js
A documentação oficial apresenta o “Proximity Principle”: coloque a verificação de autorização o mais perto possível dos dados.
O que isso significa?
Os dados estão no banco. Para protegê-los, a permissão deve ser verificada antes de acessá-los. Não apenas na rota ou na página, mas também na camada de dados.
Isso não significa que o Middleware seja inútil. Ele pode fazer a primeira interceptação: redirecionar quem não está autenticado e rejeitar funções claramente não autorizadas. Mas essa camada não basta. Você também precisa de:
- Verificações em Server Components: antes de renderizar a página, confirme se o usuário pode acessá-la
- Verificações em API Routes e Server Actions: valide novamente a permissão antes de cada operação de dados
- Verificações na camada de consulta ao banco de dados: use Row-Level Security ou filtros de consulta para garantir que cada usuário só acesse os dados autorizados
É uma defesa em camadas. Se uma delas for contornada, ainda haverá outra.
No começo, eu também achava trabalhoso demais. Depois de enfrentar problemas reais, percebi que os pontos de segurança que parecem incômodos para você são justamente os que mais interessam a um invasor.
Como combinar Middleware e getServerSession corretamente
Por que getServerSession não funciona no Middleware
Quando comecei a usar NextAuth, essa questão também me confundiu.
A documentação mostra que, em um Server Component, a sessão é obtida com getServerSession(authOptions). Naturalmente, tentei fazer o mesmo no Middleware.
O resultado foi um erro.
Depois de pesquisar, entendi o motivo: o Middleware é executado no Edge Runtime, enquanto getServerSession precisa do Node.js Runtime. Os dois não são compatíveis nesse cenário.
O Edge Runtime é um ambiente leve criado pela Vercel. Ele não oferece todas as APIs do Node.js, mas é rápido e distribuído globalmente. O Middleware usa esse ambiente em busca de desempenho; em troca, nem tudo que funciona no Node.js estará disponível ali.
Então, como obter a sessão no Middleware?
A abordagem correta: getToken ou withAuth
O NextAuth oferece duas APIs específicas para Middleware.
Opção 1: usar 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 analisa o JWT armazenado no cookie da requisição e recupera os dados do usuário. Observe que ele só funciona com a estratégia de sessão JWT; se você usa database session, essa abordagem não serve.
Opção 2: usar a função de ordem superior 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 é uma função de encapsulamento que cuida da lógica de redirecionamento. Você só precisa retornar true ou false no callback authorized, e ela redirecionará automaticamente para a página de login quando necessário.
Pessoalmente, prefiro withAuth, porque o código fica mais enxuto.
Onde usar getServerSession?
Use getServerSession em Server Components, API Routes e Server Actions.
Em um 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 />
}
Essa camada verifica se o usuário pode acessar a página. Sem permissão, o conteúdo não é exibido.
Em uma 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 })
}
Em uma 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')
}
// 执行删除
// ...
}
A divisão de responsabilidades
Em resumo:
- Middleware, com getToken: interceptação de rotas em granularidade ampla, como redirecionar usuários não autenticados e fazer verificações básicas de função
- getServerSession: controle granular de acesso, com uma validação final antes de manipular dados
Pense no Middleware como o segurança na entrada de um condomínio, que impede a passagem de quem claramente não deveria entrar. getServerSession é a fechadura da sua casa: mesmo que o segurança deixe alguém passar, sem a chave essa pessoa não entra.
Essa é a ideia da defesa em camadas.
Projeto completo de RBAC para um painel administrativo
Até aqui, vimos como combinar Middleware e getServerSession, mas ainda são peças separadas. Para criar um painel administrativo de verdade, você precisa de um sistema completo de RBAC, ou controle de acesso baseado em funções.
Entenda primeiro o modelo RBAC
A ideia central do RBAC é: usuário → função → permissão.
- Usuário, ou User: pessoas como João e Maria
- Função, ou Role: administrador, editor ou visualizador
- Permissão, ou Permission: visualizar a lista de usuários, editar artigos ou excluir comentários
Um usuário pode ter várias funções, e uma função pode incluir várias permissões. Por exemplo, João pode ser administrador e ter todas as permissões; Maria pode ser editora e ter permissão apenas para editar artigos e visualizar a lista de usuários.
As permissões podem ter três níveis de granularidade:
- Página: pode ou não acessar uma página, como
/admin/users - Funcionalidade: pode ou não usar um botão, como “Excluir”
- Dados: pode ou não ver um registro específico, como visualizar somente os artigos que criou
O modelo do banco de dados pode ser semelhante a este Prisma Schema:
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[]
}
Em projetos reais, talvez você não precise de um banco tão complexo. Se houver poucas funções, como administrador, editor e visualizador, pode ser suficiente defini-las diretamente no código.
Arquitetura de defesa em quatro camadas
Agora vamos aplicar as verificações de permissão em cada camada.
Primeira camada: Middleware — interceptação ampla
// 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*']
}
Essa camada faz apenas uma verificação básica de função, sem entrar nas permissões de cada funcionalidade.
Segunda camada: Server Component — permissão de página
// 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 />
}
Essa camada verifica se o usuário pode acessar a página. Sem a permissão necessária, ele é redirecionado.
Terceira camada: renderização condicional na interface — permissão de funcionalidade
// 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>
)
}
Essa camada mostra ou oculta os botões conforme as permissões. Se o usuário não vê o botão, naturalmente não tentará usá-lo.
Mas atenção: isso melhora a experiência do usuário, não a segurança. Quem entende de tecnologia pode alterar a interface pelo console do navegador. A proteção real está na próxima camada.
Quarta camada: Server Action ou API — validação antes de alterar dados
// 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 }
}
Essa é a última linha de defesa. Independentemente das verificações anteriores, a permissão deve ser validada mais uma vez antes da operação de dados.
Centralize a configuração de permissões
Repetir a mesma verificação em toda parte dá trabalho. Minha abordagem é centralizar as definições e a lógica de permissão em um único arquivo.
// 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
}
Assim, toda inclusão, remoção ou alteração de permissões é feita em um único lugar, sem precisar procurar por definições espalhadas pelo projeto.
Vantagens dessa arquitetura
Vale a pena escrever código em tantas camadas?
Sim.
- Segurança: se uma camada for contornada, as demais continuam protegendo os dados
- Manutenção: as permissões ficam centralizadas e são mais fáceis de alterar
- Experiência do usuário: funções indisponíveis podem ser ocultadas antes que alguém tente usá-las
- Auditoria: cada camada tem verificações explícitas, facilitando a revisão de segurança
Montar essa arquitetura dá algum trabalho no início. Porém, quando chegar a hora de acrescentar funcionalidades ou mudar as regras de acesso, ela economizará muito tempo.
Exemplo prático e modelos de código
Depois dos princípios e da arquitetura, vejamos alguns modelos de código prontos para adaptação e os problemas mais comuns.
Modelo completo de configuração do Middleware
Este exemplo oferece uma configuração completa do Middleware, com várias funções e correspondência dinâmica de rotas:
// 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*'
]
}
Pontos principais:
matcherdefine quais caminhos precisam de proteção e aceita o curinga:path*- O callback
authorizedfaz a verificação básica de autenticação - A função
middlewareaplica verificações mais específicas de função
Renderização dinâmica do menu
Gerar o menu dinamicamente conforme as permissões é uma necessidade comum em painéis administrativos. Veja uma implementação simples:
// 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>
)
}
Associar cada item do menu a uma permissão deixa a configuração clara. Para acrescentar um item, basta incluí-lo no array menuItems.
Hook reutilizável para verificar permissões
Encapsule a lógica em um React Hook para facilitar seu uso nos componentes do cliente:
// 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 }
}
O uso fica bem simples:
// 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>
)
}
Solução de problemas comuns
Problema 1: loop infinito de redirecionamento no Middleware
Sintoma: ao acessar a página, o navegador informa que houve “redirecionamentos demais”.
Causa: a própria página de login está sendo interceptada pelo Middleware, criando um ciclo.
Solução: exclua a página de login e as páginas públicas no 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).*)',
]
}
Problema 2: getServerSession retorna null
Sintoma: embora o usuário esteja autenticado, getServerSession sempre retorna null.
Causas:
authOptionsestá incorreto ou não foi fornecido- Há um problema de configuração de cookie, como domínio cruzado ou HTTPS
getServerSessionfoi usado incorretamente no Middleware
Soluções:
- Verifique se
authOptionsfoi importado corretamente - Use a função somente em Server Components ou API Routes, nunca no Middleware
- No ambiente de desenvolvimento, verifique se a variável
NEXTAUTH_URLestá correta
// 正确的用法
import { getServerSession } from "next-auth/next"
import { authOptions } from "@/app/api/auth/[...nextauth]/route"
const session = await getServerSession(authOptions)
Problema 3: desempenho ruim na verificação de permissões
Sintoma: cada carregamento de página é lento porque consulta o banco para validar as permissões.
Causa: a verificação não usa cache, então cada requisição faz uma nova consulta.
Soluções:
- Coloque a função e as permissões do usuário no JWT para evitar consultas ao banco
// 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
}
}
}
- Use React Query ou SWR no cliente para armazenar em cache o resultado da consulta de permissões
// 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 rápido
Antes de colocar o projeto em produção, confira o controle de acesso:
- O Middleware faz somente verificações amplas?
- Todas as páginas em Server Components validam as permissões?
- Todas as API Routes validam as permissões?
- Todas as Server Actions validam as permissões?
- Botões sensíveis são ocultados conforme as permissões?
- A configuração de permissões está centralizada?
- O JWT contém as informações de função?
- A página de login e as páginas públicas estão fora do Middleware?
Se todos os itens estiverem marcados, seu controle de acesso estará bem encaminhado.
Conclusão
Voltando à pergunta inicial: como implementar controle de acesso no Next.js?
A resposta é: não dependa de uma única técnica.
O Middleware é importante. Ele representa a primeira linha de defesa e bloqueia grande parte dos acessos não autorizados. Mas não resolve tudo. Você também precisa verificar a permissão da página nos Server Components, validar as operações em Server Actions e API Routes e melhorar a experiência do usuário na camada de interface.
Essa arquitetura de defesa em camadas parece trabalhosa à primeira vista. Porém, não existe solução mágica para segurança. Se uma camada for contornada, as demais continuarão protegendo os dados — e é isso que torna a abordagem confiável.
Apenas Middleware versus defesa em camadas
| Critério | Apenas Middleware | Defesa em camadas |
|---|---|---|
| Segurança | Baixa; pode ser contornado | Alta; várias camadas se complementam |
| Custo de desenvolvimento | Baixo; uma única configuração | Médio; exige verificações em vários pontos |
| Manutenção | Ruim; lógica de permissões dispersa | Boa; configuração centralizada |
| Experiência do usuário | Regular; o usuário só descobre a restrição após tentar | Boa; recursos indisponíveis são ocultados antes |
| Facilidade de auditoria | Ruim; um único ponto de verificação | Boa; há registros em cada camada |
A defesa em camadas exige mais trabalho, mas os benefícios são concretos.
Comece agora
Se você está desenvolvendo um projeto Next.js, faça estas verificações:
- O projeto controla as permissões apenas no Middleware?
- API Routes e Server Actions têm validações de autorização independentes?
- A configuração de permissões está espalhada por vários arquivos?
Se alguma resposta indicar um problema, vale refatorar o sistema para uma arquitetura em camadas. Não é preciso fazer tudo de uma vez: comece adicionando verificações às operações de dados mais importantes e, depois, complete as outras camadas.
Problemas de segurança são mais fáceis de corrigir cedo do que tarde.
Ainda tem dúvidas?
Este artigo abordou a arquitetura central e soluções práticas para o controle de acesso no Next.js, mas não consegue cobrir todos os cenários. Se você encontrar outros desafios em um projeto real, como:
- permissões em nível de dados, para visualizar somente os próprios registros
- integração com Row-Level Security no Prisma
- isolamento de permissões em sistemas multi-tenant
Compartilhe nos comentários para continuarmos a discussão.
Controle de acesso é um tema que nunca perde importância. Espero que este artigo ajude você a evitar alguns problemas pelo caminho.
Como configurar uma defesa de permissões em camadas no Next.js
Processo completo, da verificação básica no Middleware à validação detalhada com getServerSession e à checagem de permissões no banco de dados
⏱️ Estimated time: 3 hr
- 1
Step 1: Primeira camada: verificação básica no Middleware
Crie o arquivo middleware.ts:
```ts
import { withAuth } from 'next-auth/middleware'
export default withAuth({
pages: {
signIn: '/login'
}
})
export const config = {
matcher: ['/dashboard/:path*', '/admin/:path*']
}
```
Funções:
• Verificar se o usuário está autenticado
• Redirecionar quem não estiver autenticado para a página de login
• Atuar rapidamente e logo no início da requisição
Limitações:
• Pode ser contornado, como na vulnerabilidade x-middleware-subrequest
• Faz apenas uma triagem inicial
• Não é adequado para verificações detalhadas de permissão
Ponto principal: o Middleware é a primeira camada de defesa, mas não pode ser a única. - 2
Step 2: Segunda camada: validação detalhada com getServerSession
Use na página:
```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>
}
```
Funções:
• Validar a identidade do usuário em detalhes
• Verificar a função do usuário
• Aplicar controle de acesso granular
Vantagens:
• É mais seguro do que depender apenas do Middleware
• Permite verificações detalhadas de permissão
• Não pode ser contornado da mesma forma
Ponto principal: getServerSession é a segunda camada e faz a validação detalhada. - 3
Step 3: Terceira camada: verificação de permissões no banco de dados
Faça a verificação na 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: [...] })
}
```
Funções:
• Fazer a validação final da autorização
• Consultar as permissões registradas no banco de dados
• Proteger os dados
Ponto principal: o banco de dados é a terceira camada e faz a verificação final. - 4
Step 4: Implemente um sistema RBAC
Schema do banco de dados:
```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[]
}
```
Verifique a permissão:
```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
)
}
```
Pontos principais:
• Usuários têm funções
• Funções têm permissões
• Permissões controlam o acesso aos recursos
FAQ
Por que o controle de acesso não pode depender apenas do Middleware?
Exemplo de vulnerabilidade:
• CVE-2025-29927: um invasor pode adicionar o campo x-middleware-subrequest ao cabeçalho da requisição e contornar diretamente as verificações do Middleware
• Também é possível chamar a API do backend diretamente com Postman e obter dados que deveriam estar protegidos
Papel do Middleware:
• É executado no Edge Runtime e faz uma triagem inicial
• Verifica se o usuário está autenticado e se é administrador ou usuário comum
• É rápido e atua cedo, mas não basta sozinho
Solução: defesa em camadas
• Primeira camada: Middleware, com verificações básicas
• Segunda camada: getServerSession, com validação detalhada
• Terceira camada: banco de dados, com a decisão final de autorização
Ponto principal: as três camadas reforçam a segurança e nenhuma delas deve ser omitida.
Qual é a diferença entre Middleware e getServerSession?
• É executado no Edge Runtime
• É rápido e atua cedo
• Faz apenas uma triagem inicial, como verificar login e função do usuário
• Pode ser contornado, como na vulnerabilidade x-middleware-subrequest
getServerSession:
• É executado no Node.js Runtime
• Permite validações detalhadas
• Verifica funções e permissões do usuário
• Não pode ser contornado da mesma forma
Como usá-los juntos:
• Middleware faz a verificação básica, na primeira camada
• getServerSession faz a validação detalhada, na segunda camada
• O banco de dados faz a verificação final, na terceira camada
Exemplo de código:
```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 })
}
```
Como implementar um sistema RBAC?
Schema do banco de dados:
```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[]
}
```
Verifique a permissão:
```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
)
}
```
Pontos principais:
• Usuários têm funções
• Funções têm permissões
• Permissões controlam o acesso aos recursos
Como implementar uma arquitetura de defesa em camadas?
Primeira camada: Middleware, com verificações básicas
```ts
// middleware.ts
export default withAuth({
pages: { signIn: '/login' }
})
```
Segunda camada: getServerSession, com validação detalhada
```tsx
// page.tsx
const session = await getServerSession(authOptions)
if (session.user.role !== 'admin') {
redirect('/unauthorized')
}
```
Terceira camada: banco de dados, com a verificação final
```ts
// api/route.ts
const hasPermission = await checkPermission(userId, 'users', 'read')
if (!hasPermission) {
return new Response('Forbidden', { status: 403 })
}
```
Pontos principais:
• As três camadas reforçam a segurança
• Nenhuma camada deve ser omitida
• O Middleware faz a triagem inicial, getServerSession faz a validação detalhada e o banco de dados toma a decisão final
Como se proteger da vulnerabilidade x-middleware-subrequest?
Medidas de proteção:
• Não dependa apenas do Middleware
• Verifique as permissões também nas API Routes
• Use getServerSession para validar a sessão
• Faça a verificação final no banco de dados
Exemplo de código:
```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: [...] })
}
```
Pontos principais:
• Adote uma defesa em camadas
• Verifique as permissões também nas API Routes
• Use o banco de dados para a decisão final
Recomendação: nunca dependa apenas do Middleware; verifique as permissões também nas API Routes.
Quais são as boas práticas de controle de acesso?
• Middleware para verificações básicas, na primeira camada
• getServerSession para validações detalhadas, na segunda camada
• Banco de dados para a decisão final, na terceira camada
Sistema RBAC:
• Usuários têm funções
• Funções têm permissões
• Permissões controlam o acesso aos recursos
Organização do código:
• Crie funções utilitárias para verificar permissões
• Use-as de forma consistente nas API Routes
• Evite código duplicado
Recomendações de segurança:
• Nunca dependa apenas do Middleware
• Verifique as permissões também nas API Routes
• Use o banco de dados para a decisão final
• Revise a configuração de permissões regularmente
Pontos principais:
• Uma defesa em camadas melhora a segurança
• Nenhuma camada deve ser omitida
• Aprimore o sistema de permissões continuamente
Lembre-se: o controle de acesso é a base da segurança e deve ser tratado com cuidado.
17 min de leitura · Publicado em: 19 dez 2025 · Atualizado em: 4 set 2026
Guia completo Next.js
Se você chegou pela busca, o caminho mais rápido é ir para o post anterior ou próximo desta série.
Anterior
Guia prático de Next.js Middleware: matcher, limites do Edge Runtime e armadilhas comuns
De bugs em produção à solução completa: entenda o matcher do Next.js Middleware, os limites do Edge Runtime e três cenários práticos para evitar os erros mais comuns.
Parte 11 de 51
Próximo
Guia completo de login OAuth no Next.js: Google, GitHub e WeChat
Entenda de forma simples como funciona o OAuth 2.0 e aprenda a configurar login com Google, GitHub e WeChat no Next.js, evitando erros de callback e falhas comuns de segurança.
Parte 13 de 51



Comentários
Entre com GitHub para comentar