API de segurança Java EE 8

API de segurança Java EE 8

1. Visão geral

A API de segurança Java EE 8 é o novo padrão e uma maneira portátil de lidar com questões de segurança em contêineres Java.

Neste artigo,we’ll look at the three core features of the API:

  1. Mecanismo de autenticação HTTP

  2. Loja de Identidade

  3. Contexto de Segurança

Vamos primeiro entender como configurar as implementações fornecidas e, em seguida, como implementar uma personalizada.

2. Dependências do Maven

Para configurar a API de segurança do Java EE 8, precisamos de uma implementação fornecida pelo servidor ou explícita.

2.1. Usando a implementação do servidor

Os servidores compatíveis com Java EE 8 já fornecem uma implementação para a API de segurança Java EE 8 e, portanto, precisamos apenas do artefato MavenJava EE Web Profile API:


    
        javax
        javaee-web-api
        8.0
        provided
    

2.2. Usando uma implementação explícita

Primeiro, especificamos o artefato Maven para o Java EE 8Security API:


    
        javax.security.enterprise
        javax.security.enterprise-api
        1.0
    

E então, vamos adicionar uma implementação, por exemplo,Soteria - a implementação de referência:


    
        org.glassfish.soteria
        javax.security.enterprise
        1.0
    

3. Mecanismo de autenticação HTTP

Antes do Java EE 8, configuramos mecanismos de autenticação declarativamente por meio do arquivoweb.xml.

Nesta versão, a API de segurança Java EE 8 projetou a nova interfaceHttpAuthenticationMechanism como uma substituição. Portanto, aplicativos da Web agora podem configurar mecanismos de autenticação, fornecendo implementações dessa interface.

Felizmente, o contêiner já fornece uma implementação para cada um dos três métodos de autenticação definidos pela especificação do Servlet: autenticação HTTP básica, autenticação baseada em formulário e autenticação baseada em formulário personalizada.

Ele também fornece uma anotação para acionar cada implementação:

  1. @BasicAuthenticationMechanismDefinition

  2. @FormAuthenticationMechanismDefinition

  3. @CustomFormAuthenrticationMechanismDefinition

3.1. Autenticação HTTP básica

Conforme mencionado acima, um aplicativo da web pode configurar a autenticação HTTP básica usando apenas o@BasicAuthenticationMechanismDefinition annotation on a CDI bean:

@BasicAuthenticationMechanismDefinition(
  realmName = "userRealm")
@ApplicationScoped
public class AppConfig{}

Neste ponto, o contêiner Servlet procura e instancia a implementação fornecida da interfaceHttpAuthenticationMechanism.

Após o recebimento de uma solicitação não autorizada, o contêiner desafia o cliente a fornecer informações de autenticação adequadas por meio do cabeçalho de respostaWWW-Authenticate.

WWW-Authenticate: Basic realm="userRealm"

O cliente então envia o nome de usuário e a senha, separados por dois pontos “:” e codificados em Base64, por meio do cabeçalho de solicitaçãoAuthorization:

//user=example, password=example
Authorization: Basic YmFlbGR1bmc6YmFlbGR1bmc=

Observe que a caixa de diálogo apresentada para fornecer credenciais é proveniente do navegador e não do servidor.

3.2. Autenticação HTTP baseada em formulário

The @FormAuthenticationMechanismDefinition annotation triggers a form-based authentication conforme definido pela especificação do Servlet.

Então, temos a opção de especificar o login e as páginas de erro ou usar os padrões razoáveis/logine/login-error:

@FormAuthenticationMechanismDefinition(
  loginToContinue = @LoginToContinue(
    loginPage = "/login.html",
    errorPage = "/login-error.html"))
@ApplicationScoped
public class AppConfig{}

Como resultado da invocação deloginPage,, o servidor deve enviar o formulário ao cliente:

O cliente deve enviar o formulário para um processo de autenticação de backup predefinido fornecido pelo contêiner.

3.3. Autenticação HTTP baseada em formulário personalizado

