Die Authentifizierung von Benutzern ist ein wichtiger Bestandteil einer Anwendung. Auch die Beschränkung des Zugriffs auf Ressourcen durch Autorisierung. Spring Security ist eine Referenz in der Webumgebung. Es ist jedoch an die Spring-Technologie gebunden und die Größe der Bibliothek --- mehr als 10 JAR an Abhängigkeiten --- kann seine Verwendung einschränken. Außerdem kann die fehlende Integration mit Guice oder die wiederholte Bereitstellung einer App Engine-Anwendung sie ausschließen. Dies ist die Gelegenheit, sich Apache Shiro genauer anzusehen.
- Einführung in die HTTP-Authentifizierung
- Shiro Servlet-Filter
- Eine Ressource sichern
- Integration testen
- Realm und Matcher für die Authentifizierung
- Ein leistungsstarkes Genehmigungsmodell
- Mit Kommentaren autorisieren
- Shiro, ein echter Herausforderer
Authentifizierung ist der Prozess der Identitätsüberprüfung. Die Identität wird in der Regel durch login:password dargestellt.
Autorisierung ist der Prozess der Festlegung von Zugriffsrechten auf Ressourcen. Die Rechte sind in der Regel mit den Gruppen des authentifizierten Benutzers verknüpft
Einführung in die HTTP-Authentifizierung
JAAS --- Java Authentication and Authorization Service --- war eines der ersten Frameworks, das Java um Sicherheit erweitert hat. Sein Datenmodell ist heute weit verbreitet: ein Subjekt --- der Benutzer --- wird authentifiziert, wenn seine Principals --- seine Identität --- und seine Credentials --- der Nachweis seiner Identität --- mit der Authentifizierungsreferenz übereinstimmen; dann wird er zu mehreren Rollen hinzugefügt, die Berechtigungen implizieren (Operationen werden durch Berechtigungen eingeschränkt). Die HTTP-Authentifizierung wurde für Webumgebungen gewählt, ihre Unterstützung durch JAX-RS-Implementierungen und moderne Browser (mit einem Eingabefenster) machte sie ebenfalls populär. JAX-RS verwendet JAAS (über einen SecurityContext), aber seine Bindung an Java-Richtlinien und seine mangelnde Modularität machten das Sicherheits-Framework beliebter. Mit der Zeit können Sicherheitsmaßnahmen veralten, und angesichts der geringen Beiträge zur HTTP-Authentifizierung kamen andere Protokolle auf. Für OAuth, das von beliebten Websites häufig verwendet wird, gibt es Bibliotheken, Scribe und SocialAuth(die Implementierung von Shiro ist in Arbeit). Der Einfachheit halber befasst sich dieser Artikel nur mit den beiden HTTP-Authentifizierungen und ihrer Integration in Shiro. Die Basisauthentifizierung, die weniger sicher ist --- sie beinhaltet das Passwort ---, folgt diesen Schritten:- Der Server erhält eine Anfrage nach einer gesicherten Ressource;
- Der Server sendet einen Statuscode 401 und setzt die Kopfzeile WWW-Authenticate: Basic;
- Der Client wiederholt seine Anfrage mit der Kopfzeile Authorization: Basic login:password kodiert login:password in base64;
- Der Server dekodiert base64 und gibt den Zugriff auf die Ressource frei oder kehrt zu Schritt 2 zurück.
- Der Server erhält eine Anfrage nach einer gesicherten Ressource;
- Der Server sendet einen Statuscode 401 und setzt den Header WWW-Authenticate: Digest, den Header realm mit dem Anwendungsnamen und Datum und hasht den Header nonce, um das Authentifizierungsangebot zeitlich zu begrenzen;
- Der Client wiederholt seine Anfrage mit den Headern Authorization: Digest username="...", nonce="...", response="...";
- Der Server vergleicht die Berechnung md5(md5(login:realm:password):nonce:md5(httpMethod:uri)) mit der Header-Antwort und gibt den Zugriff auf die Ressource frei oder kehrt zu Schritt 2 zurück (das Kennwort wird nicht vom Client angegeben, der Server holt es sich aus seiner Referenz über das Login).
Shiro Servlet-Filter
Shiro --- ehemals JSecurity --- ist ein robustes Framework, das Authentifizierung, Autorisierung, Kryptographie und Sitzungsverwaltung bietet. Es passt gut in eine Web-Umgebung mit einem Servlet-Filter (eine weitere Abhängigkeit ist erforderlich). Die folgende web.xml zeigt, wie Sie eine Ressource sichern können: [xml] <?xml version="1.0" encoding="utf-8"?> <Web-Applikation> <filtern> <filter-name>Shiro</filter-name> <Filter-Klasse> org.apache.shiro.web.servlet.IniShiroFilter </filter-class> </filter> <filter-mapping> <filter-name>Shiro</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <Servlet> <servlet-name>Jersey</servlet-name> <Servlet-Klasse> com.sun.jersey.spi.container.servlet.ServletContainer </servlet-class> </servlet> <servlet-mapping> <servlet-name>Jersey</servlet-name> <url-pattern>/*</url-pattern> </servlet-mapping> </web-app> [/xml] Die obige Konfiguration verwendet Jersey --- JAX-RS Standardimplementierung --- zur Deklaration von Ressourcen und Shiro zum Abfangen aller Aufrufe. Um anzugeben, welche Aufrufe autorisiert/verboten sind, muss eine shiro.ini-Datei im Stammverzeichnis des Klassenpfads erstellt werden (dieses Verzeichnis kann in der Datei web.xml konfiguriert werden, sein Inhalt kann auch darin abgelegt werden; mehr dazu). Die Markierung main definiert die Klassen für die Filterlogik. Die urls-Markierung definiert Adressen und Filter mit feinerer Körnung. [java] [main] authcBasicRealm = com.xebia.shiro.StaticRealm matcher = com.xebia.shiro.ReverseCredentialsMatcher authcBasicRealm.credentialsMatcher = $matcher [urls] /** = authcBasic [/java] Der authcBasic-Filter ist Teil der standardmäßig bereitgestellten Filter. Er ist mit dem JavaBean-Stil im Hauptmarker konfigurierbar(Shiro verwendet dafür Apache BeanUtils --- Primitive können direkt angegeben werden, komplexe Typen müssen mit $ deklariert werden). Auf diese Weise konfiguriert, ermöglicht es die Authentifizierung jedes Aufrufs des Servers mit dem in den HTTP-Headern gefundenen login:password (in Basic). Das resultierende Token wird an ein Realm/Matcher-Paar übermittelt. Der erste greift auf die Referenz zu und gibt das wahre Passwort an den zweiten weiter, der überprüft, ob es mit dem angegebenen übereinstimmt. Klassische Realms --- JDBC, LDAP, etc --- werden bereitgestellt. Matcher können neu definiert werden, wenn Passwörter verschlüsselt gespeichert werden und mit der Verschlüsselung des bereitgestellten Passworts verglichen werden müssen(weitere Informationen). Wenn mehrere Realms verwendet werden, kann die Auflösungslogik im Rahmen einer Strategie festgelegt werden (alle, mindestens einer, usw.).Eine Ressource sichern
Bevor wir über Sicherheit sprechen, müssen wir eine Ressource erstellen und mit Integrationstests überprüfen, ob sie effektiv gesichert ist. Bei jedem Aufruf von /safe prüft diese Ressource, ob der Benutzer eine vip-Rolle hat, und gibt einen entsprechenden String zurück. Da Shiro mit einem Servlet-Filter konfiguriert ist, gelangt der Benutzer nicht zu dieser Methode, wenn er nicht korrekt authentifiziert wurde. [java] import org.apache.shiro.SecurityUtils; @Path("/safe") public class SafeResource { @GET public Response get() { String state; if (SecurityUtils.getSubject().hasRole("vip")) { state = "authorized"; } else { state = "authenticated"; } return Response.ok(state).build(); } } [/java] Die SafeResource-Klasse wird von Shiro gefiltert. Hier wird keine Authentifizierungsprüfung durchgeführt. Der Benutzer ist bereits authentifiziert, wenn er diese Ressourcenmethode erreicht. Bitte beachten Sie, dass im Gegensatz zu diesem Beispiel in der Shiro-Dokumentation die Bedeutung von expliziten Rollen hervorgehoben wird, d.h. die Autorisierung von Operationen durch Berechtigungen und nicht durch Rollen. Das bedeutet, dass die Rechte einer Rolle nicht mehr direkt mit ihr verknüpft sind, was die Wartung erleichtert: Wenn Rollen hinzugefügt, geändert oder gelöscht werden, kann dies geschehen, ohne dass der Code der Anwendung geändert werden muss; es reicht aus, die referenzielle Zuordnung von Rollen und Berechtigungen zu ändern. Hier finden Sie ein pom mit den Abhängigkeiten, die für das Beispiel in diesem Artikel benötigt werden. Versuchen Sie, diese auf dem neuesten Stand zu halten. [xml] <Abhängigkeiten> <Abhängigkeit> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <Version>1.1.0</version> </dependency> <Abhängigkeit> <groupId>org.apache.shiro</groupId> <artifactId>shiro-web</artifactId> <Version>1.1.0</version> </dependency> <Abhängigkeit> <groupId>com.sun.jersey</groupId> <artifactId>jersey-server</artifactId> <Version>1.6</version> </dependency> <Abhängigkeit> <groupId>com.sun.jersey</groupId> <artifactId>jersey-client</artifactId> <Version>1.6</version> </dependency> <Abhängigkeit> <groupId>org.mortbay.jetty</groupId> <artifactId>jetty-embedded</artifactId> <Version>6.1.26</version> </dependency> <Abhängigkeit> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <Version>1.1.1</version> </dependency> <Abhängigkeit> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <Version>r08</version> </dependency> </dependencies> [/xml]Integration testen
Lassen Sie uns die Filterung mit Unit-Tests anhand eines Jetty-Embedded-Servers überprüfen. Er ist mit dem Verzeichnis WEB-INF konfiguriert, das die web.xml enthält, in der die Ressourcen und der Shiro-Filter deklariert sind. [java] public class SafeResourceTest { private static final String WEB_INF_DIRECTORY = "war"; Server server; @Before public void before() throws Exception { server = new Server(8080); server.addHandler(new WebAppContext(WEB_INF_DIRECTORY, "/")); server.start(); } @After public void after() throws Exception { server.stop(); } } [/java] Es können drei Integrationstests durchgeführt werden:- Die erste prüft, ob ohne einen Wert in der HTTP-Kopfzeile ein 401-Fehler zurückgegeben wird;
- Der zweite prüft, ob die Authentifizierung mit einem korrekten Benutzer funktioniert;
- Der letzte prüft, ob die Autorisierung mit der richtigen Rolle funktioniert.
Realm und Matcher für die Authentifizierung
Nun, da die Tests erstellt sind, ist es an der Zeit, sich auf die Authentifizierungs- und Autorisierungslogik zu konzentrieren. Für die Zwecke dieses Artikels verwendet unser Realm, StaticRealm, statische Daten, die in einer statischen Klasse wie folgt definiert sind: [java] import com.google.common.collect.HashMultimap; import org.apache.shiro.crypto.hash.Sha256Hash; public class Safe { statische Karte<String, String> Passwörter = new HashMap<String, String>(); statische HashMultimap<String, String> roles = HashMultimap.create(); statisch{ passwörter.put("pierre", encrypt("green")); passwörter.put("paul", encrypt("blau")); roles.put("paul", "vip"); } private String encrypt(String password) { return new Sha256Hash(Passwort).toString(); } public static String getPassword(String username) { return passwords.get(username); } public static Set<Zeichenfolge> getRoles(String username) { return roles.get(username); } } [/java] Fürs Protokoll, ein Paar Realm/Matcher ist für die Authentifizierung zuständig (siehe die Konfigurationsdatei oben). Der erste greift auf die Referenz zu und gibt das echte Passwort an den zweiten weiter, der überprüft, ob es mit dem angegebenen übereinstimmt. Wenn die Authentifizierung fehlschlägt, wird eine AuthentificationException ausgelöst. [java] public class StaticRealm extends AuthorizingRealm { @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { UsernamePasswordToken upToken = (UsernamePasswordToken) token; String username = upToken.getUsername(); checkNotNull(username, "Null-Benutzernamen sind in diesem Realm nicht erlaubt."); String password = Safe.getPassword(username); checkNotNull(passwort, "Kein Konto gefunden für Benutzer [" + Benutzername + "]"); return new SimpleAuthenticationInfo(username, password.toCharArray(), getName()); } private void checkNotNull(Object reference, String message) { if (referenz == null) { throw new AuthenticationException(Nachricht); } } } [/java] Unter Verwendung der Klasse Safe als Referenz ruft dieser Realm den Benutzer ab, dessen Login als Parameter angegeben ist, und gibt sein Passwort an den Matcher weiter (diese Trennung der Anliegen in Shiro erleichtert die Modularität). Dieser Matcher prüft, ob das angegebene Kennwort das Gegenteil des echten Kennworts ist: [java] public class ReverseCredentialsMatcher extends SimpleCredentialsMatcher { @Override public boolean doCredentialsMatch(AuthenticationToken tok, AuthenticationInfo info) { String tokenCredentials = charArrayToString(tok.getCredentials()); String reverseToken = StringUtils.reverse(tokenCredentials); String encryptedToken = new Sha256Hash(reverseToken).toString(); String accountCredentials = charArrayToString(info.getCredentials()); return accountCredentials.equals(encryptedToken); } private String charArrayToString(Object credentials) { return new String((char[]) credentials); } } [/java] Das war's. Integrationstests sind grün. In einfachen Fällen, wie dem unseren, ist es besser, IniRealm (automatisch) zu verwenden, indem Sie in der Konfigurationsdatei Markierungen für Benutzer und Rollen hinzufügen. [java] [users] # user = password, roles... pierre = ba4788b226aa8dc2e6dc74248bb9f618cfa8c959e0c26c147be48f6839a0b088 paul = 16477688c0e00699c6cfa4497a3612d7e83c532062b64b250fed8908128ed548, vip [roles] # role = Berechtigungen... [urls] [/java] /index.html = anon /profit = authcBasic, roles[admin] /stats = authcBasic, perms["stats:read"] /** = authcBasic Auch hier kann der urls-Marker fein konfiguriert werden. Die Begrüßungsseite ist für jeden zugänglich(anon für anonyme) und die Ressourcen Profit und Statistiken sind durch Rollen oder Berechtigungen eingeschränkt. Bitte beachten Sie, dass die Reihenfolge der Anmeldung wichtig ist. Die letzte Ressource, **, wirkt sich nur auf Ressourcen aus, die nicht vorher deklariert wurden.Ein leistungsstarkes Genehmigungsmodell
Wie bereits erwähnt, empfiehlt die Dokumentation explizite Rollen, d.h. die Autorisierung von Operationen durch Berechtigungen und nicht durch Rollen. Das Berechtigungsmodell von Shiro ist sein bester Trumpf: Die Berechtigungen werden mit einer einfachen Zeichenkette beschrieben, die wie domain:action:instance organisiert ist (z.B. printer:state:lp7200) und mit Platzhaltern konfiguriert werden kann (die Berechtigung für eine Reihe von Objekten/Aktionen kann mit printer:* oder *:state erteilt werden). Dieses Modell wird auf der entsprechenden Dokumentationsseite näher erläutert. In unserem Fall ist es möglich, Ressourcen als erste Autorisierungsebene und HTTP-Verben als zweite zu verwenden. Die Hinzufügung dieser Berechtigungsfilterung zu unserer Authentifizierung kann auf diese Weise erfolgen: [java] [urls] /safe/** = authcBasic, rest[safe] [/java] Der erste Filter betrachtet die Authentifizierung mit Headern, der zweite erlaubt die Autorisierung von Ressourcen mit Berechtigungen wie resource:httpmethod (es ist notwendig, ihn mit dem Ressourcennamen zu konfigurieren). Um dies mit dem bestehenden Code zu verwenden, müssen wir unsere StaticRealm: [java] public class StaticRealm extends AuthorizingRealm { [...] @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection prins) { checkNotNull(prins, "Das Argument der Methode PrincipalCollection darf nicht null sein."); String username = (String) prins.getPrimaryPrincipal(); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo( Safe.getRoles(username)); info.setStringPermissions(Safe.getPermissions(username)); return info; } } [/java] Und die Referenz muss den Benutzern Berechtigungen hinzufügen: [java] public class Safe { [...] static{ passwords.put("paul", encrypt("blue")); permissions.put("paul", "safe:*"); } } [/java] Um es einfach zu halten, wird die Autorisierung dem Authentifizierungs-Realm überlassen. Dies ist möglich, weil Filter Rollen und Berechtigungen von authentifizierten Benutzern akkumulieren. Der authcBasic-Filter gibt authentifizierte Benutzer an den Rest-Filter weiter. Da dieser keinen Realm hat, verlässt er sich nur auf seinen Vorgänger, um Zugriff auf eine Ressource zu gewähren oder nicht. Die Reihenfolge der Filter ist also wichtig. Um eine Authentifizierung und Autorisierung zu erhalten, kann ein Aufruf für jeden Filter in der Kette gültig sein. Außerdem ist es optional, Realms an Filter zu binden, wie wir es mit authcBasic getan haben. Es ist möglich, sie unabhängig voneinander auf folgende Weise zu deklarieren: [java] [main] staticRealm = com.xebia.shiro.StaticRealm anotherRealm = com.xebia.shiro.AnotherRealm matcher = com.xebia.shiro.ReverseCredentialsMatcher staticRealm.credentialsMatcher = $matcher securityManager.realms = $staticRealm, $anotherRealm strategy = org.apache.shiro.authc.pam.FirstSuccessfulStrategy securityManager.authenticator.authenticationStrategy = $strategy [urls] /safe/** = authcBasic, rest[safe] [/java] Die obige Strategie gibt die Benutzerrollen und Berechtigungen zurück, sobald ein Realm passt. Nur diese Rechte werden verwendet, sie werden mit den folgenden Realms-Rechten kumuliert, wie es in der Standardkonfiguration der Fall war(AtLeastOneSuccessfulStrategy).Mit Kommentaren autorisieren
Die fehlende Integration der AspectJ-Annotationen von Shiro @RequiresRoles und @RequiresPermissions --- obwohl gut dokumentiert --- in der Webumgebung sollte dazu führen, dass in unserem Fall die integrierte Sicherheit von Jersey verwendet wird. Wie bereits erwähnt, verwendet es Jaas; es wird mit Marks Security-Constraint in der web.xml mit detaillierten Rollen konfiguriert. Die Klasse RolesAllowedResourceFilterFactory die für die Jersey-Authentifizierung zuständig ist, kann so geändert werden, dass sie @RolesAllowed anstelle von SecurityUtils.getSubject().hasRole(..) zu verwenden. Dies kann dazu beitragen, die Autorisierung zu vereinfachen, die sonst zu einem Labyrinth von Bedingungen werden kann. Leider beschränkt dieser Ansatz die Filterung auf Rollen; für Berechtigungen wird keine Anmerkung definiert. Und da Rollen explizit sein müssen, ist dieser Ansatz nicht zu empfehlen. Wie der letzte Absatz gezeigt hat, kann die Ressourcenberechtigung jedoch auch ohne Programmierung konfiguriert werden. Dies kann dazu beitragen, die Unannehmlichkeiten zu verringern, die durch das Fehlen von Shiros Anmerkungen in der Webumgebung entstehen.Shiro, ein echter Herausforderer
Shiro ist nicht frei von Fehlern. Die erste Lücke ist das Fehlen eines geeigneten Digest-Authentifizierungsfilters(schwer, ihn selbst zu programmieren). Was die Dokumentation betrifft, so fehlt in mehreren Klassen die JavaDoc, so dass man sich auf den Quellcode verlassen muss. Außerdem fehlt eine gründliche Dokumentation aller Konfigurationsmöglichkeiten (ein gutes Beispiel dafür ist der Vererbungsgraph von IniShiroFilter für die SecurityManager-Strategie ). Was den Code betrifft, so kann die Unmöglichkeit, zwei Sicherheitssysteme zu haben, stören(Realms können in einem SecurityManager verkettet werden, aber in einer einfachen Kette), und schließlich erschwert die umfangreiche Verwendung von Vererbung die Nutzung. Trotz seiner Jugendmängel ist Shiro immer noch ein robustes Framework, das sich leicht in jede Art von Umgebung integrieren und mühelos an einen gegebenen Kontext anpassen lässt, selbst wenn dieser komplex ist. Die Leichtigkeit dieser umfassenden Sicherheits-Toolbox wirkt sich nur geringfügig auf die Leistung aus (Kryptographie-Tools, die hier nicht vorgestellt werden, machen seine Verwendung mühelos). Seine pragmatische Architektur und seine Integration in Spring machen es zu einem echten Herausforderer von Spring Security, der es verdient, ernsthaft in Betracht gezogen zu werden.Verfasst von

Yves AMSELLEM
Contact
Let’s discuss how we can support your journey.



