Aula 14: Segurança com Jakarta Security
Objetivos
- Compreender os fundamentos de segurança em aplicações web: autenticação, autorização, confidencialidade
- Implementar autenticação baseada em formulário com Jakarta Security e Quarkus
- Configurar autorização declarativa com
@RolesAllowed,@PermitAlle@DenyAll - Proteger aplicações contra ataques comuns: CSRF, XSS, SQL Injection, Session Fixation
- Integrar segurança no CritiqueHub com login, roles e proteção de endpoints
Contexto e Motivação
Segurança não é um recurso opcional — é um requisito fundamental de qualquer aplicação web profissional. Uma única vulnerabilidade pode comprometer dados de milhares de usuários, causar prejuízos financeiros e destruir a reputação de uma empresa.
No contexto do CritiqueHub, precisamos garantir que:
- Apenas usuários autenticados possam publicar avaliações
- Apenas administradores possam gerenciar o catálogo de filmes
- Dados sensíveis (senhas, tokens) sejam armazenados e transmitidos com segurança
- A aplicação resista a ataques como CSRF, XSS e injeção
O Jakarta EE oferece uma especificação dedicada — Jakarta Security 3.0 — que padroniza mecanismos de autenticação e autorização. O Quarkus complementa com sua própria camada de segurança, integrando-se perfeitamente com o ecossistema Jakarta.
Conceitos Fundamentais
Antes de implementar, é essencial entender os três pilares da segurança:
| Pilar | Descrição | Exemplo no CritiqueHub |
|---|---|---|
| Autenticação | Verificar a identidade do usuário | Login com email/senha |
| Autorização | Verificar permissões do usuário autenticado | Admin pode deletar filmes, usuário comum não |
| Confidencialidade | Proteger dados em trânsito e em repouso | HTTPS, hashing de senhas |
Conteúdo
Arquitetura de Segurança Jakarta EE
A segurança no Jakarta EE é baseada em três componentes principais:
- Authentication Mechanism: Como as credenciais são coletadas (formulário, HTTP Basic, token)
- Identity Store: Onde as credenciais são validadas (banco de dados, LDAP, arquivo)
- Security Context: Contém o
Principal(identidade) eroles(permissões) do usuário
Configurando Segurança no Quarkus
Dependências
<!-- pom.xml -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-security</artifactId>
</dependency>
<!-- Para autenticação baseada em formulário com JPA -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-security-jpa</artifactId>
</dependency>
<!-- Para hashing de senhas com BCrypt -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-elytron-security-common</artifactId>
</dependency>
Entidade Usuário com Security JPA
O Quarkus Security JPA permite usar entidades JPA diretamente como Identity Store:
@Entity
@Table(name = "usuario")
@UserDefinition // marca como entidade de identidade
public class Usuario extends PanacheEntity {
@Username // campo usado como identificador de login
@Column(unique = true, nullable = false)
public String email;
@Password(value = PasswordType.MCF) // Modular Crypt Format (BCrypt)
@Column(nullable = false)
public String senha;
@Roles // campo ou coleção que define os roles
public String role; // "USER", "ADMIN", "MODERATOR"
@Column(nullable = false)
public String nome;
public boolean ativo = true;
// Método auxiliar para criar usuários com senha hasheada
public static Usuario criar(String nome, String email, String senha, String role) {
Usuario usuario = new Usuario();
usuario.nome = nome;
usuario.email = email;
usuario.senha = BcryptUtil.bcryptHash(senha); // hash automático
usuario.role = role;
return usuario;
}
}
BCrypt é um algoritmo de hashing propositalmente lento, com salt integrado. MD5 e SHA são rápidos demais — um atacante pode testar bilhões de combinações por segundo. Com BCrypt, mesmo com hardware especializado, a velocidade é limitada a poucas milhares por segundo, tornando ataques de força bruta impraticáveis.
Configuração de Autenticação
# application.properties
# Habilita autenticação baseada em formulário
quarkus.http.auth.form.enabled=true
quarkus.http.auth.form.login-page=/login.xhtml
quarkus.http.auth.form.error-page=/login-error.xhtml
quarkus.http.auth.form.landing-page=/index.xhtml
# Sessão HTTP para manter o estado de login
quarkus.http.auth.session.encryption-key=minha-chave-secreta-de-pelo-menos-16-chars
Autorização Declarativa
O Jakarta Security e Quarkus suportam autorização baseada em anotações, permitindo proteger endpoints e métodos de forma declarativa:
Anotações de Segurança
| Anotação | Efeito | Uso |
|---|---|---|
@RolesAllowed({"ADMIN"}) | Requer role específico | Endpoints administrativos |
@PermitAll | Acesso público, sem autenticação | Páginas de consulta |
@DenyAll | Bloqueia todo acesso | Desabilitar temporariamente |
@Authenticated | Requer autenticação (qualquer role) | Área logada genérica |
Protegendo Endpoints REST
@Path("/filmes")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class FilmeResource {
@Inject
FilmeRepository filmeRepository;
@GET
@PermitAll // qualquer pessoa pode listar filmes
public Response listar() {
return Response.ok(filmeRepository.listAll()).build();
}
@GET
@Path("/{id}")
@PermitAll
public Response buscarPorId(@PathParam("id") Long id) {
return filmeRepository.findByIdOptional(id)
.map(f -> Response.ok(f).build())
.orElse(Response.status(Status.NOT_FOUND).build());
}
@POST
@RolesAllowed({"ADMIN", "MODERATOR"}) // apenas admin e moderador
@Transactional
public Response criar(Filme filme) {
filmeRepository.persist(filme);
return Response.status(Status.CREATED).entity(filme).build();
}
@DELETE
@Path("/{id}")
@RolesAllowed("ADMIN") // apenas admin pode deletar
@Transactional
public Response deletar(@PathParam("id") Long id) {
filmeRepository.deleteById(id);
return Response.noContent().build();
}
}
Protegendo Managed Beans (Jakarta Faces)
@Named
@ViewScoped
public class FilmeBean implements Serializable {
@Inject
SecurityContext securityContext;
@Inject
FilmeRepository filmeRepository;
// Verifica se o usuário tem permissão no template
public boolean isAdmin() {
return securityContext.isCallerInRole("ADMIN");
}
public String getNomeUsuario() {
Principal principal = securityContext.getCallerPrincipal();
return principal != null ? principal.getName() : "Anônimo";
}
@RolesAllowed("ADMIN")
public String deletarFilme(Long id) {
filmeRepository.deleteById(id);
return "filmes?faces-redirect=true";
}
}
Na página XHTML, use renderização condicional:
<!-- Botão visível apenas para ADMIN -->
<p:commandButton value="Excluir"
rendered="#{filmeBean.admin}"
action="#{filmeBean.deletarFilme(filme.id)}"
update="@form" />
Configuração por Path (application.properties)
Para proteção em massa sem anotar cada endpoint:
# Proteger toda a área de administração
quarkus.http.auth.permission.admin.paths=/admin/*
quarkus.http.auth.permission.admin.policy=admin-policy
quarkus.http.auth.policy.admin-policy.roles-allowed=ADMIN
# Permitir acesso público a recursos estáticos
quarkus.http.auth.permission.public.paths=/css/*,/js/*,/images/*,/jakarta.faces.resource/*
quarkus.http.auth.permission.public.policy=permit
# Área autenticada — qualquer usuário logado
quarkus.http.auth.permission.authenticated.paths=/perfil/*,/avaliacoes/*
quarkus.http.auth.permission.authenticated.policy=authenticated
Proteção Contra Ataques Web
CSRF (Cross-Site Request Forgery)
CSRF ocorre quando um site malicioso faz requisições em nome do usuário autenticado, aproveitando cookies de sessão. O Jakarta Faces inclui proteção CSRF nativa via ViewState:
<!-- O ViewState é um token CSRF automático no Jakarta Faces -->
<h:form>
<h:inputText value="#{bean.valor}" />
<h:commandButton value="Salvar" action="#{bean.salvar}" />
<!-- h:form gera automaticamente jakarta.faces.ViewState como hidden field -->
</h:form>
Para endpoints REST, configure proteção CSRF no Quarkus:
# Habilita proteção CSRF para formulários
quarkus.csrf-reactive.enabled=true
quarkus.csrf-reactive.form-field-name=csrf-token
XSS (Cross-Site Scripting)
XSS ocorre quando dados de entrada do usuário são renderizados sem sanitização, permitindo execução de scripts maliciosos. O Jakarta Faces escapa HTML por padrão:
<!-- SEGURO: h:outputText escapa HTML automaticamente -->
<h:outputText value="#{avaliacaoBean.comentario}" />
<!-- PERIGOSO: escape="false" — NUNCA use com dados de usuário -->
<h:outputText value="#{avaliacaoBean.comentario}" escape="false" />
Boas práticas adicionais:
// Validar e sanitizar input no backend
@POST
@Path("/avaliacoes")
@RolesAllowed("USER")
@Transactional
public Response criarAvaliacao(@Valid AvaliacaoDTO dto) {
// Bean Validation já rejeita inputs inválidos
// Adicionalmente, sanitize HTML se necessário:
String comentarioLimpo = Jsoup.clean(dto.comentario(), Safelist.none());
// ... criar avaliação com comentarioLimpo
}
SQL Injection
O JPA previne SQL Injection nativamente ao usar parâmetros nomeados ou posicionais:
// SEGURO — parâmetros são escapados pelo JPA
em.createQuery("SELECT u FROM Usuario u WHERE u.email = :email")
.setParameter("email", emailInput)
.getSingleResult();
// VULNERÁVEL — concatenação direta (NUNCA faça isso!)
em.createQuery("SELECT u FROM Usuario u WHERE u.email = '" + emailInput + "'");
Resumo de Proteções
| Ataque | Proteção | Responsável |
|---|---|---|
| CSRF | ViewState (Faces), tokens CSRF (REST) | Framework |
| XSS | Escape automático, sanitização | Framework + Desenvolvedor |
| SQL Injection | Parâmetros JPQL/Criteria | Framework (se usado corretamente) |
| Session Fixation | Renovação de sessão pós-login | Container |
| Brute Force | Rate limiting, BCrypt lento | Infraestrutura + Design |
Inicialização de Dados de Segurança
Para desenvolvimento e testes, crie usuários iniciais com o mecanismo @Startup do Quarkus:
@ApplicationScoped
public class SegurancaStartup {
@Inject
EntityManager em;
@Transactional
void onStart(@Observes StartupEvent event) {
// Criar usuários apenas se não existem
if (Usuario.count() == 0) {
Usuario admin = Usuario.criar(
"Administrador", "admin@critiquehub.com", "admin123", "ADMIN"
);
admin.persist();
Usuario moderador = Usuario.criar(
"Moderador", "mod@critiquehub.com", "mod123", "MODERATOR"
);
moderador.persist();
Usuario usuario = Usuario.criar(
"Usuário Teste", "user@critiquehub.com", "user123", "USER"
);
usuario.persist();
Log.info("Usuários de desenvolvimento criados com sucesso.");
}
}
}
SecurityContext — Acessando Informações do Usuário
O SecurityContext é o ponto central para acessar informações de segurança programaticamente:
@Path("/perfil")
@Authenticated
public class PerfilResource {
@Inject
SecurityContext securityContext;
@GET
public Response meuPerfil() {
// Obtém o Principal (identidade do usuário logado)
String email = securityContext.getUserPrincipal().getName();
Usuario usuario = Usuario.find("email", email).firstResult();
if (usuario == null) {
return Response.status(Status.NOT_FOUND).build();
}
return Response.ok(Map.of(
"nome", usuario.nome,
"email", usuario.email,
"role", usuario.role,
"isAdmin", securityContext.isUserInRole("ADMIN")
)).build();
}
}
Laboratório 9 — Login e Autorização no CritiqueHub
Objetivo
Implementar autenticação e autorização completas no CritiqueHub: entidade de usuário com roles, login via formulário, proteção de endpoints REST e restrições na interface Jakarta Faces.
Passo 1 — Entidade e Startup
Crie a entidade Usuario conforme os exemplos e o SegurancaStartup para popular dados iniciais.
Passo 2 — Configurar application.properties
# Segurança
quarkus.http.auth.form.enabled=true
quarkus.http.auth.form.login-page=/login.xhtml
quarkus.http.auth.form.error-page=/login-error.xhtml
quarkus.http.auth.form.landing-page=/index.xhtml
# Permissões por path
quarkus.http.auth.permission.public.paths=/,/index.xhtml,/filmes.xhtml,/jakarta.faces.resource/*
quarkus.http.auth.permission.public.policy=permit
quarkus.http.auth.permission.admin.paths=/admin/*
quarkus.http.auth.permission.admin.policy=admin-role
quarkus.http.auth.policy.admin-role.roles-allowed=ADMIN
quarkus.http.auth.permission.user.paths=/avaliacoes/*,/perfil/*
quarkus.http.auth.permission.user.policy=authenticated
Passo 3 — Página de Login
<!-- login.xhtml -->
<ui:composition template="/WEB-INF/templates/layout.xhtml">
<ui:define name="content">
<div class="login-container">
<h:form id="loginForm" prependId="false">
<p:card header="Acesso ao CritiqueHub">
<div class="p-field">
<p:outputLabel for="username" value="E-mail" />
<p:inputText id="j_username" />
</div>
<div class="p-field">
<p:outputLabel for="password" value="Senha" />
<p:password id="j_password" feedback="false" />
</div>
<p:commandButton value="Entrar"
ajax="false"
action="/j_security_check" />
</p:card>
</h:form>
</div>
</ui:define>
</ui:composition>
Passo 4 — Proteger Endpoints REST
Aplique @RolesAllowed nos endpoints de escrita e @PermitAll nos de leitura, conforme exemplos.
Passo 5 — Teste de Segurança
@QuarkusTest
class SegurancaTest {
@Test
void acessoPublicoDeveFuncionar() {
given()
.when()
.get("/filmes")
.then()
.statusCode(200);
}
@Test
void acessoSemAutenticacaoDeveRetornar401() {
given()
.when()
.post("/filmes")
.then()
.statusCode(401);
}
@Test
@TestSecurity(user = "admin@critiquehub.com", roles = "ADMIN")
void adminDeveCriarFilme() {
given()
.contentType(ContentType.JSON)
.body("""
{"titulo": "Novo Filme", "anoLancamento": 2024}
""")
.when()
.post("/filmes")
.then()
.statusCode(201);
}
@Test
@TestSecurity(user = "user@critiquehub.com", roles = "USER")
void usuarioComumNaoDeveDeletarFilme() {
given()
.when()
.delete("/filmes/1")
.then()
.statusCode(403);
}
}
Exercícios
- Adicione um role
MODERATORque possa editar filmes mas não deletá-los; teste com@TestSecurity - Implemente um endpoint
/auth/registroque permita cadastro público de novos usuários com roleUSER - Crie um
logged-user.xhtml(partial) que exiba o nome do usuário logado e um botão de logout no header do template - Configure rate limiting para o endpoint de login (máximo 5 tentativas por minuto por IP)
- Implemente um
AuditInterceptorque registre todas as operações de escrita (POST, PUT, DELETE) com o usuário, timestamp e endpoint acessado - Proteja contra XSS: crie um
@SanitizeHtmlBean Validation constraint que limpe tags HTML perigosas de campos de texto livre