Um aplicativo da web pode acionar a implementação de autenticação baseada em formulário personalizado usando a anotação@CustomFormAuthenticationMechanismDefinition:

@CustomFormAuthenticationMechanismDefinition(
  loginToContinue = @LoginToContinue(loginPage = "/login.xhtml"))
@ApplicationScoped
public class AppConfig {
}

Mas, ao contrário da autenticação baseada em formulário padrão, estamos configurando uma página de login personalizada e invocando o métodoSecurityContext.authenticate() como um processo de autenticação de apoio.

Vamos dar uma olhada no backingLoginBean também, que contém a lógica de login:

@Named
@RequestScoped
public class LoginBean {

    @Inject
    private SecurityContext securityContext;

    @NotNull private String username;

    @NotNull private String password;

    public void login() {
        Credential credential = new UsernamePasswordCredential(
          username, new Password(password));
        AuthenticationStatus status = securityContext
          .authenticate(
            getHttpRequestFromFacesContext(),
            getHttpResponseFromFacesContext(),
            withParams().credential(credential));
        // ...
    }

    // ...
}

Como resultado da chamada da páginalogin.xhtml personalizada, o cliente envia o formulário recebido para o métodoLoginBean'slogin():

//...

3.4. Mecanismo de autenticação personalizado

A interfaceHttpAuthenticationMechanism define três métodos. The most important is the validateRequest() que devemos fornecer uma implementação.

O comportamento padrão para os outros dois métodos,secureResponse()ecleanSubject()*,*, é suficiente na maioria dos casos.

Vamos dar uma olhada em um exemplo de implementação:

@ApplicationScoped
public class CustomAuthentication
  implements HttpAuthenticationMechanism {

    @Override
    public AuthenticationStatus validateRequest(
      HttpServletRequest request,
      HttpServletResponse response,
      HttpMessageContext httpMsgContext)
      throws AuthenticationException {

        String username = request.getParameter("username");
        String password = response.getParameter("password");
        // mocking UserDetail, but in real life, we can obtain it from a database
        UserDetail userDetail = findByUserNameAndPassword(username, password);
        if (userDetail != null) {
            return httpMsgContext.notifyContainerAboutLogin(
              new CustomPrincipal(userDetail),
              new HashSet<>(userDetail.getRoles()));
        }
        return httpMsgContext.responseUnauthorized();
    }
    //...
}

Aqui, a implementação fornece a lógica de negócios do processo de validação, mas na prática, é recomendável delegar paraIdentityStore por meio deIdentityStoreHandler by invocandovalidate.

Também anotamos a implementação com a anotação@ApplicationScoped, pois precisamos torná-la habilitada para CDI.

Após uma verificação válida da credencial e uma eventual recuperação das funções do usuário,the implementation should notify the container then:

HttpMessageContext.notifyContainerAboutLogin(Principal principal, Set groups)

3.5. Aplicação da segurança do servlet

A web application can enforce security constraints by using the @ServletSecurity annotation on a Servlet implementation:

@WebServlet("/secured")
@ServletSecurity(
  value = @HttpConstraint(rolesAllowed = {"admin_role"}),
  httpMethodConstraints = {
    @HttpMethodConstraint(
      value = "GET",
      rolesAllowed = {"user_role"}),
    @HttpMethodConstraint(
      value = "POST",
      rolesAllowed = {"admin_role"})
  })
public class SecuredServlet extends HttpServlet {
}

Essa anotação tem dois atributos -httpMethodConstraintsevalue; httpMethodConstraints é usado para especificar uma ou mais restrições, cada uma representando um controle de acesso a um método HTTP por uma lista de funções permitidas.

O container irá então verificar, a cadaurl-patterne método HTTP, se o usuário conectado tem a função adequada para acessar o recurso.

4. Loja de Identidade

Este recurso é abstraído porthe IdentityStore interface, and it’s used to validate credentials and eventually retrieve group membership. Em outras palavras, ele pode fornecer recursos para autenticação, autorização ou ambos.

IdentityStore destina-se e é incentivado a ser usado porHttpAuthenticationMecanism por meio de uma interface chamadaIdentityStoreHandler. Uma implementação padrão deIdentityStoreHandler é fornecida pelo scontainer Servlet .

