Die Java EE Security API ist da!

Dies ist der dritte Tag des Java EE-Adventskalenders. Gestern war HASUNUMA Kenjis Einführung in JCA und MDB (ja) (http://www.coppermine.jp/docs/notepad/2017/12/introduction-to-jca-and-mdb.html) war.

Einführung

Nun, Java EE 8 wurde dieses Jahr endlich veröffentlicht! Ist CDI 2.0 oder Servlet 4 das Highlight? Ich persönlich interessiere mich für diesen Bereich, weil jemand ihn erklären wird Java EE Security API (JSR 375) -spec.html) Ich würde gerne darüber sprechen.

Die diesmal verwendete Probe kann aus dem Folgenden erhalten werden. https://github.com/koduki/example-javaee8-security_basic

Was ist das überhaupt?

Die Java EE-Sicherheits-API (JSR 375) ist, wie der Name schon sagt, eine Spezifikation, die die Sicherheit, insbesondere die Authentifizierung und Autorisierung, einfacher und portabler macht. Die meisten Webanwendungen verfügen über Anmeldefunktionen und Kontozugriffskontrolle. Es ist egal, wer die Funktionen hier macht, also möchte ich, dass FW das macht, oder? Mit Rails bietet Devise eine solche Funktion, und PHPs Symfony wurde als Standardfunktion von FW integriert.

Obwohl es in JavaEE seit einiger Zeit JACC-, JASPIC- und herstellerspezifische Implementierungen gibt, sind sie kompliziert und es gibt nur wenige Dokumente mit eigenen Spezifikationen. In Wirklichkeit haben die meisten Benutzer ihre eigene Authentifizierung / Autorisierung implementiert. Ich vermute, das gibt es nicht. Da die Sicherheit unabhängig von verschiedenen Komponenten wie Servlets, JSF, CDI, EJB usw. implementiert wurde, gab es keine Spezifikation für deren integrierte Verwaltung.

Unter Berücksichtigung dieser Umstände wurde JSR375 unter Bezugnahme auf OSS-Bibliotheken wie Apache Shiro mit den Schlüsselwörtern "Unified Management", "Simple" und "Portable" erstellt. Es ist eine Spezifikation.

Was kann ich tun?

Die Hauptpunkte sind die folgenden drei Punkte.

Einheitlicher Zugriff auf jede Komponente

Zunächst wurde ein Sicherheitskontext eingeführt, um die zuvor jeweils unabhängigen Authentifizierungsfunktionen zu vereinheitlichen. Bisher gab es beispielsweise die folgenden Methoden, um für jede Komponente ein Konto zu erstellen.

Der Zugriff auf diese ist im SecurityContext zusammengefasst. Die vorhandene Methode I / F jedes Prozesses bleibt gleich, aber das Backend usw. scheint sich geändert zu haben. Außerdem ist Java EE 8 vollständig in CDI integriert, sodass es beispielsweise auch dann ein Serve Red ist.

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

Der Teil, der den angemeldeten Benutzer als bestätigt hat

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

Und kann auch über SecurityContext übernommen werden.

Zugriffskontrolle durch Annotation

Die Zugriffskontrolle kann einfach durch Anmerkungen durchgeführt werden. Beispielsweise kann ein Servlet wie folgt erstellt werden, auf das nur Benutzer mit der Rolle foo zugreifen können.

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

Wenn der später beschriebene Authentifizierungsmechanismus entsprechend eingestellt ist, können Sie zur Anmeldeseite springen, indem Sie einfach die obigen Einstellungen vornehmen, und Sie können den Ablauf nach der Authentifizierung oder wenn ein Fehler auftritt, einfach definieren.

Einheitlicher Zugriff auf die Authentifizierungsfunktion

Der dritte Punkt ist der einheitliche Zugriff auf die Authentifizierungsfunktion. Wir stellen dies in Form eines Identitätsspeichers zur Verfügung. Sie können DB-, LDAP- oder benutzerdefinierte Authentifizierungsfunktionen verwenden.

Neue Webanwendungen haben häufig RDB als Back-End, und Unternehmenssysteme verwenden häufig LDAP wie ActiveDirectory. Sie können diese beiden verwenden, indem Sie die Einstellungen mit Anmerkungen schreiben. Da EL-Ausdrücke für Anmerkungen verwendet werden können, kann nicht nur eine harte Codierung, sondern auch eine moderne Verwendung wie die Erfassung aus Einstellungsdateien und Umgebungsvariablen problemlos durchgeführt werden.

