Skip to main content

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.

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

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

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.

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

      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)