Um aplicativo pode fornecer sua implementação deIdentityStore ou usar uma das duas implementações internas fornecidas pelo contêiner para Banco de Dados e LDAP.

4.1. Lojas de identidade internas

O servidor compatível com Java EE deve fornecer implementações parathe two Identity Stores: Database and LDAP.

A implementação do banco de dadosIdentityStore é inicializada pela passagem de dados de configuração para a anotação@DataBaseIdentityStoreDefinition:

@DatabaseIdentityStoreDefinition(
  dataSourceLookup = "java:comp/env/jdbc/securityDS",
  callerQuery = "select password from users where username = ?",
  groupsQuery = "select GROUPNAME from groups where username = ?",
  priority=30)
@ApplicationScoped
public class AppConfig {
}

Como dados de configuração,we need a JNDI data source to an external database, são configurados dois comandos JDBC para verificar o chamador e seus grupos e, finalmente, um parâmetro de prioridade que é usado no caso de armazenamento múltiplo.

IdentityStore com alta prioridade é processado posteriormente peloIdentityStoreHandler.

Como o banco de dados,LDAP IdentityStore implementation is initialized through the @LdapIdentityStoreDefinition passando os dados de configuração:

@LdapIdentityStoreDefinition(
  url = "ldap://localhost:10389",
  callerBaseDn = "ou=caller,dc=example,dc=com",
  groupSearchBase = "ou=group,dc=example,dc=com",
  groupSearchFilter = "(&(member=%s)(objectClass=groupOfNames))")
@ApplicationScoped
public class AppConfig {
}

Aqui precisamos da URL de um servidor LDAP externo, como pesquisar o chamador no diretório LDAP e como recuperar seus grupos.

4.2. Implementando umIdentityStore personalizado

A interfaceIdentityStore define quatro métodos padrão:

default CredentialValidationResult validate(
  Credential credential)
default Set getCallerGroups(
  CredentialValidationResult validationResult)
default int priority()
default Set validationTypes()

O métodopriority() retorna um valor para a ordem de iteração em que esta implementação é processada porIdentityStoreHandler. UmIdentityStore com prioridade mais baixa é tratado primeiro.

Por padrão, umIdentityStore processa a validação de credenciais(ValidationType.VALIDATE)e a recuperação de grupo (ValidationType.PROVIDE_GROUPS). Podemos substituir esse comportamento para que ele possa fornecer apenas um recurso.

Assim, podemos configurar oIdentityStore para ser usado apenas para validação de credenciais:

@Override
public Set validationTypes() {
    return EnumSet.of(ValidationType.VALIDATE);
}

Nesse caso, devemos fornecer uma implementação para o métodovalidate():

@ApplicationScoped
public class InMemoryIdentityStore implements IdentityStore {
    // init from a file or harcoded
    private Map users = new HashMap<>();

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

    @Override
    public Set validationTypes() {
        return EnumSet.of(ValidationType.VALIDATE);
    }

    public CredentialValidationResult validate(
      UsernamePasswordCredential credential) {

        UserDetails user = users.get(credential.getCaller());
        if (credential.compareTo(user.getLogin(), user.getPassword())) {
            return new CredentialValidationResult(user.getLogin());
        }
        return INVALID_RESULT;
    }
}

Ou podemos escolher configurar oIdentityStore para que possa ser usado apenas para recuperação de grupo:

@Override
public Set validationTypes() {
    return EnumSet.of(ValidationType.PROVIDE_GROUPS);
}

Devemos então fornecer uma implementação para os métodosgetCallerGroups():

@ApplicationScoped
public class InMemoryIdentityStore implements IdentityStore {
    // init from a file or harcoded
    private Map users = new HashMap<>();

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

    @Override
    public Set validationTypes() {
        return EnumSet.of(ValidationType.PROVIDE_GROUPS);
    }

    @Override
    public Set getCallerGroups(CredentialValidationResult validationResult) {
        UserDetails user = users.get(
          validationResult.getCallerPrincipal().getName());
        return new HashSet<>(user.getRoles());
    }
}

