L'API de sécurité Java EE est là!

C'est le troisième jour du calendrier de l'avent Java EE. Hier, c'était [[Présentation] Introduction à JCA et MDB (ja)] de HASUNUMA Kenji (http://www.coppermine.jp/docs/notepad/2017/12/introduction-to-jca-and-mdb.html) était.

introduction

Eh bien, Java EE 8 est enfin sorti cette année! CDI 2.0 ou Servlet 4 est-il le point culminant? Je suis personnellement intéressé par ce domaine car quelqu'un vous l'expliquera API de sécurité Java EE (JSR 375) -spec.html) Je voudrais parler.

L'échantillon utilisé cette fois peut être obtenu à partir de ce qui suit. https://github.com/koduki/example-javaee8-security_basic

Qu'est-ce que c'est en premier lieu?

L'API de sécurité Java EE (JSR 375), comme son nom l'indique, est une spécification qui rend la sécurité, en particulier l'authentification et l'autorisation, plus simple et plus portable. La plupart des applications Web ont des capacités de connexion et un contrôle d'accès au compte. Peu importe qui fait les fonctions ici, alors je veux que FW le fasse, non? Avec Rails, Devise fournit une telle fonction, et Symfony de PHP a été incorporé en tant que fonction standard de FW.

Cependant, bien que les implémentations JACC, JASPIC et spécifiques au fournisseur existent dans JavaEE depuis longtemps, elles étaient compliquées et il y avait peu de documents en raison de leurs propres spécifications, donc la réalité est que la plupart des gens ont implémenté leur propre authentification / autorisation. Je soupçonne que non. De plus, comme la sécurité était implémentée indépendamment par divers composants tels que les servlets, JSF, CDI, EJB, etc., il n'y avait pas de spécification pour leur gestion intégrée.

Compte tenu de ces circonstances, JSR375 a été créé en référence à des bibliothèques OSS telles que Apache Shiro avec les mots-clés «gestion unifiée», «simple» et «portable». C'est une spécification.

Que puis-je faire?

Les principaux points sont les trois points suivants.

Accès unifié à chaque composant

Tout d'abord, un contexte de sécurité a été introduit pour unifier les fonctions d'authentification qui étaient auparavant indépendantes pour chacune. Par exemple, jusqu'à présent, il existait les méthodes suivantes pour acquérir un compte pour chaque composant.

L'accès à ces derniers est résumé dans le SecurityContext. La méthode I / F existante de chaque processus reste la même, mais le backend etc. semble avoir changé. De plus, Java EE 8 est totalement intégré à CDI, donc même s'il s'agit d'un serveur rouge, par exemple.

