Um app de tutorial confia no usuário: assume que o CSV está bem-formado, que o campo numérico tem número, que quem acessa pode acessar. Um app de produção assume o contrário: dados chegam sujos, usuários erram, e alguém mal-intencionado vai testar os limites. Este módulo constrói as três camadas que separam protótipo de produto — comunicar (notificações e modais), defender a entrada (validação) e proteger o app (autenticação e mitigação de ataques).
Dar feedback ao usuário com showNotification, modalDialog e barras de progresso; validar entradas de forma robusta e amigável com req, validate/need e o pacote shinyvalidate; e aplicar fundamentos de segurança em Shiny: autenticação, gestão de segredos, e mitigação de injeção de SQL e XSS.
Notificações: o feedback efêmero
O usuário clicou em "salvar" — funcionou? Silêncio é a pior resposta. showNotification dá um retorno leve e temporário.
observeEvent(input$salvar, {
# ... faz o salvamento ...
showNotification(
"Registro salvo com sucesso.",
type = "message", # "default" | "message" | "warning" | "error"
duration = 4 # segundos; NULL = fica até o usuário fechar
)
})
Para operações com etapas, você pode atualizar uma notificação existente pelo seu id — útil para "processando… → concluído":
observeEvent(input$processar, {
id <- showNotification("Processando...", duration = NULL, closeButton = FALSE)
on.exit(removeNotification(id), add = TRUE) # garante remoção ao final
resultado <- tarefa_demorada()
showNotification("Concluído!", id = id, type = "message", duration = 3)
})
Barras de progresso para tarefas longas
Se algo demora mais de um ou dois segundos, o usuário precisa ver que o app está trabalhando. withProgress/incProgress mostram uma barra.
observeEvent(input$rodar, {
withProgress(message = "Treinando modelo", value = 0, {
n <- 10
for (i in seq_len(n)) {
etapa(i) # trabalho real
incProgress(1/n, detail = paste("passo", i, "de", n))
}
})
})
Para o padrão moderno e não bloqueante (a UI continua responsiva enquanto processa), a combinação é promises + future — cada tarefa pesada roda num processo separado. Esse é o assunto de processos externos com callr; por ora, saiba que withProgress resolve a maioria dos casos.
Modais: quando exigir atenção ou confirmação
Uma notificação é ignorável; um modal bloqueia até o usuário decidir. Use para confirmações destrutivas ("tem certeza que quer excluir?") e formulários focados.
# Abrir um modal de confirmação
observeEvent(input$excluir, {
showModal(modalDialog(
title = "Confirmar exclusão",
"Esta ação não pode ser desfeita. Deseja continuar?",
footer = tagList(
modalButton("Cancelar"), # fecha o modal
actionButton("confirma_excluir", "Excluir", class = "btn-danger")
),
easyClose = FALSE # não fecha clicando fora
))
})
# Só age depois da confirmação explícita
observeEvent(input$confirma_excluir, {
removeModal()
excluir_registro()
showNotification("Registro excluído.", type = "warning")
})
O padrão "modal de confirmação → ação só no botão de confirmação" é a defesa mais barata contra ações destrutivas acidentais. Note que input$excluir só abre o modal; a exclusão real depende de input$confirma_excluir.
Validação: a diferença entre req e validate
Retomando a arquitetura reativa com mais profundidade. Há dois tipos de "parar":
req()— pré-condição técnica, aborta em silêncio. Use quando a ausência é normal e o usuário não precisa de explicação (ex.: ainda não subiu arquivo).validate(need())— pré-condição de negócio, exibe mensagem amigável no lugar do output. Use para orientar o usuário sobre o que corrigir.
output$grafico <- renderPlot({
req(input$arquivo) # técnico: silêncio
dados <- read.csv(input$arquivo$datapath)
validate(
need(ncol(dados) >= 2, "O arquivo precisa ter ao menos 2 colunas."),
need(input$col_x %in% names(dados), "Selecione uma coluna X válida."),
need(is.numeric(dados[[input$col_y]]), "A coluna Y precisa ser numérica.")
)
plot(dados[[input$col_x]], dados[[input$col_y]])
})
Validação de formulários com shinyvalidate
Para formulários com vários campos, validar dentro de cada render fica repetitivo. O pacote shinyvalidate centraliza regras por input e mostra a mensagem embaixo do campo, como frameworks web modernos.
library(shiny)
library(shinyvalidate)
server <- function(input, output, session) {
iv <- InputValidator$new() # cria o validador
iv$add_rule("email", sv_required()) # obrigatório
iv$add_rule("email", sv_email()) # formato de e-mail
iv$add_rule("idade", sv_integer()) # inteiro
iv$add_rule("idade", sv_between(18, 120)) # faixa
iv$add_rule("senha", function(value) { # regra customizada
if (nchar(value) < 8) "A senha precisa ter ao menos 8 caracteres."
})
iv$enable() # liga a validação visual
observeEvent(input$enviar, {
if (iv$is_valid()) { # só prossegue se tudo ok
salvar_cadastro(input$email, input$idade)
showNotification("Cadastro realizado!", type = "message")
} else {
iv$enable() # revela as mensagens de erro
showNotification("Corrija os campos destacados.", type = "error")
}
})
}
A vantagem sobre validate/need: as mensagens ficam junto de cada campo (não no lugar de um output), a checagem é declarativa (add_rule), e iv$is_valid() dá um "portão" único antes de submeter. Validadores podem ser compostos entre módulos — importante para programação em Shiny.
Segurança I — autenticação e autorização
Shiny, por si só, não tem login. Não deixe um app com dados sensíveis exposto sem uma camada de autenticação. As opções, do mais simples ao mais robusto:
- Camada da plataforma (recomendado): Posit Connect, ShinyProxy ou um reverse proxy (Nginx + OAuth2) cuidam do login antes de o app carregar. É a abordagem mais segura, porque a autenticação não vive dentro do código do app.
- Pacotes de auth no app:
shinymanager,polishedoufirebaseadicionam uma tela de login ao próprio app. Prático para casos menores.
library(shinymanager)
# Credenciais (em produção: em banco/hash, NUNCA em texto no código)
credenciais <- data.frame(
user = c("ana", "admin"),
password = c("$7$C6..hash...", "$7$C6..hash..."), # hashes, não texto puro
admin = c(FALSE, TRUE),
stringsAsFactors = FALSE
)
ui <- secure_app(fluidPage(
# ... sua UI normal ...
))
server <- function(input, output, session) {
res_auth <- secure_server(check_credentials(credenciais))
# A partir daqui, o app só roda para usuários autenticados.
# res_auth$user, res_auth$admin permitem autorização por papel.
}
Autenticação ("quem é você?") é diferente de autorização ("o que você pode fazer?"). Depois de logado, use o papel do usuário (res_auth$admin) para mostrar/esconder abas, permitir edição só a administradores etc. — combinando com shinyjs (programação em Shiny) para ocultar controles.
Segurança II — segredos: nunca no código
Chaves de API, senhas de banco e tokens jamais devem aparecer no código-fonte (nem no Git). Use variáveis de ambiente.
# ERRADO — segredo no código, vai para o Git e vaza
con <- dbConnect(RPostgres::Postgres(), password = "senha123")
# CERTO — lê de variável de ambiente (definida no servidor ou em .Renviron)
con <- dbConnect(
RPostgres::Postgres(),
host = Sys.getenv("DB_HOST"),
user = Sys.getenv("DB_USER"),
password = Sys.getenv("DB_PASSWORD") # nunca literal no código
)
O arquivo .Renviron (guarda os segredos localmente) precisa estar no .gitignore. Em produção, as variáveis são injetadas pelo ambiente (Docker --env, ShinyProxy, secrets do orquestrador). Isso é detalhado em implantação com Docker.
Segurança III — injeção de SQL
Se o app monta consultas concatenando texto vindo do usuário, ele está aberto a SQL injection. A defesa é parametrizar — nunca colar o input direto na query.
# ERRADO — concatenação: um input malicioso reescreve a query
query <- paste0("SELECT * FROM clientes WHERE nome = '", input$nome, "'")
# input$nome = "'; DROP TABLE clientes; --" → catástrofe
# CERTO — query parametrizada: o driver trata o valor como DADO, não código
res <- dbGetQuery(
con,
"SELECT * FROM clientes WHERE nome = ?",
params = list(input$nome)
)
# Alternativa com glue_sql, também segura (escapa corretamente)
library(glue)
q <- glue_sql("SELECT * FROM clientes WHERE nome = {input$nome}", .con = con)
O placeholder ? (ou $1, conforme o driver) garante que o banco trate a entrada como valor, não como parte do comando SQL. Essa é uma regra absoluta: toda query com dado do usuário é parametrizada.
Segurança IV — XSS e conteúdo do usuário
Se você renderiza texto do usuário como HTML (via HTML(), renderUI, htmlOutput), um invasor pode injetar <script> — um ataque de XSS (Cross-Site Scripting). A regra: nunca passe entrada de usuário por HTML() sem sanitizar.
# PERIGOSO — renderiza HTML bruto do usuário
output$saida <- renderUI({
HTML(input$comentario) # se o comentário tiver <script>, executa!
})
# SEGURO — trata como texto puro (escapa < > & etc.)
output$saida <- renderText({
input$comentario # renderText/textOutput escapam por padrão
})
# Se PRECISAR de HTML do usuário, sanitize antes
library(sanitizeHTML) # ou htmltools::htmlEscape para escapar
output$saida <- renderUI({
HTML(sanitize_html(input$comentario)) # remove tags perigosas
})
Regra prática: prefira textOutput/renderText (que escapam automaticamente) para exibir conteúdo do usuário. Só use HTML()/renderUI com conteúdo que você controla, ou depois de sanitizar.
Camadas juntas: um fluxo defensivo completo
server <- function(input, output, session) {
iv <- InputValidator$new()
iv$add_rule("valor", sv_required()); iv$add_rule("valor", sv_gt(0))
iv$enable()
observeEvent(input$transferir, {
# 1) valida a entrada
if (!iv$is_valid()) {
showNotification("Corrija os campos.", type = "error"); return()
}
# 2) confirma ação sensível
showModal(modalDialog(
"Confirmar transferência de R$ ", format(input$valor), "?",
footer = tagList(modalButton("Não"),
actionButton("ok_transf", "Sim", class = "btn-danger"))
))
})
observeEvent(input$ok_transf, {
removeModal()
id <- showNotification("Processando...", duration = NULL)
on.exit(removeNotification(id), add = TRUE)
# 3) query parametrizada (sem injeção)
dbExecute(con, "UPDATE contas SET saldo = saldo - ? WHERE id = ?",
params = list(input$valor, isolate(usuario_id())))
showNotification("Transferência concluída.", id = id, type = "message")
})
}
Esse fluxo — validar → confirmar → executar com segurança → dar feedback — é o esqueleto de qualquer ação sensível num app de produção.
O feedback efêmero é o showNotification, que aceita um id para se atualizar de "processando" para "pronto"; para tarefas longas, withProgress e incProgress mostram barras. O modalDialog bloqueia a tela em confirmações destrutivas, seguindo uma regra que evita acidente: abrir com uma ação, executar só no botão de confirmação. Entre os dois jeitos de parar, o req() é pré-condição técnica e aborta em silêncio, enquanto validate(need()) é regra de negócio e mostra mensagem no lugar do output. O shinyvalidate centraliza as regras por campo com add_rule e os sv_*, exibe o erro sob cada input e oferece o portão iv$is_valid(). Sobre acesso: o Shiny não tem login nativo, e o melhor caminho é autenticar na plataforma — Connect, ShinyProxy ou um reverse proxy —, com o shinymanager servindo casos menores; e vale distinguir autenticação de autorização. Segredos vão em variáveis de ambiente lidas com Sys.getenv, nunca no código, com o .Renviron no .gitignore. Contra SQL injection, sempre parametrizar com ?, params ou glue_sql, jamais concatenar input. Contra XSS, preferir renderText e textOutput, que escapam, e reservar HTML() e renderUI para conteúdo próprio ou sanitizado. Toda ação sensível segue o mesmo esqueleto: validar, confirmar, executar com segurança e dar feedback.
Exercícios
-
Quando usar
req()e quando usarvalidate(need())? Dê um exemplo de cada.✓ Resposta:req()para pré-condições técnicas cuja ausência é normal e não precisa de explicação — ex.:req(input$arquivo)antes de qualquer upload (aborta em silêncio, deixando o output vazio).validate(need(...))para regras de negócio em que o usuário precisa saber o que corrigir — ex.:validate(need(is.numeric(dados[[col]]), "A coluna precisa ser numérica.")), que substitui o output por uma mensagem amigável. Resumo: silêncio técnico →req; orientação ao usuário →validate/need. -
Por que a exclusão de um registro deve depender de
input$confirma_excluire não deinput$excluirdiretamente?✓ Resposta:Porque
input$excluiré o clique que abre o modal de confirmação, não a decisão final. Ações destrutivas devem exigir um segundo passo explícito: o modal apresenta o aviso e só o botãoconfirma_excluirdentro dele efetiva a exclusão. Isso evita exclusões acidentais (clique errado) — o padrão "abrir com uma ação, executar só na confirmação" é a proteção mínima para operações irreversíveis. -
Este código é vulnerável. Aponte o problema e corrija: > ``
r > q <- paste0("SELECT * FROM users WHERE email = '", input$email, "'") > dbGetQuery(con, q) >``✓ Resposta:É vulnerável a SQL injection: concatenar
input$emailna string permite que uma entrada como' OR '1'='1ou'; DROP TABLE users; --altere ou destrua a consulta. A correção é parametrizar, deixando o driver tratar o valor como dado:dbGetQuery(con, "SELECT * FROM users WHERE email = ?", params = list(input$email))Alternativamente,
glue_sql("... WHERE email = {input$email}", .con = con), que escapa corretamente. Nunca cole entrada de usuário direto na query. -
Um colega vai exibir comentários de usuários com
renderUI({ HTML(input$comentario) }). Que risco isso cria e qual a alternativa segura?✓ Resposta:Cria risco de XSS (Cross-Site Scripting): se o comentário contiver
<script>...</script>(ou um atributoonerroretc.), oHTML()renderiza e o navegador executa esse código, permitindo roubo de sessão, redirecionamentos maliciosos etc. A alternativa segura é exibir como texto puro comrenderText/textOutput, que escapam< > &automaticamente. Se HTML for realmente necessário, sanitizar antes (remover tags/atributos perigosos) — nunca renderizar HTML de usuário sem tratamento. -
Por que autenticar na plataforma (ShinyProxy, reverse proxy) é geralmente mais seguro que implementar login dentro do próprio app?
✓ Resposta:Porque a autenticação acontece antes de o código do app carregar: usuários não autenticados nunca chegam ao Shiny, reduzindo a superfície de ataque. A lógica de login (sessões, hashes, tokens, OAuth) fica numa camada madura e auditada (proxy/IdP), não misturada ao código de negócio, onde é fácil errar. Além disso, centraliza a política de acesso para vários apps. Login no app (
shinymanager) é aceitável para casos menores, mas coloca a responsabilidade de segurança dentro da aplicação.