Pular para o conteúdo principal

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, @PermitAll e @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:

PilarDescriçãoExemplo no CritiqueHub
AutenticaçãoVerificar a identidade do usuárioLogin com email/senha
AutorizaçãoVerificar permissões do usuário autenticadoAdmin pode deletar filmes, usuário comum não
ConfidencialidadeProteger dados em trânsito e em repousoHTTPS, 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) e roles (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 — Por que não MD5 ou SHA?

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çãoEfeitoUso
@RolesAllowed({"ADMIN"})Requer role específicoEndpoints administrativos
@PermitAllAcesso público, sem autenticaçãoPáginas de consulta
@DenyAllBloqueia todo acessoDesabilitar temporariamente
@AuthenticatedRequer 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

AtaqueProteçãoResponsável
CSRFViewState (Faces), tokens CSRF (REST)Framework
XSSEscape automático, sanitizaçãoFramework + Desenvolvedor
SQL InjectionParâmetros JPQL/CriteriaFramework (se usado corretamente)
Session FixationRenovação de sessão pós-loginContainer
Brute ForceRate limiting, BCrypt lentoInfraestrutura + 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

  1. Adicione um role MODERATOR que possa editar filmes mas não deletá-los; teste com @TestSecurity
  2. Implemente um endpoint /auth/registro que permita cadastro público de novos usuários com role USER
  3. Crie um logged-user.xhtml (partial) que exiba o nome do usuário logado e um botão de logout no header do template
  4. Configure rate limiting para o endpoint de login (máximo 5 tentativas por minuto por IP)
  5. Implemente um AuditInterceptor que registre todas as operações de escrita (POST, PUT, DELETE) com o usuário, timestamp e endpoint acessado
  6. Proteja contra XSS: crie um @SanitizeHtml Bean Validation constraint que limpe tags HTML perigosas de campos de texto livre

Referências