Login não acha a conta: o espaço invisível no fim do e-mail
Descubra o espaço comum, o espaço inquebrável e o caractere de largura zero que quebram a busca por e-mail, limpe a borda no servidor e arrume os dados que já estão sujos.
A cliente copia o e-mail de uma conversa no WhatsApp, cola no login, digita a senha certa e recebe "conta não encontrada". O suporte abre o banco, olha a linha, e o e-mail está igualzinho. A resposta curta: tem um caractere invisível na borda, e a comparação de string é exata. Limpe o começo e o fim de e-mail, código e termo de busca no servidor, antes de gravar e antes de procurar, com uma função que remova também o espaço inquebrável e os caracteres de largura zero. Não mexa no espaço do meio de um nome, nem na senha.
O exemplo que atravessa este guia é o cadastro de uma loja pequena, em PHP com MySQL, onde a cliente Ana não consegue entrar com ana@exemplo.com.
Por que o espaço que você não vê muda tudo
Para o banco, ana@exemplo.com e ana@exemplo.com seguido de um espaço são textos diferentes. O olho não distingue, a tela também não. E o espaço comum não é o único suspeito. Texto copiado de mensageiro, PDF ou planilha costuma trazer:
- Espaço inquebrável (U+00A0), que em UTF-8 são os bytes
c2 a0. Editores e aplicativos de mensagem usam para não quebrar linha. - Espaço de largura zero (U+200B), bytes
e2 80 8b. Não ocupa espaço nenhum na tela. - Marca de ordem de bytes (U+FEFF), bytes
ef bb bf, que aparece no começo de linhas vindas de arquivos CSV exportados. - Tab e quebra de linha, quando a pessoa copia uma célula de planilha.
A função trim() do PHP, sem argumentos, remove só espaço, tab, quebra de linha, retorno de carro, byte nulo e tab vertical. O espaço inquebrável passa intacto. No JavaScript, String.prototype.trim() remove o inquebrável e o U+FEFF, mas não o espaço de largura zero.
O detalhe do MySQL que confunde o diagnóstico
As collations antigas do MySQL, como utf8mb4_unicode_ci e utf8mb4_general_ci, são do tipo PAD SPACE: na comparação com =, espaços comuns no fim são ignorados. A collation padrão do MySQL 8, utf8mb4_0900_ai_ci, é NO PAD e compara o espaço também. Resultado: o mesmo login funciona no servidor antigo e falha no novo. E nenhuma das duas ignora o espaço inquebrável. Se o bug aparece "só em alguns ambientes", confira a collation da coluna antes de culpar o código.
Como encontrar o caractere invisível
Pare de olhar o texto e olhe os bytes. Em PHP, bin2hex mostra tudo:
<?php
var_dump(bin2hex($_POST['email']));
// string(34) "616e61406578656d706c6f2e636f6dc2a0"
O fim c2a0 denuncia o espaço inquebrável. No banco, SELECT HEX(email) FROM usuarios WHERE id = 42 faz o mesmo. Num log, registre o tamanho junto do valor: um e-mail de 15 letras com mb_strlen igual a 16 já diz que tem algo a mais. Só não registre a senha para fazer esse teste; o guia de log sem senha explica por quê.
Como limpar do jeito certo
- Separe os campos em dois grupos. Identificadores, como e-mail, CPF, cupom, CEP e código de rastreio, não têm espaço legítimo na borda. Texto livre, como nome, endereço e comentário, tem espaço legítimo no meio.
- Para identificadores, remova da borda todo espaço Unicode e os caracteres de largura zero. E-mail ainda vai para minúsculas.
- Para texto livre, limpe só a borda. Não junte espaços internos nem mude maiúsculas: "Ana Maria da Silva" precisa ficar como a pessoa escreveu.
- Não limpe senha. Espaço pode fazer parte dela, e alterar o que a pessoa digitou cria uma senha que ela não conhece.
- Aplique a mesma função no cadastro, no login, na recuperação de senha e na busca do painel. Limpar só no cadastro deixa o login comparando sujo com limpo.
- Faça no servidor. A limpeza no navegador é conforto, não garantia: aplicativo, API e importação de planilha não passam por ela.
A função de limpeza em PHP
<?php
function limparBorda(string $texto): string
{
$limpo = preg_replace('/^[\s\p{Z}\x{200B}\x{FEFF}]+|[\s\p{Z}\x{200B}\x{FEFF}]+$/u', '', $texto);
if ($limpo === null) {
throw new InvalidArgumentException('Texto com encoding inválido.');
}
return $limpo;
}
function normalizarEmail(string $email): string
{
return mb_strtolower(limparBorda($email), 'UTF-8');
}
$email = normalizarEmail($_POST['email'] ?? '');
$nome = limparBorda($_POST['nome'] ?? '');
$senha = $_POST['senha'] ?? '';
\p{Z} cobre todos os separadores Unicode, incluindo o espaço inquebrável. \s pega tab e quebra de linha. Os dois códigos explícitos cuidam do espaço de largura zero e da marca de ordem de bytes, que não são classificados como espaço. O modificador u faz a expressão trabalhar com caracteres UTF-8 em vez de bytes; sem ele, o c2 a0 não seria reconhecido. E preg_replace devolve null quando recebe UTF-8 inválido, por isso o teste.
No navegador, o campo type="email" já descarta espaço ASCII na borda ao ler o valor, o que ajuda quem digita. Ainda assim, mantenha a função no servidor, porque o espaço inquebrável e as outras entradas não passam por ali. A comparação da senha continua como no guia de login com senha protegida, sem nenhum tratamento de texto antes do password_verify.
Como arrumar o que já está sujo no banco
Limpar a entrada daqui para frente não conserta as contas antigas. Faça uma passagem única, com a mesma função, e cuidado com um efeito colateral: duas linhas que eram diferentes podem virar iguais depois da limpeza, e a coluna de e-mail costuma ter índice único.
<?php
$pdo = new PDO($dsn, $usuario, $senhaBanco, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
$linhas = $pdo->query('SELECT id, email FROM usuarios')->fetchAll(PDO::FETCH_ASSOC);
$conflito = $pdo->prepare('SELECT id FROM usuarios WHERE email = ? AND id <> ?');
$atualiza = $pdo->prepare('UPDATE usuarios SET email = ? WHERE id = ?');
foreach ($linhas as $linha) {
$limpo = normalizarEmail($linha['email']);
if ($limpo === $linha['email']) {
continue;
}
$conflito->execute([$limpo, $linha['id']]);
if ($conflito->fetch()) {
echo 'Conflito no id ' . $linha['id'] . ': ' . $limpo . PHP_EOL;
continue;
}
$atualiza->execute([$limpo, $linha['id']]);
}
Rode primeiro com o UPDATE comentado, para ver quantas linhas mudariam e quantos conflitos existem. Conflito é decisão humana: duas contas da mesma pessoa, uma delas criada porque o login falhou. Faça backup antes; o guia de backup de MySQL que restaura mostra como testar a volta.
Erros comuns
- Confiar no
trim()padrão. O espaço inquebrável fica, e o login continua falhando para quem colou do celular. - Limpar só no cadastro. O login compara a entrada suja com o valor limpo do banco e não acha a conta.
- Remover todos os espaços do campo. "Ana Maria" vira "AnaMaria" no cadastro e na nota fiscal.
- Aplicar
trimna senha. Quem criou senha com espaço no fim nunca mais entra, ou entra com uma senha diferente da que escolheu. - Calcular hash ou criptografar antes de limpar. Se você guarda o hash do CPF ou do e-mail para busca, o espaço fica gravado no hash para sempre e nenhuma limpeza posterior encontra o registro.
- Atualizar a coluna inteira de uma vez sem checar duplicados. O
UPDATEpara no primeiro conflito do índice único, ou, sem índice, cria duas contas com o mesmo e-mail.
Checklist
- Existe uma função única de limpeza de borda que trata espaço Unicode e caracteres de largura zero.
- Cadastro, login, recuperação de senha e busca usam a mesma função.
- E-mail é gravado e comparado em minúsculas.
- Nome e endereço mantêm os espaços internos.
- A senha chega ao
password_verifyexatamente como foi digitada. - Você sabe qual collation a coluna de e-mail usa.
- Os registros antigos passaram pela limpeza, com os conflitos revisados à mão.
Cadastre uma conta de teste com um espaço inquebrável no fim do e-mail e tente entrar digitando sem ele. Funcionou? E quais outros campos do seu sistema são, na verdade, códigos que alguém vai colar de outro lugar?
Referência: http://forum.wmonline.com.br/topic/153553-tirar-espacos-em-branco-de-string/