public class MyServlet extends HttpServlet {
    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String webName = request.getUserPrincipal().getName();

La partie qui a confirmé l'utilisateur connecté comme

public class MyServlet extends HttpServlet {
    @Inject
    private SecurityContext securityContext;

    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String webName = securityContext.getCallerPrincipal().getName();

Et peut également être pris via SecurityContext.

Contrôle d'accès par annotation

Le contrôle d'accès peut être facilement effectué par des annotations. Par exemple, un servlet auquel seuls les utilisateurs avec le rôle foo peuvent accéder peut être créé comme suit.

@ServletSecurity(@HttpConstraint(rolesAllowed = "foo"))
public class Servlet extends HttpServlet {

Si le mécanisme d'authentification décrit plus loin est défini de manière appropriée, vous pouvez accéder à la page de connexion en définissant simplement ce qui précède, et vous pouvez facilement définir le flux après l'authentification ou lorsqu'une erreur se produit.

Accès unifié à la fonction d'authentification

Le troisième point est l'accès unifié à la fonction d'authentification. Nous fournissons cela sous la forme d'un magasin d'identité. Vous pouvez utiliser la fonction d'authentification DB, LDAP ou personnalisée.

Les nouvelles applications Web auront souvent RDB comme back-end, et les systèmes d'entreprise utiliseront souvent LDAP tel qu'ActiveDirectory. Vous pouvez utiliser ces deux simplement en écrivant les paramètres avec des annotations. Étant donné que les expressions EL peuvent être utilisées pour les annotations, non seulement le codage en dur, mais également les utilisations modernes telles que l'acquisition à partir de fichiers de configuration et de variables d'environnement peuvent être effectuées sans problème.

En outre, vous souhaiterez souvent utiliser votre propre API d'authentification ou des API d'authentification standard telles que OAuth et SAML. Même dans ce cas, vous pouvez le gérer en créant un magasin d'identité ou un mécanisme d'authentification personnalisé. En fait, l'article "OpenID Connect with Java EE 8 Security API" Il existe également un exemple de prise en charge d'OpenID Connect, qui est authentifié avec des transitions relativement compliquées. Il semble que l'API interne puisse généralement être prise en charge en se référant à cela. Comme mentionné dans l'article, je souhaite une prise en charge d'OAutht et des fonctions générales d'authentification en standard ou à proximité.

Composant

Bien que l'explication soit un peu dupliquée, les trois composants suivants sont principalement utilisés directement.

Authentication Mechanism

Le mécanisme d'authentification contrôle l'authentification et le flux de contrôle ultérieur. Il semble que l'authentification BASIC sera effectuée avec @BasicAuthenticationMechanismDefinition, qui est principalement fourni, ou HttpAuthenticationMechanism sera hérité et des formulaires personnalisés seront utilisés. En combinant HttpAuthenticationMechanism et @LoginToContinue etc., je pense que le contrôle de connexion général peut être créé rapidement.

Le système interne simple et l'API sont une authentification BASIC, et si vous avez besoin d'un écran de connexion, vous pouvez utiliser un formulaire personnalisé.

Identity Store

Le magasin d'identité offre la possibilité de s'authentifier.

Par exemple, si vous souhaitez que le backend soit un RDB, vous pouvez utiliser @DatabaseIdentityStoreDefinition pour définir:

@DatabaseIdentityStoreDefinition(
        dataSourceLookup = "${Config.getDataSourceName()}", // "java:global/MyAppDataSource"
        callerQuery = "select password from caller where name = ?",
        groupsQuery = "select group_name from caller_groups where caller_name = ?",
        hashAlgorithm = Pbkdf2PasswordHash.class,
        priorityExpression = "#{100}",
        hashAlgorithmParameters = {
            "Pbkdf2PasswordHash.Iterations=3072",
            "${applicationConfig.dyna}"
        } // just for test / example
)

Comme vous pouvez également écrire une expression EL dans l'annotation, vous pouvez également créer un fichier de paramètres ou une classe à lire de l'extérieur et en obtenir la valeur de paramètre comme dans cet exemple.

SecurityContext

SecurityContext est un mécanisme pour un accès unifié aux deux fonctions ci-dessus. Comme mentionné ci-dessus, les méthodes d'accès au mécanisme d'authentification sont diverses et compliquées. SecurityContext permet de:

--getCallerPrincipal: récupère l'appelant (utilisateur). Obtenir en tant que chaîne avec getName --isCallerInRole: vérifie si le rôle passé à l'argument en a un connecté

Exemple d'implémentation

Montrons un exemple d'implémentation super simple. Cette fois avec la certification BASIC.

Tout d'abord, installez GlassFish 5 et travaillez avec NetBeans. Si vous créez un projet Maven avec NetBeans 8.2, ce sera EE7, modifions-le donc en EE8. À propos, Java est également spécifié dans la version 1.8.

@@ -18,7 +18,7 @@
         <dependency>
             <groupId>javax</groupId>
             <artifactId>javaee-web-api</artifactId>
-            <version>7.0</version>
+            <version>8.0</version>
             <scope>provided</scope>
         </dependency>
     </dependencies>
@@ -30,8 +30,8 @@
                 <artifactId>maven-compiler-plugin</artifactId>
                 <version>3.1</version>
                 <configuration>
-                    <source>1.7</source>
-                    <target>1.7</target>
+                    <source>1.8</source>
+                    <target>1.8</target>
                     <compilerArguments>
                         <endorseddirs>${endorsed.dir}</endorseddirs>
                     </compilerArguments>

Tout d'abord, faites servir une cible en rouge. Je vais omettre la description, mais aussi créer des beans.xml et ApplicationConfig.java vides

@WebServlet("/myservlet")
public class MyServlet extends HttpServlet {