Darüber hinaus möchten Sie häufig Ihre eigene Authentifizierungs-API oder Standard-Authentifizierungs-APIs wie OAuth und SAML verwenden. Selbst in diesem Fall können Sie damit umgehen, indem Sie einen benutzerdefinierten Identitätsspeicher oder Authentifizierungsmechanismus erstellen. Der Artikel "OpenID Connect mit Java EE 8-Sicherheits-API" Es gibt auch ein Beispiel für die Unterstützung von OpenID Connect, das mit relativ komplizierten Übergängen authentifiziert wird. Es scheint, dass die interne API normalerweise unterstützt werden kann, indem auf diese verwiesen wird. Wie im Artikel erwähnt, möchte ich Unterstützung für OAutht und allgemeine Authentifizierungsfunktionen als Standard oder in der Nähe davon.

Komponente

Obwohl die Erklärung ein wenig dupliziert ist, werden die folgenden drei Komponenten hauptsächlich direkt verwendet.

Authentication Mechanism

Der Authentifizierungsmechanismus steuert die Authentifizierung und den nachfolgenden Kontrollfluss. Es scheint, dass die BASIC-Authentifizierung mit @BasicAuthenticationMechanismDefinition durchgeführt wird, die hauptsächlich bereitgestellt wird, oder dass HttpAuthenticationMechanism vererbt und benutzerdefinierte Formulare verwendet werden. Durch die Kombination von HttpAuthenticationMechanism und @LoginToContinue usw. kann die allgemeine Anmeldesteuerung schnell erstellt werden.

Das einfache interne System und die API sind die BASIC-Authentifizierung. Wenn Sie einen Anmeldebildschirm benötigen, können Sie ein benutzerdefiniertes Formular verwenden.

Identity Store

Der Identity Store bietet die Möglichkeit zur Authentifizierung.

Wenn Sie beispielsweise möchten, dass das Backend eine RDB ist, können Sie mit @DatabaseIdentityStoreDefinition Folgendes definieren:

@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
)

Da Sie auch einen EL-Ausdruck in die Annotation schreiben können, können Sie auch eine Einstellungsdatei oder eine Klasse erstellen, die von außen gelesen werden soll, und den Einstellungswert daraus abrufen, wie in diesem Beispiel.

SecurityContext

SecurityContext ist ein Mechanismus für den einheitlichen Zugriff auf die beiden oben genannten Funktionen. Wie oben erwähnt, sind die Zugriffsmethoden auf den Authentifizierungsmechanismus vielfältig und kompliziert. SecurityContext macht es einfach:

--getCallerPrincipal: Ruft den Anrufer (Benutzer) ab. Get als String mit getName --isCallerInRole: Überprüfen Sie, ob die an das Argument übergebene Rolle angemeldet ist

Implementierungsbeispiel

Lassen Sie uns ein supereinfaches Implementierungsbeispiel zeigen. Diesmal mit BASIC-Zertifizierung.

Installieren Sie zuerst GlassFish 5 und arbeiten Sie mit NetBeans. Wenn Sie ein Maven-Projekt mit NetBeans 8.2 erstellen, handelt es sich um EE7. Ändern Sie es also in EE8. Java ist übrigens auch in 1.8 angegeben.

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

Lassen Sie zuerst ein Ziel rot dienen. Ich werde die Beschreibung weglassen, aber auch beans.xml und ApplicationConfig.java leer erstellen

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

}

Beim Zugriff sieht das Ergebnis wie folgt aus.

$ 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

BASIC-Zertifizierung

Machen Sie diese Seite zunächst nur für Administratoren zugänglich. Geben Sie @ServletSecurity für das Serve Red an, um die Rolle einzuschränken.

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

Wenn Sie erneut versuchen, darauf zuzugreifen, können Sie feststellen, dass ein 401-Fehler aufgetreten ist.

$ 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

Fügen Sie die BASIC-Authentifizierung hinzu, da Sie nicht wie bisher darauf zugreifen können. Fügen Sie ApplicationConfig.java @BasicAuthenticationMechanismDefinition hinzu.

@BasicAuthenticationMechanismDefinition(
        realmName = "test realm"
)

@ApplicationScoped
@Named
public class ApplicationConfig {

}

Wir werden auch einen Identitätsspeicher erstellen, der das Back-End für die Definition des grundlegenden Authentifizierungsmechanismus darstellt.

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

Das Passwort ist auch eine fest codierte, ultra-einfache Spezifikation. Ein gutes Mädchen sollte in der Produktion nicht nachahmen, oder? Lassen Sie uns dies bereitstellen und ausführen.

$ 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

Sie können sehen, dass das Ergebnis ordnungsgemäß zurückgegeben wird, ohne 401 zu werden. Sie können das Ergebnis von securityContext richtig sehen.

