Spring Security (formerly Acegi Security) is a great framework for adding authentication and authorization to your Java web app. I wanted to take advantage of it in my Compojure apps. Here's an example of how to do it:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(defn- boot-spring | |
"Initialize a Jetty server for Spring and also Spring Security" | |
([server handler context-config-location] | |
(let [filter (doto (org.mortbay.jetty.servlet.FilterHolder. org.springframework.web.filter.DelegatingFilterProxy) | |
(.setName "springSecurityFilterChain")) | |
servlet (doto (org.mortbay.jetty.servlet.ServletHolder. (ring.util.servlet/servlet handler)) | |
(.setName "default")) | |
context (doto (org.mortbay.jetty.servlet.Context. server "/" | |
(bit-or org.mortbay.jetty.servlet.Context/SESSIONS | |
org.mortbay.jetty.servlet.Context/SECURITY)) | |
(.addFilter filter "/*" 0) | |
(.addServlet servlet "/") | |
(.addEventListener (org.springframework.web.context.ContextLoaderListener.)))] | |
(when context-config-location | |
(.setInitParams context {"contextConfigLocation" context-config-location})) | |
(.addHandler server context))) | |
([server handler] | |
(boot-spring server handler nil))) |
The next step is to wrap this in a ring/ring-adapter-jetty configurator (i.e., a function that takes a server):
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(defn boot [] | |
(jetty/run-jetty #'app | |
{:port 9090 | |
:join? false | |
:configurator (fn [server] (boot-spring server #'app "classpath:security.xml")) | |
:default-handler? false})) |
To illustrate the integration, here is an example Spring Security configuration which establishes two users and two roles (user and administration) and applies it to URL patterns. If the user issues a HTTP request with a URL pattern that requires authorization and they are not already authenticated, then Spring Security as configured below will actually render a plain-looking login form before proceeding. We get this basic functionality for free with Spring Security although it can be replaced with a custom form of our own design.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?xml version="1.0" encoding="UTF-8"?> | |
<beans:beans xmlns:beans="http://www.springframework.org/schema/beans" | |
xmlns="http://www.springframework.org/schema/security" | |
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.xsd | |
http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd"> | |
<authentication-manager> | |
<authentication-provider> | |
<user-service> | |
<user name="user" password="1234" authorities="ROLE_USER"/> | |
<user name="admin" password="1234" authorities="ROLE_ADMIN,ROLE_USER"/> | |
</user-service> | |
</authentication-provider> | |
</authentication-manager> | |
<http auto-config="true"> | |
<intercept-url pattern="/user**" access="ROLE_USER"/> | |
<intercept-url pattern="/admin**" access="ROLE_ADMIN"/> | |
<intercept-url pattern="/**" access="IS_AUTHENTICATED_ANONYMOUSLY"/> | |
</http> | |
</beans:beans> |
The Compojure application this secures looks like this:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(defroutes main-routes | |
(GET "/" req (index-html req)) | |
(GET "/admin" req (example-html req)) | |
(GET "/user" req (example-html req)) | |
(GET "/other" req (example-html req)) | |
(route/resources "/") | |
(route/not-found "Page not found")) |
Here the HTML rendering prints out the Spring context and security context which contain lots of goodies we can use later:
Your example works, but this occasionally pops up in the logs:
ReplyDeleteException: java.lang.IllegalArgumentException: ServletContext must not be null
Assert.java:112 org.springframework.util.Assert.notNull
WebApplicationContextUtils.java:109 org.springframework.web.context.support.WebApplicationContextUtils.getWebApplicationContext
WebApplicationContextUtils.java:99 org.springframework.web.context.support.WebApplicationContextUtils.getWebApplicationContext
core.clj:31 ring-spring-security.core/application-context
core.clj:50 ring-spring-security.core/wrap-reload-spring[fn]
core.clj:57 ring-spring-security.core/wrap-dump[fn]
reload.clj:14 ring.middleware.reload/wrap-reload[fn]
stacktrace.clj:15 ring.middleware.stacktrace/wrap-stacktrace-log[fn]
stacktrace.clj:79 ring.middleware.stacktrace/wrap-stacktrace-web[fn]
Var.java:365 clojure.lang.Var.invoke
jetty.clj:16 ring.adapter.jetty/proxy-handler[fn]
(Unknown Source) ring.adapter.jetty.proxy$org.mortbay.jetty.handler.AbstractHandler$0.handle
HandlerCollection.java:114 org.mortbay.jetty.handler.HandlerCollection.handle
HandlerWrapper.java:152 org.mortbay.jetty.handler.HandlerWrapper.handle
Server.java:322 org.mortbay.jetty.Server.handle
HttpConnection.java:542 org.mortbay.jetty.HttpConnection.handleRequest
HttpConnection.java:928 org.mortbay.jetty.HttpConnection$RequestHandler.headerComplete
HttpParser.java:549 org.mortbay.jetty.HttpParser.parseNext
HttpParser.java:212 org.mortbay.jetty.HttpParser.parseAvailable
HttpConnection.java:404 org.mortbay.jetty.HttpConnection.handle
SocketConnector.java:228 org.mortbay.jetty.bio.SocketConnector$Connection.run
QueuedThreadPool.java:582 org.mortbay.thread.QueuedThreadPool$PoolThread.run
Any idea why?
Re: Exception: java.lang.IllegalArgumentException: ServletContext must not be null
ReplyDeleteIt seems to be because I'm reloading the Spring application context on every request. The exception seems harmless but you can work around it by not reloading the Spring application context on every request. e.g. In the following (from core.clj), comment out (wrap-reload-spring):
(def app
(-> (handler/site main-routes)
(wrap-reload-spring)
(wrap-dump)
(wrap-reload '(ring-spring-security.core))
(wrap-stacktrace)))