    private static final long serialVersionUID = 1L;
    @Inject
    private SecurityContext securityContext;

    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        response.getWriter().write("This is a servlet \n");

        String webName = null;
        if (securityContext.getCallerPrincipal() != null) {
            webName = securityContext.getCallerPrincipal().getName();
        }
        response.getWriter().write("web username: " + webName + "\n");

        response.getWriter().write("web user has role \"admin\": " + securityContext.isCallerInRole("admin") + "\n");
        response.getWriter().write("web user has role \"users\": " + securityContext.isCallerInRole("users") + "\n");
        response.getWriter().write("web user has role \"guest\": " + securityContext.isCallerInRole("guest") + "\n");
    }

}

Lors de l'accès, le résultat sera le suivant.

$ curl -L http://localhost:8080/example-javaee8-security_basic/myservlet
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   137  100   137    0     0   9133      0 --:--:-- --:--:-- --:--:--  9133
This is a servlet
web username: null
web user has role "admin": false
web user has role "users": false
web user has role "guest": false

Certification BASIC

Tout d'abord, rendez cette page accessible uniquement à l'administrateur. Spécifiez @ServletSecurity pour le serveur rouge afin de limiter le rôle.

@WebServlet("/myservlet")
@ServletSecurity(@HttpConstraint(rolesAllowed = "admin"))
public class MyServlet extends HttpServlet {
.
.
.

Si vous essayez à nouveau d'y accéder, vous pouvez voir que cela a entraîné une erreur 401.

$ curl -IL http://localhost:8080/example-javaee8-security_basic/myservlet
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0  1090    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
HTTP/1.1 401 Unauthorized
Server: GlassFish Server Open Source Edition  5.0

Puisque vous ne pouvez pas y accéder tel quel, ajoutez une authentification BASIC. Ajoutez @BasicAuthenticationMechanismDefinition à ApplicationConfig.java.

@BasicAuthenticationMechanismDefinition(
        realmName = "test realm"
)

@ApplicationScoped
@Named
public class ApplicationConfig {

}

Nous allons également créer un magasin d'identités qui sera le back-end pour la définition du mécanisme d'authentification de base.

@ApplicationScoped
public class TestIdentityStore implements IdentityStore {
    public CredentialValidationResult validate(UsernamePasswordCredential usernamePasswordCredential) {
        if (usernamePasswordCredential.compareTo("admin", "password")) {
            return new CredentialValidationResult("admin", new HashSet<>(asList("admin", "users")));
        }
        return INVALID_RESULT;
    }
}

Le mot de passe est également une spécification ultra-simple codée en dur. Une bonne fille ne devrait pas imiter dans la production, non? Déployons et exécutons ceci.

$ curl -LI -u admin:password http://localhost:8080/example-javaee8-security_basic/myservlet
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0   137    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
HTTP/1.1 200 OK
Server: GlassFish Server Open Source Edition  5.0

$ curl -u admin:password http://localhost:8080/example-javaee8-security_basic/myservlet
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   137  100   137    0     0   4419      0 --:--:-- --:--:-- --:--:--  8562
This is a servlet
web username: admin
web user has role "admin": true
web user has role "users": false
web user has role "guest": false

Vous pouvez voir que le résultat est renvoyé correctement sans devenir 401. Vous pouvez voir correctement le résultat de securityContext.

Utilisation de la base de données

Le codage en dur n'est pas tellement, alors changeons le backend en RDB. Tout d'abord, vous avez besoin d'une base de données à utiliser. Si vous utilisez GlassFish5, Derby sera inclus, alors utilisez-le. Initialisez la base de données avec le code suivant pour créer une source de données.

À l'origine, vous devriez créer un outil de gestion, mais cette fois, le mot de passe est également haché et stocké. J'ai également changé le mot de passe du précédent pour le rendre plus facile à vérifier.

@DataSourceDefinition(
        name = "java:global/MyAppDataSource",
        minPoolSize = 0,
        initialPoolSize = 0,
        className = "org.apache.derby.jdbc.ClientDataSource",
        user = "APP",
        password = "APP",
        databaseName = "myapp",
        properties = {"connectionAttributes=;create=true"}
)
@Singleton
@Startup
public class DatabaseSetup {