Nutzung der Datenbank

Hardcodierung ist nicht so viel, also ändern wir das Backend auf RDB. Zunächst benötigen Sie eine Datenbank. Wenn Sie GlassFish5 verwenden, wird Derby mitgeliefert. Verwenden Sie das also. Initialisieren Sie die Datenbank mit dem folgenden Code, um eine Datenquelle zu erstellen.

Ursprünglich sollten Sie ein Verwaltungstool erstellen, aber dieses Mal wird das Kennwort auch gehasht und gespeichert. Ich habe auch das Passwort gegenüber dem vorherigen geändert, um die Überprüfung zu erleichtern.

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

}

Löschen Sie als Nächstes den zuvor erstellten TestIdentityStore und fügen Sie stattdessen @DatabaseIdentityStoreDefinition zu ApplicationConfig.java hinzu.

@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"};
    }
}

Die verwendete Datenquelle ist die zuvor in DatabaseSetup.java erstellte. Der Kennwort-Hash-Algorithmus verwendet auch PBKDF2, sodass das empfangene Kennwort denselben Hashwert hat wie das in der Datenbank registrierte.

Ich werde es versuchen.

$ 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

Haben Sie sich mit dem neuen Passwort authentifiziert, das in der Datenbank registriert ist? Da das Back-End des Authentifizierungsmechanismus auf diese Weise getrennt wird, kann die Implementierung leicht geändert werden.

Zusammenfassung

Es gibt viele Orte, die ich noch nicht untersucht habe, aber ich habe versucht, Java EE Security API (JSR 375) zu berühren. Es war. Wenn es sich um eine einfache API-Authentifizierungsfunktion handelt, ist dies praktisch, da anscheinend auch heutige Inhalte schnell erstellt werden können.

In Zukunft möchte ich mehr praktische Teile wie LDAP-Verknüpfung und benutzerdefinierte Formularverknüpfung ausprobieren, um einen Artikel zu erstellen.

Dann viel Spaß beim Hacken!

Referenz

Recommended Posts

Die Java EE Security API ist da!
5. Tag von Java
[java8] Um die Stream-API zu verstehen
Wo ist die Zeitzone von Javas LocalDateTime.now ()?
Analysieren der COTOHA-API-Syntaxanalyse in Java
Versuchen Sie es mit der Stream-API in Java
Rufen Sie die Windows-Benachrichtigungs-API in Java auf
Rufen Sie die Salesforce REST-API von Java aus auf
Was ist das beste Lesen von Dateien (Java)
Was ist die Hauptmethode in Java?
Was ist java
ChatWork4j für die Verwendung der ChatWork-API in Java
Was ist das Java Servlet / JSP MVC-Modell?
Die Reihenfolge der Java-Methodenmodifikatoren ist festgelegt
Der in Java 10 eingeführte Schnittpunkttyp ist erstaunlich (?)
Java Stream API
Was ist Java <>?
Was ist der flüchtige Modifikator für Java-Variablen?
Was ist java
Versuchen Sie es mit der Syntaxanalyse der COTOHA-API in Java
[Java] In der Ausgabe wird etwas als "-0.0" angezeigt
[Java] Das Referenzwort ist schlecht!
Der Vergleich von enum ist == und gleich ist gut [Java]
Verwenden Sie Java-Lambda-Ausdrücke außerhalb der Stream-API
Was ist besser, Kotlin oder zukünftiges Java?
[JAVA] Was ist der Unterschied zwischen Schnittstelle und Zusammenfassung? ?? ??
Die Geschichte, dass .java auch in Unity 2018 erstellt wurde
Packen Sie die API-Antwort (Java)
[Java] Stream API / Map
Fehlerbehebung bei der Docker-Client Java API
Was ist Java-Kapselung?
Zabbix API in Java
Was ist Java-Technologie?
Was ist Java API-Java?
[Java] Was ist flatMap?
[Java] Was ist ArrayList?
Was ist die LocalDateTime-Klasse? [Java-Anfänger] -Datum und Zeitklasse-
[Java] "T" ist in der API-Antwort im Datumstyp JSON enthalten
Ich möchte die Java 8 DateTime-API (jetzt) langsam verwenden.
[Java] [Play Framework] Bis das Projekt mit Gradle gestartet wird
[Java] Beachten Sie, dass der Fall, in dem gleich ist, falsch ist, aber == ist.
[Java] com.sun.glass.WindowEvent wird importiert und das Fenster wird nicht geschlossen
Java: Das Problem ist schneller, Stream oder Loop
Jetzt ist es an der Zeit, mit der Stream-API zu beginnen
So spielen Sie eine MIDI-Datei mit der Java Sound API ab