Servlet de Impressão de Documentos
1. Resumo da solução
Criar um Servlet (/public/doc/*) que recebe um token único (hash assinado) na URL.
O token é gerado pelo ERP (quando o usuário “gera link”) a partir de empresaId, documentoId e expiryTimestamp.
O token contém dados codificados + assinatura HMAC-SHA256 com chave secreta.
O servlet valida o token e, se válido, entrega o PDF Jasper. O link expira após 5 dias.
2. Requisitos funcionais
• Usuário no ERP gera link e recebe URL temporária.
• Link válido por 5 dias.
• Após expiração, retorna 403 Forbidden.
• Cada geração, caso os 5 dias tenham passado, criam um novo token.
• Logs de auditoria obrigatórios.
• A ideia é que todos os documentos possam ser impressos dessa forma, mas iniciaremos somente com o pedido de venda para avaliar o impacto no sistema.
—————
3. Requisitos não-funcionais / Segurança
• Assinatura HMAC-SHA256 com chave secreta.
• Uso de Base64URL sem padding.
• Uso de UTC para timestamps.
• Cabeçalhos HTTP de segurança (Cache-Control, X-Content-Type-Options, CSP).
• Chave secreta fixa, definida em código (depois consideraremos rotatividade).
5. Formato do token
Stateful (salvar token em tabela) — permite revogação.
Payload: v1|empresaId|documentoId|expiryEpochSeconds
Assinatura: HMAC-SHA256(secret, payload)
Token final: Base64Url(payload) . "." . Base64Url(signature)
Exemplo:
v1|123|9876|1740000000
—————
6. Endpoints
POST /endpoint de criação do documento
• Gera link temporário
• Retorno:
{
"url": "https://app.gerenciaqui.com.br/api-v1/public/doc/{token}",
"expiresAt": "2025-09-10T12:34:56Z"
}
GET /api-v1/public/doc/{token}
• Valida token
• Retorna PDF Jasper ou erro 403/410.
—————
7. Fluxos
Geração de link
1. Usuário solicita geração no ERP.
2. Gera payload (empresaId, documentoId, expiry). Armazena o token na na tabela relacionada
3. Calcula assinatura HMAC-SHA256.
4. Retorna URL com token.
Acesso ao link
1. Cliente acessa URL pública.
2. Servlet valida assinatura e expiração.
3. Gera PDF Jasper a partir das configurações do cliente ou do documento. Como somente estamos falando do pedido or enquanto, somente considerar as configurações padrão como norte (do modelo a ser impresso). Sempre gerar para não ter acúmulo de pdfs em disco.
4. Retorna PDF ou erro.
—————
8. Logs e auditoria
A cada link gerado, deverá ser inserida um registro no log de operações no formato: <LINK DE COMPARTILHAMENTO GERADO PARA <DOCUMENTO (VENDA...)><ID>: <LINK>".
O revoke por enquanto só será ser possível via endpoint interno (pode ser disponibilizado para o suporte em caso de problemas, não sendo visível para o cliente). Quando o usuário solicitar a geração de link para um documento cujo token esteja revogado, atualizar o registro do documento na tabela com o novo link, para não gerar registros desnecessários. O histórico é acessível via log.
—————
9. Criação da tabela
CREATE TABLE documenttokens (
token VARCHAR(512) PRIMARY KEY,
empresaId BIGINT NOT NULL,
documentoTipo INT NOT NULL,
documentoId BIGINT NOT NULL,
expiresAt TIMESTAMP WITH TIME ZONE NOT NULL,
createdBy BIGINT,
createdAt TIMESTAMP WITH TIME ZONE DEFAULT now(),
revoked BOOLEAN DEFAULT false
);
—————
10. Exemplo de código Java (simplificado)
Geração do Token
public String generateToken(long empresaId, long documentoId, Instant expiryUtc) {
String payload = String.format("v1|%d|%d|%d", empresaId, documentoId, expiryUtc.getEpochSecond());
String payloadB64 = base64UrlEncode(payload.getBytes(StandardCharsets.UTF_8));
byte[] sig = hmac(payload.getBytes(StandardCharsets.UTF_8));
String sigB64 = base64UrlEncode(sig);
return payloadB64 + "." + sigB64;
}
Servlet (esqueleto)
@WebServlet("/public/doc/*")
public class PublicDocumentServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
String token = extractTokenFromPath(req);
try {
PublicLinkUtil.TokenPayload payload = linkUtil.validateToken(token);
Documento doc = documentoService.findById(payload.documentoId);
if (doc == null || doc.getEmpresaId() != payload.empresaId) {
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "Documento inválido");
return;
}
byte[] pdf = documentoService.getPdfBytes(payload.documentoId);
resp.setContentType("application/pdf");
resp.setHeader("Content-Disposition", "inline; filename=\"" + doc.getFileName() + "\"");
resp.getOutputStream().write(pdf);
} catch (Exception e) {
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "Token inválido ou expirado");
}
}
}
—————
11. Testes sugeridos
• Unit tests para geração e validação de token.
• Teste de token corrompido/expirado.
• Teste de carga no servlet com múltiplos acessos.
• Teste de segurança contra brute force. (pedir auxílio ao Matheus)
No Comments