    @Resource(lookup = "java:global/MyAppDataSource")
    private DataSource dataSource;

    @Inject
    private Pbkdf2PasswordHash passwordHash;

    @PostConstruct
    public void init() {

        Map<String, String> parameters = new HashMap<>();
        parameters.put("Pbkdf2PasswordHash.Iterations", "3072");
        parameters.put("Pbkdf2PasswordHash.Algorithm", "PBKDF2WithHmacSHA512");
        parameters.put("Pbkdf2PasswordHash.SaltSizeBytes", "64");
        passwordHash.initialize(parameters);

//        executeUpdate(dataSource, "DROP TABLE caller");
//        executeUpdate(dataSource, "DROP TABLE caller_groups");
        executeUpdate(dataSource, "CREATE TABLE caller(name VARCHAR(64) PRIMARY KEY, password VARCHAR(255))");
        executeUpdate(dataSource, "CREATE TABLE  caller_groups(caller_name VARCHAR(64), group_name VARCHAR(64))");

        executeUpdate(dataSource, "INSERT INTO caller VALUES('admin', '" + passwordHash.generate("secret1".toCharArray()) + "')");

        executeUpdate(dataSource, "INSERT INTO caller_groups VALUES('admin', 'admin')");
    }

    @PreDestroy
    public void destroy() {
        try {
            executeUpdate(dataSource, "DROP TABLE caller");
            executeUpdate(dataSource, "DROP TABLE caller_groups");
        } catch (Exception ex) {
            ex.printStackTrace();
            // silently ignore, concerns in-memory database
        }
    }

    private void executeUpdate(DataSource dataSource, String query) {
        try (Connection connection = dataSource.getConnection()) {
            try (PreparedStatement statement = connection.prepareStatement(query)) {
                statement.executeUpdate();
            }
        } catch (SQLException ex) {
            throw new IllegalStateException(ex);
        }
    }

}

Ensuite, supprimez le TestIdentityStore créé précédemment et ajoutez à la place @DatabaseIdentityStoreDefinition à ApplicationConfig.java.

@BasicAuthenticationMechanismDefinition(
        realmName = "test realm"
)
@DatabaseIdentityStoreDefinition(
        dataSourceLookup = "java:global/MyAppDataSource",
        callerQuery = "select password from caller where name = ?",
        groupsQuery = "select group_name from caller_groups where caller_name = ?",
        hashAlgorithm = Pbkdf2PasswordHash.class,
        priorityExpression = "#{100}",
        hashAlgorithmParameters = {
            "Pbkdf2PasswordHash.Iterations=3072",
            "${applicationConfig.dyna}"
        } // just for test / example
)
@ApplicationScoped
@Named
public class ApplicationConfig {

