Ah! Good ol ACEGI. Or should I say the good old new ACEGI aka Spring Security. In one of my previous posts I blogged about configuring good ol ACEGI. Since I last looked at it ACEGI is now Spring Security. Configuration nightmare has been reduced greatly.
For those attempting Spring Security for the first time, a piece of advice. Ignore all comparisons to ACEGI and move on. You will find your brain in better shape at the end of the excercise.
First of all to make my life easier I created a Dynamic Web Project in Eclipse 3.3.2. Next I have Tomcat configured to run within my IDE for quick testing. My goal is to simply protect a bunch of web pages using Spring Security. Its easy to extend this to larger web apps.
First lets see the web.xml below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
<web-app> <display-name>springsecurity</display-name> <context-param> <param-name>log4jConfigLocation</param-name> <param-value>/WEB-INF/log4j.properties</param-value> </context-param> <context-param> <param-name>contextConfigLocation</param-name> <param-value> /WEB-INF/application-security.xml /WEB-INF/application-service.xml </param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <listener> <listener-class>org.springframework.web.util.Log4jConfigListener</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> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list> </web-app> |
Only thing of interest is the listener org.springframework.web.filter.DelegatingFilterProxy. I would love to compare this to ACEGI configuration but I will resist the urge. Just forget any old stuff. Suffice it to say that all URLS with the configured pattern pass through this filter and Spring Security is “performed” on them.
Next let us see the login.jsp page:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
<%@ include file="includes.jsp"%> <%@ page import="org.springframework.security.ui.AbstractProcessingFilter"%> <%@ page import="org.springframework.security.ui.webapp.AuthenticationProcessingFilter"%> <%@ page import="org.springframework.security.AuthenticationException"%> <html> <head> <title>Login</title> </head> <body> <% if (session.getAttribute(AbstractProcessingFilter.SPRING_SECURITY_LAST_EXCEPTION_KEY) != null) { %> <font color="red"> Your login attempt was not successful, please try again.<BR> <br /> Reason: <%=((AuthenticationException)session.getAttribute(AbstractProcessingFilter.SPRING_SECURITY_LAST_EXCEPTION_KEY)).getMessage()%> </font> <% } %> <form method="post" id="loginForm" action="<c:url value='j_spring_security_check'/>"> Username: <input type="text" name="j_username" id="j_username" /> <br /> Password: <input type="password" name="j_password" id="j_password" /><br /> <input type="submit" value="Login" /> </form> </body> </html> |
Refer to the login form and the way the parameters are named. For Spring Security to pick up the attributes you must name it the same as above.
Most important … here is the spring context file application-security.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
<beans> <security:authentication-manager alias="authenticationManager" /> <security:http auto-config="true" access-denied-page="/accessdenied.jsp"> <security:form-login login-page="/login.jsp" authentication-failure-url="/login.jsp" default-target-url="/index.jsp" /> <security:logout logout-success-url="/login.jsp" /> <security:intercept-url pattern="/index.jsp" access="ROLE_ADMIN,ROLE_USER" /> <security:intercept-url pattern="/admin/**" access="ROLE_ADMIN" /> <security:intercept-url pattern="/**" access="ROLE_ANONYMOUS" /> </security:http> <bean id="loggerListener" class="org.springframework.security.event.authentication.LoggerListener" /> <security:authentication-provider> <security:password-encoder hash="md5" /> <security:user-service> <security:user password="5f4dcc3b5aa765d61d8327deb882cf99" name="thomasm" authorities="ROLE_USER,ROLE_ANONYMOUS" /> <security:user password="5f4dcc3b5aa765d61d8327deb882cf99" name="admin" authorities="ROLE_ADMIN,ROLE_USER,ROLE_ANONYMOUS" /> </security:user-service> </security:authentication-provider> </beans> |
- Spring namespace is used to configure security.
- security:authentication-manager need not be listed. If not a default will be created. List it if you need to refer it from some other configuration. In my case I just did to make the point.
- security:http is very self explanatory. We configure form login page names and the roles-to-URL patterns to protect. You can choose to not have this mapping here and instead implement your own object definition source class and provide the info from there.
- Finally security:authentication-provider is used to configure a password encoder and in this case a in-memory user store. The password is ‘password’. I have listed the MD5 values above to show the use of the encoder.
Once logged in the user is sent to the index.jsp which shows some common content and some admin specific content (if user has admin role).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<%@ include file="includes.jsp"%> <html> <head> <title>Home</title> </head> <body> You are logged in. To log out click <a href='<c:url value="j_spring_security_logout"/>'>log out</a> <br /> <a href="admin/admin.jsp">admin</a> <br /> <authz:authorize ifAllGranted="ROLE_ADMIN"> <p style="font-weight: bold">This text is only visible to admin users.</p> </authz:authorize> </body> </html> |
- Note the use of the taglib authz to show/hide admin related content.
Following screen shots show you how this all works…
The bold text above is only displayed to users with admin role.
Now for a few tips. Obviously you will not be hardcoding the user name and password into the configuration. Right my friend! Now for this you can implement your own class that gets the credentials from wherever you choose. This class then hooks into Spring Security. Here is a sample class CustomUserService.java where I have mocked the credentials in code.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
package com.test; import org.springframework.dao.DataAccessException; import org.springframework.security.GrantedAuthority; import org.springframework.security.GrantedAuthorityImpl; import org.springframework.security.userdetails.User; import org.springframework.security.userdetails.UserDetails; import org.springframework.security.userdetails.UserDetailsService; import org.springframework.security.userdetails.UsernameNotFoundException; public class CustomUserService implements UserDetailsService { public UserDetails loadUserByUsername(String user) throws UsernameNotFoundException, DataAccessException { User ud = null; if ("admin".equals(user)) { GrantedAuthority[] auths = new GrantedAuthority[] { new GrantedAuthorityImpl("ROLE_ADMIN"), new GrantedAuthorityImpl("ROLE_USER"), new GrantedAuthorityImpl("ROLE_ANONYMOUS") }; ud = new User(user, "5f4dcc3b5aa765d61d8327deb882cf99", true, true, true, true, auths); } else if ("thomasm1".equals(user)) { GrantedAuthority[] auths = new GrantedAuthority[] { new GrantedAuthorityImpl("ROLE_USER"), new GrantedAuthorityImpl("ROLE_ANONYMOUS") }; ud = new User(user, "5f4dcc3b5aa765d61d8327deb882cf99", true, true, true, true, auths); } return ud; } } |
In the application-security.xml file you need to make the following change.
1 2 3 4 |
<security:authentication-provider user-service-ref="customUserService"> <security:password-encoder hash="md5" /> </security:authentication-provider> |
The rest of the code should work same. Finally one more useful feature that I have used with the good ol ACEGI. That is to provide security access protection to the service layer. Add the following to your configuration…
1 2 3 |
<global-method-security secured-annotations="enabled" jsr250-annotations="enabled" /> |
Next add the annotations @Secured( {“ROLE_SECRET_AGENT”} ) to your service methods.