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.
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
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.
Die Hauptpunkte sind die folgenden drei Punkte.
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.
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.
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.
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
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
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.
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.
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!
Recommended Posts