API de sécurité Java EE 8

API de sécurité Java EE 8

1. Vue d'ensemble

L'API de sécurité Java EE 8 est la nouvelle norme et un moyen portable de gérer les problèmes de sécurité dans les conteneurs Java.

Dans cet article,we’ll look at the three core features of the API:

  1. Mécanisme d'authentification HTTP

  2. Magasin d'identité

  3. Contexte de sécurité

Nous allons d'abord comprendre comment configurer les implémentations fournies, puis comment en implémenter une personnalisée.

2. Dépendances Maven

Pour configurer l'API de sécurité Java EE 8, nous avons besoin d'une implémentation fournie par le serveur ou explicite.

2.1. Utilisation de l'implémentation du serveur

Les serveurs compatibles Java EE 8 fournissent déjà une implémentation pour l'API de sécurité Java EE 8, et nous n'avons donc besoin que de l'artefact MavenJava EE Web Profile API:


    
        javax
        javaee-web-api
        8.0
        provided
    

2.2. Utilisation d'une implémentation explicite

Tout d'abord, nous spécifions l'artefact Maven pour les Java EE 8Security API:


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

Ensuite, nous ajouterons une implémentation, par exemple,Soteria - l'implémentation de référence:


    
        org.glassfish.soteria
        javax.security.enterprise
        1.0
    

3. Mécanisme d'authentification HTTP

Avant Java EE 8, nous avons configuré les mécanismes d'authentification de manière déclarative via le fichierweb.xml.

Dans cette version, l'API de sécurité Java EE 8 a conçu la nouvelle interfaceHttpAuthenticationMechanism en remplacement. Par conséquent, les applications Web peuvent maintenant configurer les mécanismes d'authentification en fournissant des implémentations de cette interface.

Heureusement, le conteneur fournit déjà une implémentation pour chacune des trois méthodes d'authentification définies par la spécification Servlet: authentification HTTP de base, authentification basée sur un formulaire et authentification personnalisée basée sur un formulaire.

Il fournit également une annotation pour déclencher chaque implémentation:

  1. @BasicAuthenticationMechanismDefinition

  2. @FormAuthenticationMechanismDefinition

  3. @CustomFormAuthenrticationMechanismDefinition

3.1. Authentification HTTP de base

Comme mentionné ci-dessus, une application Web peut configurer l'authentification HTTP de base simplement en utilisant les@BasicAuthenticationMechanismDefinition annotation on a CDI bean:

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

À ce stade, le conteneur Servlet recherche et instancie l'implémentation fournie de l'interfaceHttpAuthenticationMechanism.

À la réception d'une demande non autorisée, le conteneur défie le client de fournir des informations d'authentification appropriées via l'en-tête de réponseWWW-Authenticate.

WWW-Authenticate: Basic realm="userRealm"

Le client envoie ensuite le nom d'utilisateur et le mot de passe, séparés par deux points «:» et encodés en Base64, via l'en-tête de requêteAuthorization:

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

Notez que la boîte de dialogue présentée pour fournir les informations d'identification provient du navigateur et non du serveur.

3.2. Authentification HTTP basée sur un formulaire

The @FormAuthenticationMechanismDefinition annotation triggers a form-based authentication comme défini par la spécification Servlet.

Ensuite, nous avons la possibilité de spécifier les pages de connexion et d'erreur ou d'utiliser les pages raisonnables par défaut/login et/login-error:

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

Suite à l'appel deloginPage,, le serveur doit envoyer le formulaire au client:

Le client doit ensuite envoyer le formulaire à un processus d'authentification de sauvegarde prédéfini fourni par le conteneur.

3.3. Authentification HTTP basée sur un formulaire personnalisé

Une application Web peut déclencher l'implémentation de l'authentification basée sur un formulaire personnalisé à l'aide de l'annotation@CustomFormAuthenticationMechanismDefinition:

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

Mais contrairement à l'authentification par formulaire par défaut, nous configurons une page de connexion personnalisée et invoquons la méthodeSecurityContext.authenticate() comme processus d'authentification de sauvegarde.

