LGPD em Aplicações Laravel: 8 Controles Que Resolvem 90% das Auditorias
Checklist técnico para aplicações Laravel atenderem LGPD na prática. Anonimização, soft delete, logs auditáveis, criptografia em repouso e os 8 controles que sempre aplico antes de qualquer sistema ir pra produção.
LGPD é a lei que mais virou meme no Brasil. Todo mundo fala, ninguém implementa direito, e quando a ANPD bate na porta o time descobre que o “termo de uso atualizado” não conta como controle técnico.
Esse artigo é o checklist que aplico em toda aplicação Laravel que entrego antes de subir pra produção. São 8 controles técnicos que, quando aplicados juntos, resolvem 90% do que vai aparecer em auditoria de LGPD. Os outros 10% são papelada do jurídico que não é meu departamento.
Por que isso virou prioridade em 2026
Multas da ANPD passaram de 47 casos em 2024 para mais de 380 em 2025. O valor médio aplicado triplicou. E o critério que pega quase todo mundo é o mesmo: ausência de medida técnica capaz de demonstrar boa-fé.
Boa-fé na LGPD não é discurso. É log, é criptografia, é processo de exclusão, é trilha auditável. Tudo isso é código.
Os 8 controles
1. Criptografia em repouso para dados sensíveis
Toda coluna que guarda CPF, CNPJ, RG, telefone, e-mail, endereço, dado bancário, dado de saúde passa por cast encrypted do Laravel ou por uma camada de cripto própria.
// app/Models/Cliente.php
class Cliente extends Model
{
protected $casts = [
'cpf' => 'encrypted',
'rg' => 'encrypted',
'telefone' => 'encrypted',
'endereco' => 'encrypted:array',
];
}
Detalhe que muita gente esquece: o cast encrypted usa a APP_KEY. Se você perder a key, perdeu o dado. Backup da key em cofre separado é parte do procedimento, não nice-to-have.
2. Hash + busca por coluna espelho para campos que precisam ser pesquisáveis
Cripto resolve repouso, mas mata WHERE cpf = ?. Solução: coluna paralela com hash.
// Migration
$table->string('cpf')->nullable(); // encrypted
$table->string('cpf_hash', 64)->nullable()->index(); // SHA-256
// No model
public function setCpfAttribute($value)
{
$this->attributes['cpf'] = $value;
$this->attributes['cpf_hash'] = hash('sha256', preg_replace('/\D/', '', $value));
}
// Busca
Cliente::where('cpf_hash', hash('sha256', $cpfInput))->first();
Você ganha busca exata sem expor o dado em claro no índice do banco.
3. Soft delete + anonimização programada
Direito ao esquecimento (Art. 18, VI) exige que o titular consiga apagar o dado. Mas na contabilidade você tem obrigação fiscal de guardar por anos.
Solução: soft delete imediato + anonimização agendada após o prazo legal.
// app/Jobs/AnonymizeExpiredRecords.php
class AnonymizeExpiredRecords implements ShouldQueue
{
public function handle(): void
{
Cliente::onlyTrashed()
->where('deleted_at', '<', now()->subYears(5))
->each(function ($cliente) {
$cliente->update([
'nome' => 'ANONIMIZADO',
'cpf' => null,
'cpf_hash' => null,
'email' => null,
'telefone' => null,
'endereco' => null,
'anonimizado_em' => now(),
]);
});
}
}
Agendado no Kernel.php para rodar diariamente. Auditoria pede log, você mostra o job + a coluna anonimizado_em.
4. Log auditável de acesso a dado pessoal
Todo SELECT em dado sensível tem que deixar rastro de quem leu, quando, e por quê.
// app/Concerns/LogsPersonalDataAccess.php
trait LogsPersonalDataAccess
{
public static function bootLogsPersonalDataAccess()
{
static::retrieved(function ($model) {
if (auth()->check()) {
DB::table('pii_access_log')->insert([
'user_id' => auth()->id(),
'model' => static::class,
'model_id' => $model->getKey(),
'context' => request()->route()?->getName(),
'ip' => request()->ip(),
'created_at' => now(),
]);
}
});
}
}
Em models de cliente, paciente, beneficiário: use LogsPersonalDataAccess;. Acabou.
5. Política de retenção por tipo de dado
LGPD Art. 15: dado pessoal só pode ser tratado pelo tempo necessário à finalidade. Cada tipo tem prazo diferente:
| Tipo de dado | Prazo legal típico | Onde guardar prazo |
|---|---|---|
| Currículo de candidato | 6 meses | data_retention_months no model |
| Cliente ativo | enquanto contrato | flag is_active |
| Histórico fiscal | 5 anos após encerramento | purge_after data |
| Log de auditoria | 6 anos | regra fixa no scheduler |
| Cookie de marketing | 12 meses | TTL na sessão |
Tabela de política como source of truth: config/lgpd.php. Job diário verifica e processa.
6. Consentimento granular, versionado e revogável
accept_terms = true no users table não é consentimento válido. ANPD quer ver quando, qual versão, e como pode revogar.
// Migration
Schema::create('user_consents', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id');
$table->string('purpose'); // marketing, comunicacao, terceiros, cookies
$table->string('terms_version');
$table->ipAddress('ip');
$table->boolean('granted');
$table->timestamp('granted_at');
$table->timestamp('revoked_at')->nullable();
$table->index(['user_id', 'purpose']);
});
Tela de “Minha privacidade” no app: usuário vê cada finalidade e pode revogar com 1 clique. Auditor pede prova: você mostra a linha com IP, timestamp e versão dos termos vigentes naquela data.
7. Mascaramento em logs e telas
Log nunca grava CPF, e-mail ou cartão em claro. Eu uso um middleware MaskSensitiveInLogs:
public function handle($request, Closure $next)
{
$masked = $this->maskFields($request->all(), ['cpf', 'rg', 'senha', 'cartao']);
Log::withContext(['request' => $masked]);
return $next($request);
}
private function maskFields(array $data, array $fields): array
{
foreach ($fields as $f) {
if (isset($data[$f])) {
$data[$f] = '***' . substr($data[$f], -3);
}
}
return $data;
}
Frontend (Inertia/React) usa o mesmo padrão para mascaramento em listagens. CPF aparece como ***.***.***-12.
8. Exportação de dados pessoais sob demanda
Art. 18, II: titular pode pedir portabilidade dos próprios dados. Resposta em até 15 dias úteis.
Não dá pra resolver isso na unha quando o pedido chega. Tem que ter rota pronta:
// routes/api.php
Route::middleware(['auth'])->get('/privacidade/exportar', function () {
$userData = [
'perfil' => auth()->user()->only(['nome', 'email', 'telefone']),
'pedidos' => auth()->user()->pedidos()->get()->toArray(),
'consentimentos' => auth()->user()->consents()->get()->toArray(),
'log_acessos' => auth()->user()->piiAccessLogs()->limit(1000)->get(),
];
return response()->json($userData)
->header('Content-Disposition', 'attachment; filename=meus-dados.json');
});
Bonus: gerar PDF assinado digitalmente também atende o requisito.
O que esses controles NÃO resolvem
- Política de privacidade desatualizada (departamento jurídico)
- DPO designado (papelada formal)
- Treinamento da equipe (RH + jurídico)
- Análise de impacto à proteção de dados (DPIA) para tratamentos de alto risco (consultor LGPD)
Esses são processos, não código. Mas sem os 8 controles acima, nada disso te salva quando a ANPD pedir log de acesso a dado pessoal.
Resumo do que aplicar amanhã
- Cast
encryptedem toda coluna sensível - Coluna
*_hashpara busca exata - Soft delete + job de anonimização programada
- Trait
LogsPersonalDataAccessem models com PII - Tabela de política de retenção + scheduler
user_consentsgranular + versionado- Middleware de mascaramento em logs
- Endpoint
/privacidade/exportarpor usuário autenticado
Todos os 8 cabem em uma sprint. Em um ERP que opero, esse pacote ficou pronto em 6 dias úteis de um dev sênior.
Quer um audit gratuito?
A gente faz revisão técnica de aderência LGPD em aplicações Laravel como serviço. Recebe relatório com cada controle marcado como Verde / Amarelo / Vermelho e prioridade de correção. Fale com nosso time.