BT

Disseminando conhecimento e inovação em desenvolvimento de software corporativo.

Contribuir

Tópicos

Escolha a região

Início Artigos Jakarta Security e Rest na nuvem: Parte 1. Hello World da segurança

Jakarta Security e Rest na nuvem: Parte 1. Hello World da segurança

Fonte: https://br.freepik.com/fotos-vetores-gratis/tecnologia

Apesar de ser um aspecto bastante importante, segurança é um tópico pouco discutido na indústria do desenvolvimento de software. Como consequência, muitas decisões são realizadas sem levar em conta essa questão. Essa é uma série de artigos que falará sobre segurança no mundo Jakarta EE com Jakarta Security com microservices na nuvem. Nessa primeira parte, criaremos o "Hello world" da segurança, discutiremos um pouco sobre sua importância, os erros mais comuns, além de criarmos o nosso primeiro contato com a API do Jakarta Security, obviamente.

Muito tem se falado sobre segurança da informação, mas afinal, o que isso realmente significa? Resumidamente, a segurança da informação está relacionada à proteção dos dados dos usuários e da empresa em si, baseando-se nos pilares da ISO/IEC 17799:2005:

  • Confidencialidade: Propriedade que limita o acesso à informação tão somente às entidades legítimas, ou seja, àquelas autorizadas pelo proprietário da informação;
  • Integridade: Propriedade que garante que a informação manipulada mantenha todas as características originais estabelecidas pelo proprietário da informação, incluindo controle de mudanças e garantia do seu ciclo de vida (corrente, intermediária e permanente);
  • Disponibilidade: Propriedade que garante que a informação esteja sempre disponível para o uso legítimo, ou seja, por aqueles usuários autorizados pelo proprietário da informação;
  • Autenticidade: Propriedade que garante que a informação é proveniente da fonte anunciada e que não foi alvo de mutações ao longo de um processo.

Problemas de segurança resultam em grandes prejuízos para as empresas, seja no curto, médio ou longo prazo. Afinal, mesmo depois que a correção é realizada pela empresa, é necessário um tempo para voltar a credibilidade anterior, dando uma grande vantagem para a concorrência.

Primeiras dicas de segurança no Java

Uma boa maneira de começar a falar sobre segurança no Java são algumas dicas simples para evitar brechas no nosso sistema. Assim, listamos aqui os maiores erros de segurança.

  1. Problema de encapsulamento: É impossível começar com problemas de segurança sem falar do maior problema existente nas aplicações Java: os problemas de encapsulamento. Certamente, além de ser um problema de code smell deixar todas as classes e atributos públicos, essa atitude gera vários problemas de integridade dos dados. Afinal, o grande ponto sobre POO, segundo o clean code, está no fato de esconder os dados para expor o comportamento. É muito importante sempre pensar em ter uma API protegida.
  2. Queries concatenadas: Um problema muito comum, principalmente quando se opta pelo JDBC, está relacionado a fazer uma query no banco de dados relacional com uma string concatenada. Essa solução poderá resultar em SQL Injection.
  3. Cuidado com o Log: O log é um ponto importante, tanto para verificar o comportamento, quanto para verificar bugs. É muito importante ter atenção nos campos que serão expostos no "toString", por exemplo.
  4. Cuidado ao expor dados sensíveis: Muito comum nos microservices. É importante saber quais informações serão expostas e para quem. Existem algumas maneiras de evitar isso pensando apenas nos microservices. Por exemplo, utilizando a notação que ignora o campo a ser serializado, ou explicitando os dados que serão expostos criando uma Camada DTO.
  5. Evite a serialização do Java: É muito importante ter atenção no uso dessa interface. Nela existem diversos problemas de segurança, assim, utilize a interface Serializable apenas se for necessária. Lembre-se que, na grande maioria dos casos, seu uso não é recomendado.
  6. Cuidado com encriptação ou algoritmos de hash: Obviamente, o uso de criptografia é bastante importante, portanto, tenha atenção na implementação.
  7. Atenção com suas dependências: Existem diversos estudos que se calcula que cerca de 90% do código que se coloca em produção está relacionado a projetos de terceiros. Assim, é muito importante ter a devida atenção na atualização dos softwares, incluindo a JVM em si. Tenha em mente que além de novas funcionalidades e melhorias de performances, as atualizações ajudam a corrigir diversos problemas de segurança se a fazemos com a devida frequência.
  8. Senha de banco de dados: Esse é um dos maiores problemas de segurança. Evite colocar a senha dentro do código. O The Twelve Factor App fala bastante sobre as vantagens da configuração. Além disso, o NoSQL não significa NoSecurity, portanto, é importante colocar senha ou restringir o acesso desse tipo de banco de dados. Existe um estudo que fala que cerca de 75% dos bancos redis que estrão expostos ao público não utilizam senha.
  9. Acesso dos servidores e banco de dados: Este problema está relacionado muito mais a operações, porém, é muito importante verificar acesso dos servidores e banco de dados. Ou seja, é importante que um servidor acesse os bancos de dados que necessitam e que apenas os servidores de acesso público e as portas necessárias sejam expostas.
  10. Utilizar ferramentas de segurança não é algo ruim. Atualmente existem ferramentas que gerenciam a segurança e tem especialistas focados nisso. Muito pior que pagar por uma solução cara e madura, é reinventar a roda com uma solução que causa diversos problemas de segurança, sem falar que resulta num gasto de tempo e grande trabalho, fazendo com que o foco saia do nosso negócio.