Jetons également un œil au supportLoginBean, qui contient la logique de connexion:

@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));
        // ...
    }

    // ...
}

À la suite de l'appel de la pagelogin.xhtml personnalisée, le client soumet le formulaire reçu à la méthodeLoginBean'slogin():

//...

3.4. Mécanisme d'authentification personnalisé

L'interfaceHttpAuthenticationMechanism définit trois méthodes. The most important is the validateRequest() dont nous devons fournir une implémentation.

Le comportement par défaut des deux autres méthodes,secureResponse() etcleanSubject()*,* est suffisant dans la plupart des cas.

Jetons un œil à un exemple de mise en œuvre:

@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();
    }
    //...
}

Ici, l'implémentation fournit la logique métier du processus de validation, mais dans la pratique, il est recommandé de déléguer auxIdentityStore via lesIdentityStoreHandler by en invoquantvalidate.

Nous avons également annoté l'implémentation avec l'annotation@ApplicationScoped car nous devons la rendre compatible CDI.

Après une vérification valide des informations d'identification et une récupération éventuelle des rôles utilisateur,the implementation should notify the container then:

HttpMessageContext.notifyContainerAboutLogin(Principal principal, Set groups)

3.5. Application de la sécurité des servlets

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 {
}

Cette annotation a deux attributs -httpMethodConstraints etvalue; httpMethodConstraints permet de spécifier une ou plusieurs contraintes, chacune représentant un contrôle d'accès à une méthode HTTP par une liste de rôles autorisés.

Le conteneur vérifiera ensuite, pour chaqueurl-pattern et méthode HTTP, si l'utilisateur connecté a le rôle approprié pour accéder à la ressource.

4. Magasin d'identité

Cette fonctionnalité est abstraite parthe IdentityStore interface, and it’s used to validate credentials and eventually retrieve group membership.  En d'autres termes, elle peut fournir des capacités d'authentification, d'autorisation ou les deux.

IdentityStore est destiné et encouragé à être utilisé par lesHttpAuthenticationMecanism via une interface appeléeIdentityStoreHandler. Une implémentation par défaut desIdentityStoreHandler est fournie par le scontainer Servlet .

Une application peut fournir son implémentation desIdentityStore ou utilise l'une des deux implémentations intégrées fournies par le conteneur pour Database et LDAP.

4.1. Magasins d'identité intégrés

Le serveur compatible Java EE doit fournir des implémentations pourthe two Identity Stores: Database and LDAP.

L'implémentation de la base de donnéesIdentityStore est initialisée en passant une donnée de configuration à l'annotation@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 {
}

En tant que données de configuration,we need a JNDI data source to an external database, deux instructions JDBC pour vérifier l'appelant et ses groupes et enfin un paramètre de priorité qui est utilisé en cas de stockage multiple sont configurés.

IdentityStore avec une priorité élevée est traité ultérieurement par lesIdentityStoreHandler.

Comme la base de données,LDAP IdentityStore implementation is initialized through the @LdapIdentityStoreDefinition en passant les données de configuration:

@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 {
}

Ici, nous avons besoin de l'URL d'un serveur LDAP externe, de la recherche de l'appelant dans l'annuaire LDAP et de la récupération de ses groupes.

4.2. Implémentation d'unIdentityStore personnalisé

L'interfaceIdentityStore définit quatre méthodes par défaut:

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

La méthodepriority() renvoie une valeur pour l'ordre d'itération que cette implémentation est traitée parIdentityStoreHandler. UnIdentityStore avec une priorité inférieure est traité en premier.

Par défaut, unIdentityStore traite à la fois la validation des informations d'identification(ValidationType.VALIDATE) et la récupération de groupe (ValidationType.PROVIDE_GROUPS). Nous pouvons remplacer ce comportement afin qu’il ne puisse fournir qu’une seule capacité.

Ainsi, nous pouvons configurer lesIdentityStore à utiliser uniquement pour la validation des informations d'identification:

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

