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:
-
Mecanismo de autenticação HTTP
-
Loja de Identidade
-
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:
-
@BasicAuthenticationMechanismDefinition
-
@FormAuthenticationMechanismDefinition
-
@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.