Uma boa dica para evitar o grande número desses erros de segurança, é olhar as 10 maiores boas práticas de segurança Java escrita pela Snyk.

Hello world

Após explicar os conceitos de segurança, vamos criar um Hello World com a API de segurança provida pelo Jakarta EE. Faremos uma aplicação muito simples, utilizando como implementação o Payara. Criaremos diversos recursos e cada um deles retornará um texto simples, porém, cada serviço disponível terá sua respectiva regra de permissão, dado que temos três regras de acesso (gerente, usuário e admin) e teremos os seguintes serviços:

  • Um que todos acessam sem problema algum;
  • Um que apenas o admin acessa;
  • Uma que apenas o gerente e o admin acessam;
  • Uma que o usuário acessa;
  • Uma que ninguém acessa, mas, qual o objetivo deste exemplo? Seria recurso que não está disponível para nenhum usuário, ainda.

Se já trabalhamos com o JAX-RS, então, estamos habituados a criar uma classe que herdará o Application e terá a notação ApplicationPath. Com segurança, essa classe terá mais algumas informações. Nesse caso, definiremos as regras que serão utilizadas na aplicação. No nosso caso, também utilizaremos um dos mecanismos de autenticação que a API já disponibiliza.

Por padrão a API de segurança do Jakarta EE traz os seguintes mecanismos de autenticação:

  • BasicAuthenticationMechanismDefinition
  • FormAuthenticationMechanismDefinition
  • CustomFormAuthenrticationMechanismDefinition

Nesse caso utilizaremos o mecanismo Basic, que não explicaremos neste artigo porque teremos um texto totalmente dedicado a esse mecanismo.

import javax.annotation.security.DeclareRoles;
import javax.enterprise.context.ApplicationScoped;
import javax.security.enterprise.authentication.mechanism.http.BasicAuthenticationMechanismDefinition;
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;

@ApplicationPath("")
@BasicAuthenticationMechanismDefinition(realmName = "userRealm")
@ApplicationScoped
@DeclareRoles({"ADMIN", "MANAGER", "USER"})  // You need to indicate all roles that are used by the app
public class ApplicationConfig extends Application {
}

Tão logo o usuário envia as informações via Basic, precisaremos validar as credenciais. Ou seja, primeiro o usuário se autentica para que, em seguida, seja validado. No Jakarta EE Security esse processo de validação é realizado graças ao IdentityStore que pode ter diversas implementações, como banco de dados, arquivos ou LDAP. No nosso exemplo, isso será realmente simples e tudo será gerenciado pela memória e pelo nome do usuário.

import javax.enterprise.context.ApplicationScoped;
import javax.security.enterprise.credential.Credential;
import javax.security.enterprise.credential.UsernamePasswordCredential;
import javax.security.enterprise.identitystore.CredentialValidationResult;
import javax.security.enterprise.identitystore.IdentityStore;
import java.util.Collections;

import static javax.security.enterprise.identitystore.CredentialValidationResult.INVALID_RESULT;

@ApplicationScoped
public class InMemoryIdentityStore implements IdentityStore {

    @Override
    public int priority() {
        return 10;
    }

    @Override
    public CredentialValidationResult validate(Credential credential) {

        if (credential instanceof UsernamePasswordCredential) {
            UsernamePasswordCredential user = UsernamePasswordCredential
                    .class.cast(credential);

            switch (user.getCaller()) {
                case "admin":
                    return new CredentialValidationResult("admin", Collections.singleton("ADMIN"));
                case "manager":
                    return new CredentialValidationResult("admin", Collections.singleton("MANAGER"));
                case "user":
                    return new CredentialValidationResult("admin", Collections.singleton("USER"));
                default:
                    return INVALID_RESULT;
            }

        }
        return INVALID_RESULT;
    }

}