Dans ce cas, nous devrions fournir une implémentation pour la méthodevalidate():

@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 nous pouvons choisir de configurer lesIdentityStore afin qu'il ne puisse être utilisé que pour la récupération de groupe:

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

Nous devrions alors fournir une implémentation pour les méthodesgetCallerGroups():

@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());
    }
}

CommeIdentityStoreHandler s'attend à ce que l'implémentation soit un bean CDI, nous la décorons avec l'annotationApplicationScoped.

5. API de contexte de sécurité

L'API de sécurité Java EE 8 fournit desan access point to programmatic security through the SecurityContext interface. Il s'agit d'une alternative lorsque le modèle de sécurité déclaratif appliqué par le conteneur n'est pas suffisant.

Une implémentation par défaut de l'interfaceSecurityContext doit être fournie à l'exécution en tant que bean CDI, et nous devons donc l'injecter:

@Inject
SecurityContext securityContext;

À ce stade, nous pouvons authentifier l'utilisateur, récupérer un utilisateur authentifié, vérifier son appartenance à un rôle et accorder ou refuser l'accès à une ressource Web via les cinq méthodes disponibles.

5.1. Récupération des données de l'appelant

Dans les versions précédentes de Java EE, nous récupérons lesPrincipal ou vérifions l'appartenance au rôle différemment dans chaque conteneur.

Alors que nous utilisons les méthodesgetUserPrincipal() et isUserInRole() desHttpServletRequest dans un conteneur de servlet, des méthodes similairesgetCallerPrincipal() andisCallerInRole() methods desEJBContext sont utilisées dans Conteneur EJB.

La nouvelle API de sécurité Java EE 8 a normalisé ceby en fournissant une méthode similaire via l'interfaceSecurityContext:

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

La méthodegetCallerPrincipal() renvoie une représentation spécifique au conteneur de l'appelant authentifié tandis que la méthodegetPrincipalsByType() récupère tous les principaux d'un type donné.

Cela peut être utile si l'appelant spécifique à l'application est différent de l'appelant conteneur.

5.2. Test d'accès aux ressources Web

Tout d'abord, nous devons configurer une ressource protégée:

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

Et puis, pour vérifier l'accès à cette ressource protégée, nous devons appeler lehasAccessToWebResource() method:

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

Dans ce cas, la méthode renvoie true si l'utilisateur est dans le rôleUSER_ROLE.

5.3. Authentification de l'appelant par programme

Une application peut déclencher par programme le processus d'authentification en appelantauthenticate():

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

Le conteneur est alors notifié et invoque à son tour le mécanisme d'authentification configuré pour l'application. Le paramètreAuthenticationParameters fournit une information d'identification àHttpAuthenticationMechanism:

withParams().credential(credential)

Les valeursSUCCESS etSEND_FAILURE desAuthenticationStatus définissent une authentification réussie et échouée tandis queSEND_CONTINUE signale un état en cours du processus d'authentification.

6. Exécuter les exemples

Pour mettre en évidence ces exemples, nous avons utilisé la dernière version de développement du serveurOpen Liberty qui prend en charge Java EE 8. Celui-ci est téléchargé et installé grâce auxliberty-maven-plugin qui peuvent également déployer l'application et démarrer le serveur.

Pour exécuter les exemples, accédez simplement au module correspondant et appelez cette commande:

mvn clean package liberty:run

En conséquence, Maven téléchargera le serveur, construira, déploiera et exécutera l’application.

7. Conclusion

Dans cet article, nous avons abordé la configuration et la mise en œuvre des principales fonctionnalités de la nouvelle API de sécurité Java EE 8.

Tout d'abord, nous avons commencé par montrer comment configurer les mécanismes d'authentification intégrés par défaut et comment en implémenter un personnalisé. Nous avons ensuite vu comment configurer le magasin d’identités intégré et comment en implémenter un personnalisé. Et enfin, nous avons vu comment appeler les méthodes desSecurityContext.

Comme toujours, les exemples de code pour cet article sont disponiblesover on GitHub.