    public String[] getDyna() {
        return new String[]{"Pbkdf2PasswordHash.Algorithm=PBKDF2WithHmacSHA512", "Pbkdf2PasswordHash.SaltSizeBytes=64"};
    }
}

La source de données utilisée est celle créée dans DatabaseSetup.java précédemment. L'algorithme de hachage de mot de passe utilise également PBKDF2 afin que le mot de passe reçu ait la même valeur de hachage que celle enregistrée dans la base de données.

J'essaierai ceci.

$ curl -u admin:secret1 http://localhost:8080/example-javaee8-security_basic/myservlet
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   137  100   137    0     0   4419      0 --:--:-- --:--:-- --:--:--  4419
This is a servlet
web username: admin
web user has role "admin": true
web user has role "users": false
web user has role "guest": false

Vous êtes-vous authentifié avec le nouveau mot de passe enregistré dans la base de données? Puisque le back-end du mécanisme d'authentification est séparé de cette manière, l'implémentation peut être facilement modifiée.

Résumé

Il y a beaucoup d'endroits que je n'ai pas encore étudiés, mais j'ai essayé de toucher API de sécurité Java EE (JSR 375). C'était. S'il s'agit d'une simple fonction d'authentification API, c'est pratique car il semble que même le contenu d'aujourd'hui puisse être créé rapidement.

À l'avenir, j'aimerais essayer des parties plus pratiques telles que le lien LDAP et le lien de formulaire personnalisé pour créer un article.

Alors bon piratage!

référence

Recommended Posts

L'API de sécurité Java EE est là!
5ème jour de Java
[java8] Pour comprendre l'API Stream
Où est le fuseau horaire de Java LocalDateTime.now ()?
Analyser l'analyse syntaxique de l'API COTOHA en Java
Essayez d'utiliser l'API Stream en Java
Appelez l'API de notification Windows en Java
Accédez à l'API REST Salesforce depuis Java
Quelle est la meilleure lecture de fichier (Java)
Quelle est la méthode principale en Java?
Qu'est-ce que 'java
ChatWork4j pour l'utilisation de l'API ChatWork en Java
Qu'est-ce que le modèle Java Servlet / JSP MVC?
L'ordre des modificateurs de méthode Java est fixe
Le type d'intersection introduit dans Java 10 est incroyable (?)
API Java Stream
Qu'est-ce que Java <>?
Quel est le modificateur volatile pour les variables Java?
Qu'est-ce que 'java
Essayez d'utiliser l'analyse syntaxique de l'API COTOHA en Java
[Java] Quelque chose est affiché comme "-0.0" dans la sortie
[Java] Le mot passant par référence est mauvais!
La comparaison d'énumération est ==, et equals est bonne [Java]
Utiliser des expressions Java lambda en dehors de l'API Stream
Quel est le meilleur, Kotlin ou futur Java?
[JAVA] Quelle est la différence entre interface et abstract? ?? ??
L'histoire que .java est également construite dans Unity 2018
Emballez la réponse de l'API (java)
[Java] API / carte de flux
Dépannage de l'API Java Docker-Client
Qu'est-ce que l'encapsulation Java?
Pratique de l'API Java8 Stream
API Zabbix en Java
Qu'est-ce que la technologie Java?
Qu'est-ce que Java API-java
[Java] Qu'est-ce que flatMap?
[Java] Qu'est-ce que ArrayList?
Qu'est-ce que la classe LocalDateTime? [Java débutant] -Date et classe d'heure-
[Java] "T" est inclus dans le type de date JSON dans la réponse de l'API
Je souhaite utiliser l'API Java 8 DateTime lentement (maintenant)
[Java] [Play Framework] Jusqu'à ce que le projet soit démarré avec Gradle
[Java] Notez le cas où égal est faux mais == est ture.
[Java] com.sun.glass.WindowEvent est importé et la fenêtre ne se ferme pas
Java: dont le problème est plus rapide, en flux ou en boucle
Il est maintenant temps de commencer avec l'API Stream
Comment lire un fichier MIDI à l'aide de l'API Java Sound