ComoIdentityStoreHandler espera que a implementação seja um bean CDI, nós o decoramos com a anotaçãoApplicationScoped.

5. API de contexto de segurança

A API de segurança Java EE 8 fornecean access point to programmatic security through the SecurityContext interface. É uma alternativa quando o modelo de segurança declarativo imposto pelo contêiner não é suficiente.

Uma implementação padrão da interfaceSecurityContext deve ser fornecida no tempo de execução como um bean CDI e, portanto, precisamos injetá-lo:

@Inject
SecurityContext securityContext;

Nesse ponto, podemos autenticar o usuário, recuperar um autenticado, verificar sua participação na função e conceder ou negar acesso ao recurso da web através dos cinco métodos disponíveis.

5.1. Recuperando dados do chamador

Nas versões anteriores do Java EE, recuperávamosPrincipal ou veríamos a associação da função de maneira diferente em cada contêiner.

Embora usemos os métodosgetUserPrincipal()e isUserInRole() deHttpServletRequest em um contêiner de servlet, métodos semelhantesgetCallerPrincipal() areiaisCallerInRole() methods paraEJBContext são usados ​​em Container EJB.

A nova API de segurança Java EE 8 padronizou esteby espalhando um método semelhante por meio da interfaceSecurityContext:

Principal getCallerPrincipal();
boolean isCallerInRole(String role);
 Set getPrincipalsByType(Class type);

O métodogetCallerPrincipal() retorna uma representação específica do contêiner do chamador autenticado, enquanto o métodogetPrincipalsByType() recupera todos os principais de um determinado tipo.

Pode ser útil caso o chamador específico do aplicativo seja diferente do contêiner.

5.2. Testando o Acesso a Recursos da Web

Primeiro, precisamos configurar um recurso protegido:

@WebServlet("/protectedServlet")
@ServletSecurity(@HttpConstraint(rolesAllowed = "USER_ROLE"))
public class ProtectedServlet extends HttpServlet {
    //...
}

E então, para verificar o acesso a este recurso protegido, devemos invocar ohasAccessToWebResource() method:

securityContext.hasAccessToWebResource("/protectedServlet", "GET");

Neste caso, o método retorna verdadeiro se o usuário estiver na funçãoUSER_ROLE.

5.3. Autenticando o chamador programaticamente

Um aplicativo pode acionar programaticamente o processo de autenticação invocandoauthenticate():

AuthenticationStatus authenticate(
  HttpServletRequest request,
  HttpServletResponse response,
  AuthenticationParameters parameters);

O contêiner é notificado e, por sua vez, invocará o mecanismo de autenticação configurado para o aplicativo. O parâmetroAuthenticationParameters fornece uma credencial paraHttpAuthenticationMechanism:

withParams().credential(credential)

Os valoresSUCCESSeSEND_FAILURE deAuthenticationStatus projetam uma autenticação bem-sucedida e com falha, enquantoSEND_CONTINUE sinaliza um status em andamento do processo de autenticação.

6. Executando os exemplos

Para destacar esses exemplos, usamos a versão de desenvolvimento mais recente do servidorOpen Liberty que oferece suporte a Java EE 8. Ele é baixado e instalado graças aoliberty-maven-plugin, que também pode implantar o aplicativo e iniciar o servidor.

Para executar os exemplos, basta acessar o módulo correspondente e chamar este comando:

mvn clean package liberty:run

Como resultado, o Maven fará o download do servidor, criará, implantará e executará o aplicativo.

7. Conclusão

Neste artigo, abordamos a configuração e implementação dos principais recursos da nova API de segurança Java EE 8.

Primeiro, começamos mostrando como configurar os mecanismos de autenticação internos padrão e como implementar um mecanismo personalizado. Mais tarde, vimos como configurar o Identity Store interno e como implementar um personalizado. E, finalmente, vimos como chamar métodos deSecurityContext.

Como sempre, os exemplos de código para este artigo estão disponíveisover on GitHub.