Middleware no Next.js: autenticação e redirecionamento
1. Introdução ao Middleware no Next.js
O Middleware no Next.js é uma funcionalidade poderosa que permite executar código antes que uma requisição seja completada. Ele age como um interceptador no ciclo de vida da aplicação, sendo executado antes das rotas e APIs serem processadas. Isso significa que você pode tomar decisões sobre a requisição antes mesmo dela chegar ao seu destino final.
O papel do middleware na arquitetura do Next.js é fundamental para implementar lógicas de segurança, redirecionamento e processamento global. Entre os casos de uso mais comuns estão:
- Autenticação: verificar tokens JWT antes de permitir acesso a rotas protegidas
- Redirecionamento: direcionar usuários não autenticados para páginas de login
- Logging: registrar informações sobre requisições
- Geolocalização: adaptar conteúdo baseado na localização do usuário
- A/B Testing: redirecionar usuários para diferentes versões de páginas
2. Configuração e Estrutura Básica do Middleware
Para criar um middleware no Next.js, você precisa criar um arquivo middleware.ts (ou middleware.js) na raiz do seu projeto. A estrutura básica é simples:
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// Lógica do middleware aqui
return NextResponse.next()
}
// Configuração de matcher para definir quais rotas interceptar
export const config = {
matcher: ['/dashboard/:path*', '/admin/:path*']
}
O config.matcher é crucial para performance, pois define exatamente quais rotas serão interceptadas. Você pode usar padrões como:
- '/:path*' - todas as rotas
- '/dashboard/:path*' - todas as rotas que começam com /dashboard
- ['/api/:path*', '/admin/:path*'] - múltiplos padrões
3. Implementando Autenticação com Middleware
A autenticação é um dos usos mais comuns do middleware. Vamos implementar um sistema de verificação de tokens JWT armazenados em cookies:
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { jwtVerify } from 'jose'
const secret = new TextEncoder().encode(process.env.JWT_SECRET)
export async function middleware(request: NextRequest) {
const token = request.cookies.get('auth-token')?.value
// Rotas públicas que não precisam de autenticação
const publicPaths = ['/login', '/register', '/api/auth']
if (publicPaths.some(path => request.nextUrl.pathname.startsWith(path))) {
return NextResponse.next()
}
// Verificar token
if (!token) {
return NextResponse.redirect(new URL('/login', request.url))
}
try {
// Verificar e decodificar o token JWT
const { payload } = await jwtVerify(token, secret)
// Adicionar informações do usuário ao header da requisição
const response = NextResponse.next()
response.headers.set('x-user-id', payload.sub as string)
response.headers.set('x-user-role', payload.role as string)
return response
} catch (error) {
// Token inválido ou expirado
return NextResponse.redirect(new URL('/login', request.url))
}
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)']
}
4. Redirecionamento Condicional e Proteção de Rotas
O redirecionamento condicional é essencial para proteger rotas sensíveis. Vamos explorar diferentes cenários:
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const token = request.cookies.get('auth-token')?.value
const url = request.nextUrl.clone()
// Redirecionar usuário logado para dashboard se tentar acessar login
if (url.pathname === '/login' && token) {
url.pathname = '/dashboard'
return NextResponse.redirect(url)
}
// Redirecionar usuário não logado para login com redirect param
if (!token && url.pathname.startsWith('/dashboard')) {
url.pathname = '/login'
url.searchParams.set('redirect', request.nextUrl.pathname)
return NextResponse.redirect(url)
}
// Proteger rotas de admin
if (url.pathname.startsWith('/admin')) {
const userRole = request.cookies.get('user-role')?.value
if (userRole !== 'admin') {
url.pathname = '/403'
return NextResponse.rewrite(url)
}
}
return NextResponse.next()
}
5. Tratamento de Rotas Públicas vs. Privadas
Uma boa prática é organizar as rotas em grupos e usar matchers avançados:
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
const publicRoutes = ['/login', '/register', '/forgot-password', '/api/auth']
const authRoutes = ['/dashboard', '/profile', '/settings']
const adminRoutes = ['/admin']
export function middleware(request: NextRequest) {
const token = request.cookies.get('auth-token')?.value
const pathname = request.nextUrl.pathname
// Verificar se é rota pública
if (publicRoutes.some(route => pathname.startsWith(route))) {
return NextResponse.next()
}
// Verificar rotas autenticadas
if (authRoutes.some(route => pathname.startsWith(route))) {
if (!token) {
return NextResponse.redirect(new URL('/login', request.url))
}
}
// Verificar rotas de admin
if (adminRoutes.some(route => pathname.startsWith(route))) {
if (!token) {
return NextResponse.redirect(new URL('/login', request.url))
}
// Verificar role de admin
}
return NextResponse.next()
}
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico|public).*)',
]
}
6. Integração com API Routes e Server Actions
O middleware pode se comunicar com API Routes para validação de tokens:
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export async function middleware(request: NextRequest) {
// Para rotas de API, validar token no header
if (request.nextUrl.pathname.startsWith('/api/protected')) {
const authHeader = request.headers.get('authorization')
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return NextResponse.json(
{ error: 'Token não fornecido' },
{ status: 401 }
)
}
const token = authHeader.split(' ')[1]
try {
// Validar token com API interna
const validationResponse = await fetch(
`${request.nextUrl.origin}/api/auth/validate`,
{
headers: { 'Authorization': `Bearer ${token}` }
}
)
if (!validationResponse.ok) {
throw new Error('Token inválido')
}
const userData = await validationResponse.json()
const response = NextResponse.next()
response.headers.set('x-user-data', JSON.stringify(userData))
return response
} catch (error) {
return NextResponse.json(
{ error: 'Token inválido ou expirado' },
{ status: 401 }
)
}
}
return NextResponse.next()
}
7. Performance, Cache e Boas Práticas
O middleware é executado no Edge Runtime por padrão, o que oferece baixa latência mas tem limitações:
// middleware.ts - Boas práticas de performance
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
// Cache de configurações
const PUBLIC_ROUTES = new Set(['/login', '/register', '/api/auth'])
const PROTECTED_ROUTES = /^\/(dashboard|profile|settings)/
export function middleware(request: NextRequest) {
const pathname = request.nextUrl.pathname
// Verificação rápida usando Set
if (PUBLIC_ROUTES.has(pathname)) {
return NextResponse.next()
}
// Regex para rotas protegidas
if (PROTECTED_ROUTES.test(pathname)) {
const token = request.cookies.get('auth-token')?.value
if (!token) {
return NextResponse.redirect(new URL('/login', request.url))
}
}
return NextResponse.next()
}
// Dicas de performance:
// 1. Evite operações assíncronas desnecessárias
// 2. Use estruturas de dados rápidas (Set, Map)
// 3. Mantenha o middleware leve e rápido
// 4. Use matchers específicos ao invés de genéricos
8. Exemplo Completo: Sistema de Autenticação com Redirecionamento
Vamos criar um sistema completo de autenticação:
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
// Configurações
const AUTH_COOKIE = 'session-token'
const LOGIN_PAGE = '/login'
const DASHBOARD_PAGE = '/dashboard'
const PUBLIC_ROUTES = ['/login', '/register', '/api/auth', '/public']
const PROTECTED_ROUTES = ['/dashboard', '/profile', '/settings']
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
const token = request.cookies.get(AUTH_COOKIE)?.value
// 1. Rotas públicas
if (PUBLIC_ROUTES.some(route => pathname.startsWith(route))) {
// Se usuário já está logado e tenta acessar login, redirecionar
if (token && pathname === LOGIN_PAGE) {
return NextResponse.redirect(new URL(DASHBOARD_PAGE, request.url))
}
return NextResponse.next()
}
// 2. Rotas protegidas
if (PROTECTED_ROUTES.some(route => pathname.startsWith(route))) {
if (!token) {
// Salvar URL de destino para redirect pós-login
const loginUrl = new URL(LOGIN_PAGE, request.url)
loginUrl.searchParams.set('callbackUrl', pathname)
return NextResponse.redirect(loginUrl)
}
// Token existe, validar e prosseguir
try {
const response = NextResponse.next()
response.headers.set('x-auth-token', token)
return response
} catch (error) {
// Token inválido, redirecionar para login
const loginUrl = new URL(LOGIN_PAGE, request.url)
loginUrl.searchParams.set('error', 'session_expired')
return NextResponse.redirect(loginUrl)
}
}
// 3. Outras rotas (páginas estáticas, etc.)
return NextResponse.next()
}
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico|public).*)',
]
}
// app/login/page.jsx
'use client'
import { useState } from 'react'
import { useRouter, useSearchParams } from 'next/navigation'
export default function LoginPage() {
const router = useRouter()
const searchParams = useSearchParams()
const callbackUrl = searchParams.get('callbackUrl') || '/dashboard'
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const handleLogin = async (e) => {
e.preventDefault()
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
})
if (response.ok) {
// Redirecionar para URL original ou dashboard
router.push(callbackUrl)
}
}
return (
<form onSubmit={handleLogin}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Senha"
/>
<button type="submit">Entrar</button>
</form>
)
}
Este sistema completo demonstra:
- Proteção de rotas sensíveis
- Redirecionamento inteligente pós-login
- Tratamento de sessões expiradas
- Fluxo completo de autenticação
O middleware é uma ferramenta essencial no Next.js moderno, permitindo controle granular sobre o fluxo de autenticação e redirecionamento sem sacrificar performance.
Referências
- Documentação Oficial do Next.js sobre Middleware — Guia completo sobre configuração e uso de middleware no Next.js
- NextAuth.js com Middleware — Tutorial de integração do NextAuth.js com middleware para autenticação
- JWT Authentication com Next.js Middleware — Vídeo tutorial prático sobre implementação de JWT no middleware
- Edge Runtime vs Node.js Runtime no Next.js — Comparação entre runtimes e boas práticas para middleware
- Protegendo Rotas com Middleware no Next.js — Artigo técnico sobre proteção de rotas e redirecionamento condicional
- Next.js Middleware Patterns — Padrões avançados de middleware para autenticação e autorização