Depois desse super mecanismo de validação o último passo é criar os serviços e identificá-los de acordo com a regra de permissão.

import javax.annotation.security.DenyAll;
import javax.annotation.security.PermitAll;
import javax.annotation.security.RolesAllowed;
import javax.enterprise.context.RequestScoped;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;

@Path("")
@RequestScoped
public class HelloWorldResource {

    @GET
    @PermitAll
    @Produces("text/plain")
    public String doGet() {
        return "hello from everyone";
    }

    @Path("admin")
    @GET
    @RolesAllowed("ADMIN")
    @Produces("text/plain")
    public String admin() {
        return "hello from admin";
    }

    @Path("manager")
    @GET
    @RolesAllowed({"MANAGER", "ADMIN"})
    @Produces("text/plain")
    public String manager() {
        return "hello from manager";
    }

    @Path("user")
    @GET
    @RolesAllowed({"MANAGER", "ADMIN", "USER"})
    @Produces("text/plain")
    public String user() {
        return "hello from user";
    }

    @Path("nobody")
    @GET
    @DenyAll
    @Produces("text/plain")
    public String nobody() {
        return "hello from nobody";
    }
}

A aplicação está pronta para utilizarmos! Uma coisa interessante deste exemplo é que, podemos testar os retornos e os respectivos códigos, como os das situações de 401 e de 403.

Movendo para a nuvem

Já tive o prazer de falar várias vezes das vantagens do uso da cloud computing sem comentar sobre o seu risco, de acordo do tipo de serviço que foi escolhido. Uma das grandes vantagens do PaaS está na abstração da camada de infraestrutura, diminuindo consideravelmente o risco de migração para o cloud, inclusive, da segurança. Ter um time de serviço que será responsável pelo controle de acesso aos servidores e dos containers dentro do cluster, além do processo de atualização de banco de dados e da automação de backup, são pontos muito importantes, de bastante abstração , que um PaaS poderá oferecer.

Para facilitar a migração do código local para um ambiente na nuvem, utilizaremos um PaaS, no caso, o Platform.sh. De maneira bem resumida, ele é a segunda geração de PaaS do qual irá gerenciar todos os recurso para nós, num conceito de infraestrutura como código. Assim, basta fazer o push no nosso repositório Git que o PaaS se responsabilizará de criar os containers, configurar as permissões de acesso dentro do cluster e finalizar o deploy em produção.

Para fazer o deploy precisamos de três arquivos: Um para definir as informações da aplicação, outro para os serviços que a aplicação precisa e, a última para definir as rotas. De modo que teremos o seguinte:

Para configurar as rotas

"https://{default}/":
  type: upstream
  upstream: "app:http"

"https://www.{default}/":
  type: redirect
  to: "https://{default}/"

Para configurar a aplicação:

name: app
type: "java:11"
disk: 1024
hooks:
    build:  mvn clean package payara-micro:bundle
web:
    commands:
        start: java -jar -Xmx$(jq .info.limits.memory /run/config.json)m -XX:+ExitOnOutOfMemoryError target/microprofile-microbundle.jar --port $PORT

Como utilizamos tudo na memória não é necessário utilizar o arquivo de serviços

Nesse artigo falamos um pouco sobre o aspecto de segurança, a importância de se pensar nela, as dicas de como evitar problemas deste tipo, além de iniciar os conceitos da especificação de segurança. Uma coisa importante de nível de código foi que conseguimos fazer esse exemplo prático com apenas três classes. Isso demonstra, claramente, que houve uma melhoria considerável na API de segurança dentro das especificações do Java. Na segunda parte, falaremos um pouco sobre o BASIC, suas vantagens e desvantagens. Para ver a aplicação final desta série, dê uma olhada neste link.

Sobre o autor

Otávio Santana é engenheiro de software, com grande experiência em desenvolvimento opensource, com diversas contribuições ao JBoss Weld, Hibernate, Apache Commons e outros projetos. Focado em desenvolvimento poliglota e aplicações de alto desempenho, Otávio trabalhou em grandes projetos nas áreas de finanças, governo, mídias sociais e e-commerce. Membro do comitê executivo do JCP e de vários Expert Groups de JSRs, é também Java Champion e recebeu os prêmios JCP Outstanding Award e Duke's Choice Award.

Avalie esse artigo

Relevância
Estilo/Redação

Conteúdo educacional

BT