Spring Security 3: Login und Rollen

Keine Zeit? Hier die Kurzversion in 30 Sekunden.
- Spring Security hängt sich über einen Servlet-Filter (
springSecurityFilterChainmitDelegatingFilterProxy) in die Webanwendung.- Mit dem
security-Namespace reichen<http>,<intercept-url>und<form-login>für Login und Rollenprüfung.- Benutzer kommen zum Testen aus
<user-service>, im echten Betrieb aus<jdbc-user-service>und der Datenbank.- Passwörter werden mit
<password-encoder hash="sha-256">und einem Salt geprüft.- Mit
@PreAuthorizeund@Securedlassen sich auch einzelne Methoden absichern.
Nachdem ich letzten Oktober über das Speichern von Passwörtern geschrieben habe, kommt hier der passende Teil dazu: Wie bekommt man in einer Java-Webanwendung überhaupt einen Login mit Rollen? Mit Spring Security 3 ist das erstaunlich wenig Arbeit, wenn man einmal weiß, wo man anfangen muss. Ich gehe von einer normalen Spring-3-Webanwendung (Spring MVC, Java 6) aus.
Der Filter in der web.xml
Spring Security arbeitet über einen Servlet-Filter. Der Filter in der web.xml ist nur ein Platzhalter, der die Arbeit an eine Bean im Spring-Kontext weitergibt, und die muss genau springSecurityFilterChain heißen:
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
/WEB-INF/applicationContext.xml
/WEB-INF/security.xml
</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>Die Bean legt der Namespace gleich selbst an, ihr müsst sie nicht definieren.
Die security.xml
Jetzt zur eigentlichen Konfiguration. Ich nehme den Security-Namespace als Standard-Namespace, dann muss man weniger tippen:
<beans:beans xmlns="http://www.springframework.org/schema/security"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-3.0.xsd">
<http auto-config="true" use-expressions="true">
<intercept-url pattern="/login.jsp" access="permitAll"/>
<intercept-url pattern="/admin/**" access="hasRole('ROLE_ADMIN')"/>
<intercept-url pattern="/**" access="isAuthenticated()"/>
<form-login login-page="/login.jsp"
default-target-url="/start"
authentication-failure-url="/login.jsp?fehler=1"/>
</http>
<authentication-manager>
<authentication-provider>
<user-service>
<user name="anna" password="geheim" authorities="ROLE_USER"/>
<user name="admin" password="admin" authorities="ROLE_USER, ROLE_ADMIN"/>
</user-service>
</authentication-provider>
</authentication-manager>
</beans:beans>auto-config="true" schaltet Formular-Login, Logout und einige weitere Standards ein. Mit use-expressions="true" schreibt man die Zugriffsregeln als Ausdruck (hasRole(...), isAuthenticated(), permitAll). Wichtig ist die Reihenfolge der intercept-url-Einträge: Es gilt der erste Treffer. Die allgemeine Regel /** gehört deshalb ganz nach unten, sonst kommt nie jemand auf die Login-Seite. Rollen müssen übrigens mit ROLE_ beginnen.
Die user-service-Variante mit Klartext-Passwörtern ist nur zum Testen gedacht. Ohne login-page erzeugt Spring Security übrigens selbst eine einfache Login-Seite, zum ersten Ausprobieren reicht das.
Eigene Login-Seite
Das Formular schickt an j_spring_security_check, und die Felder heißen j_username und j_password:
<form action="${pageContext.request.contextPath}/j_spring_security_check" method="post">
<input type="text" name="j_username"/>
<input type="password" name="j_password"/>
<input type="submit" value="Anmelden"/>
</form>Der Logout läuft über /j_spring_security_logout, ein einfacher Link reicht.
Benutzer aus der Datenbank
Für den echten Betrieb tauscht man den user-service gegen die JDBC-Variante aus:
<authentication-provider>
<password-encoder hash="sha-256">
<salt-source user-property="username"/>
</password-encoder>
<jdbc-user-service data-source-ref="dataSource"/>
</authentication-provider>Ohne weitere Angaben erwartet Spring Security die Tabellen users (username, password, enabled) und authorities (username, authority). Wer ein eigenes Schema hat, gibt die SQL-Abfragen über users-by-username-query und authorities-by-username-query an. Der password-encoder hasht das eingegebene Passwort mit SHA-256 und hängt den Salt an, den ihr hier als Benutzernamen festlegt. Beim Anlegen eines Benutzers müsst ihr das Passwort natürlich genauso hashen, sonst passt der Vergleich nie. Zu Salt und Hashing habe ich im Oktober ausführlicher geschrieben. Der Benutzername als Salt ist besser als gar keiner, aber ein zufälliger Salt pro Benutzer wäre schöner. Das geht dann mit einer eigenen Spalte und einem eigenen SaltSource.
Methoden absichern
Zum Schluss noch kurz die Absicherung auf Methodenebene. In der security.xml schaltet man sie ein:
<global-method-security secured-annotations="enabled" pre-post-annotations="enabled"/>Danach gehen beide Varianten:
@Secured("ROLE_ADMIN")
public void kundeLoeschen(long id) { ... }
@PreAuthorize("hasRole('ROLE_ADMIN') or #name == principal.username")
public Kunde laden(String name) { ... }@Secured kennt nur Rollen, @PreAuthorize versteht dieselben Ausdrücke wie oben, auch mit Zugriff auf die Parameter. Wichtig: Das funktioniert nur bei Beans, die Spring verwaltet, und der Kontext mit der Bean muss diese global-method-security auch sehen.
Habt ihr Spring Security schon im Einsatz? Mich würde interessieren, ob ihr beim XML bleibt oder eigene Filter schreibt.
Christian Ackermann
Softwareentwickler (C++, C, Java, C#) aus der Oberpfalz, bloggt seit 2001 über Linux, Technik und Kochen.