Die Dokumentation, in der die neuen Features von Spring Security beschrieben werden, ist leider an einigen Stellen nicht detailliert genug. Ich habe es mit ausprobieren dennoch hinbekommen und will es hier kurz beschreiben.
Zunächst die Konfiguration der Spring MVC Controller. Die Konfiguration in der web.xml Datei sieht wie folgt aus.
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<servlet>
<servlet-name>spring-controller</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>spring-controller</servlet-name>
<url-pattern>/admin/*</url-pattern>
</servlet-mapping>
Da der gewählte Servlet-Name spring-controller lautet, wird eine Konfigurationsdatei mit Namen spring-controller-servlet.xml im WEB-INF Verzeichnis benötigt. Diese sieht bei mir wie folgt aus.
<beans xmlns="http://www.springframework.org/schema/beans" xsi="http://www.w3.org/2001/XMLSchema-instance" context="http://www.springframework.org/schema/context" p="http://www.springframework.org/schema/p" schemalocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd">
<!-- Enable autowiring -->
<context:component-scan package="com.mini.biz.servlets">
<!-- Enabale @Controller annotations -->
<bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping">
<!-- Enabale @RequestMapping annotations -->
<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
<!-- View resolver -->
<bean id="viewResolver" class="org.springframework.web.servlet.view.UrlBasedViewResolver">
<property name="viewClass" value="org.springframework.web.servlet.view.JstlView">
<property name="prefix" value="/WEB-INF/view/">
<property name="suffix" value=".jsp">
</property>
</property>
</property></bean></bean></bean></context:component-scan></beans>
Es kommen drei neue Spring 2.5. Elemente darin vor. Das context:component-scan Element sorgt dafür, dass das angegebene Package gescannt wird um das Autowiring der Dependencies durchzuführen. Die DefaultAnnotationHandlerMapping bean ermöglicht das Nutzen der @Controller Annotations. Die AnnotationMethodHandlerAdapter bean ermöglicht das Nutzen der @RequestMapping Annotations. Die viewResolver Bean ist im „oldschool“ Spring MVC Style deklariert. Hier gibt es meiner Meinung nach bisher keine andere Möglichkeit. Meine Requests müssen also wie folgt aussehen: /admin/* damit sie vom Spring DispatcherServlet verarbeitet werden (siehe web.xml). Alles weitere dahinter, z.B. /admin/show.do muss von einem Controller abgearbeitet werden. Die entsprechenden Annotationen im Controller sehen also so aus:
@Controller
@RequestMapping("/show.do")
public class AdminShowController {
In meinem zu schützenden Admin Bereich gibt es 3 Ausgabeseiten, die auf /show.do, /edit.do und /pdf.do gemappt sind. Für einen formularbasierten Schutz, wird ein Login Formular benötigt. Ich führe dafür einen neuen Controller ein.
@Controller
@RequestMapping("/anmelden.do")
public class AdminLoginController
{
@RequestMapping(method = RequestMethod.GET)
public String getDefaultForm()
{
return "login";
}
@RequestMapping(value = "/process.do", method = RequestMethod.POST)
public String onSubmit()
{
return "login";
}
}
Per default reagiert der Controller also auf /admin/anmelden.do. Ich wollte eigentlich login.do als Mapping verwenden, aber irgendwie scheint das in Spring MVC bereits ein reservierter Handler zu sein, den man nicht selbst noch einmal vergeben kann. Der AdminLoginController gibt login als View Name zurück. In meiner Webapplikation muss es also eine Datei /WEB-INF/view/login.jsp geben, die das Login Formular enthält.
Kommen wir nun zur Spring Security Konfiguration. Zunächst müssen diese beiden Elemente in der web.xml dazukommen.
<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>/admin/*</url-pattern>
</filter-mapping>
Alles unterhalb von /admin/ durchläuft damit zusätzlich die springSecurityFilterChain. Für die Konfiguration der Security Beans lege ich eine separate XML Datei an, die ich in der applicationContext.xml Datei einbinde. Meine securityBeans.xml sieht wie folgt aus:
<b:beans xmlns="http://www.springframework.org/schema/security" b="http://www.springframework.org/schema/beans" xsi="http://www.w3.org/2001/XMLSchema-instance" schemalocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-2.0.1.xsd">
<http config="true">
<intercept-url pattern="/admin/anmelden.do*" filters="none">
<intercept-url pattern="/**" access="ROLE_ADMIN">
<form-login page="/admin/anmelden.do" url="/admin/process.do">
</form-login>
<authentication-provider>
<user-service>
<user name="bob" password="bob" authorities="ROLE_ADMIN">
</user>
</user-service>
</authentication-provider>
</intercept-url></intercept-url></http></b:beans>
Die auf der Spring Security Webseite angegebene Namespace Konfiguration könnt ihr nicht 1:1 übernehmen, da es sonst zu einer Exception mit Ausgabe „namespace Invalid content was found“ kommt. Übernehmt einfach meine Namespace deklaration wenn ihr ein separates File habt. Alternativ kann man es auch so machen.
<beans xmlns="http://www.springframework.org/schema/beans" security="http://www.springframework.org/schema/security" xsi="http://www.w3.org/2001/XMLSchema-instance" schemalocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-2.0.xsd">
<security:http config="true">
<security:intercept-url pattern="/admin/anmelden.do*" filters="none">
<security:intercept-url pattern="/**" access="ROLE_ADMIN">
<security:form-login page="/admin/anmelden.do" url="/admin/process.do">
</security:form-login>
<security:authentication-provider>
<security:user-service>
<security:user name="bob" password="bob" authorities="ROLE_ADMIN">
</security:user>
</security:user-service>
</security:authentication-provider>
</security:intercept-url></security:intercept-url></security:http></beans>
Was macht diese Konfiguration? Zunächst wird die URL des Login Formulars /admin/anmelden.do komplett aus der Sicherung herausgenommen. Das ist notwendig damit man das Login Formular sehen kann ohne sich vorher zu authentifizieren. Mit /** werden dann alle URL's unterhalb von /admin geschützt und der Rolle ROLE_ADMIN zugewiesen. Jetzt kommt mit dem form-login Element das Herzstück. login-page=“/admin/anmelden.do“ legt die (eigene) URL des Login Formulars fest. default-target-url="/admin/show.do" ist die URL, an die nach erfolgreichem Login weitergeleitet wird.
Wichtig ist das login-processing-url Attribut. Es muss sich von login-page und default-target-url unterscheiden. Ist login-processing-url = login-page, dann wird der AbstractProcessingFilter von Spring Security nicht durchlaufen, da das Login Formular ungeschützt ist. Ist login-processing-url = default-target-url funktioniert zwar der Login, bei der Weiterleitung an die target URL wird aber erneut nach den Username und Password Parametern geschaut, die dann natürlich nicht vorhanden sind. Ich habe mich für login-processing-url="/admin/process.do" entschieden und diese neue URL im AdminLoginController auf POST Requests gemapped (siehe oben).
Das security:authentication-provider Element definiert einen einzelnen rudimentären Benutzer für die Rolle Admin. Username bob mit Passwort bob. Hier bietet Spring Security angeklügelte andere Mechanismen. Ich denke für eine sehr einfache Webanwendung reicht die Verwendung eines md5 oder sha Hashs in der Art:
<authentication-provider>
<password-encoder hash="sha">
<user-service>
<user name="bob" password="4e7421b1b8765d8f9406d87e7cc6aa784c4ab97f" authorities="ROLE_ADMIN">
</user>
</user-service>
</password-encoder></authentication-provider>
Damit ist eigentlich alles bereits fertig. Die /WEB-INF/view/login.jsp Datei sieht vereinfacht so aus:
<form action="/stats/process.do" method="post">
Benutzername: <input name="j_username" type="text">
Passwort: <input name="j_password" type="password">
<input value="Login" type="submit">
</form>
Die beiden Eingabefelder müssen in der Standardkonfiguration j_username und j_password heissen.