Simple web application with Spring Security: Part 7
More feedback was received from our customer and the customer want a slight tweak to user story 2. They want a user that was in an admin role to be automatically brought to the admin page and users in user role’s would be brought to the home page after a successful login.
User story 2 described now as:
User Story 2: With a valid username/password a user should be able to log in and view the application home page.
Note: Users that have an admin role should be automatically brought to the admin page by default.
To do this, we will need to determine at what point spring security works out the url to send an authenticated user to and customize this to meet our needs.
Determining target url
After some debugging and looking at the spring security code, we see that within the AbstractProcessingFilter there is a method called determineTargetUrl that is responsible for determining the target url to send the user to after successful authentication.
The code for it is:
protected String determineTargetUrl(HttpServletRequest request) { // Don't attempt to obtain the url from the saved request if alwaysUsedefaultTargetUrl is set String targetUrl = alwaysUseDefaultTargetUrl ? null : targetUrlResolver.determineTargetUrl(getSavedRequest(request), request, SecurityContextHolder.getContext().getAuthentication()); if (targetUrl == null) { targetUrl = getDefaultTargetUrl(); } return targetUrl; }
From the code, we can see that the responsibility for working out the target url is delegated out to a targetUrlResolver. It is passed the saved request, the request and the users authentication object. The cleanest way for us to implement our functionality will be to implement our own TargetUrlResolver and to configure the AUTHENTICATION_PROCESSING_FILTER to use our resolver instead of the default one.
Implementing our TargetUrlResolver
The code:
package com.heraclitus.web; import javax.servlet.http.HttpServletRequest; import org.springframework.security.Authentication; import org.springframework.security.GrantedAuthority; import org.springframework.security.ui.TargetUrlResolver; import org.springframework.security.ui.TargetUrlResolverImpl; import org.springframework.security.ui.savedrequest.SavedRequest; import com.heraclitus.domain.Role; /** * I am responsible for determining what url should be the targetUrl for the * user based on their {@link GrantedAuthority}. * * If I cant match any of the {@link GrantedAuthority}'s with our application * {@link Role}'s I default back to the {@link TargetUrlResolverImpl} * implementation provided by spring security. */ public class RoleBasedTargetUrlResolverImpl implements TargetUrlResolver { private static final String ADMIN_ROLE_TARGET_URL = "/admin.htm"; private final TargetUrlResolver defaultSpringSecurityUrlResolver; public RoleBasedTargetUrlResolverImpl( final TargetUrlResolver fallbackTargetUrlResolver) { defaultSpringSecurityUrlResolver = fallbackTargetUrlResolver; } /** * I determine the target url based on the {@link Authentication} and * whether their {@link GrantedAuthority} match any of our applications * {@link Role} 's. */ public String determineTargetUrl(final SavedRequest savedRequest, final HttpServletRequest currentRequest, final Authentication auth) { if (containsAdminAuthority(auth)) { return ADMIN_ROLE_TARGET_URL; } return defaultSpringSecurityUrlResolver.determineTargetUrl( savedRequest, currentRequest, auth); } private boolean containsAdminAuthority(final Authentication auth) { for (final GrantedAuthority grantedAuthority : auth.getAuthorities()) { if (grantedAuthority.getAuthority().equals( Role.ADMIN_ROLE.roleName())) { return true; } } return false; } }
Things to note:
Code for Role’s
package com.heraclitus.domain; /** * I describe the different application roles that exist */ public enum Role { ADMIN_ROLE("ROLE_ADMIN", 1), USER_ROLE("ROLE_USER", 0); private final int order; private final String roleName; private Role(final String roleName, final int order) { this.roleName = roleName; this.order = order; } public int order() { return order; } public String roleName() { return roleName; } }
Unit tests:
package com.heraclitus.web; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; import org.jmock.Expectations; import org.jmock.Mockery; import org.jmock.integration.junit4.JMock; import org.jmock.integration.junit4.JUnit4Mockery; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.security.Authentication; import org.springframework.security.GrantedAuthority; import org.springframework.security.ui.TargetUrlResolver; import com.heraclitus.domain.Role; import com.heraclitus.spring.security.AuthenticationConfigurableStub; import com.heraclitus.spring.security.GrantedAuthorityConfigurableStub; /** * I test {@link RoleBasedTargetUrlResolverImpl}. */ @SuppressWarnings("synthetic-access") @RunWith(JMock.class) public class RoleBasedTargetUrlResolverImplTest { // collaborators/peers private TargetUrlResolver fallbackTargetUrlResolver; // mockery private final Mockery mockery = new JUnit4Mockery(); // class under test private RoleBasedTargetUrlResolverImpl roleBasedTargetUrlResolver; @Before public void setupTestMethod() { fallbackTargetUrlResolver = mockery.mock(TargetUrlResolver.class); roleBasedTargetUrlResolver = new RoleBasedTargetUrlResolverImpl( fallbackTargetUrlResolver); } @Test public void shouldReturnAdminTargetUrlWhenAdminAuthorityExists() { // setup final GrantedAuthority adminAuthority = new GrantedAuthorityConfigurableStub( Role.ADMIN_ROLE); final GrantedAuthority[] authorities = new GrantedAuthority[] { adminAuthority }; final Authentication auth = new AuthenticationConfigurableStub( authorities); // exercise test final String determinedTargetUrl = roleBasedTargetUrlResolver .determineTargetUrl(null, null, auth); // state verification assertThat(determinedTargetUrl, is("/admin.htm")); } @Test public void shouldDelegateToFallbackTargetResolverWhenAdminAuthorityDoesNotExist() { // setup final GrantedAuthority userAuthority = new GrantedAuthorityConfigurableStub( Role.USER_ROLE); final GrantedAuthority[] authorities = new GrantedAuthority[] { userAuthority }; final Authentication auth = new AuthenticationConfigurableStub( authorities); // behavior verification mockery.checking(new Expectations() { { one(fallbackTargetUrlResolver).determineTargetUrl(null, null, auth); will(returnValue("/anyTargetUrl")); } }); // exercise test final String determinedTargetUrl = roleBasedTargetUrlResolver .determineTargetUrl(null, null, auth); // state verification assertThat(determinedTargetUrl, is("/anyTargetUrl")); } }
Acceptance tests:
@Test public void shouldForwardUsersWithAdminRoleToTheAdminPageAfterLogin() { loginAsUser(driver, withAdminRole()); assertThat(driver.getTitle(), is("Admin: Spring Security Web Application")); }
The applicationContext-security.xml file:
<?xml version="1.0" encoding="UTF-8"?> <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-2.5.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-2.0.4.xsd"> <beans:bean id="sessionRegistry" class="org.springframework.security.concurrent.SessionRegistryImpl" /> <beans:bean id="defaultConcurrentSessionController" class="org.springframework.security.concurrent.ConcurrentSessionControllerImpl"> <beans:property name="sessionRegistry" ref="sessionRegistry" /> <beans:property name="exceptionIfMaximumExceeded" value="true" /> </beans:bean> <beans:bean id="defaultTargetUrlResolver" class="org.springframework.security.ui.TargetUrlResolverImpl" /> <beans:bean id="roleBasedTargetUrlResolver" class="com.heraclitus.web.RoleBasedTargetUrlResolverImpl"> <beans:constructor-arg ref="defaultTargetUrlResolver" /> </beans:bean> <beans:bean id="daoAuthenticationProvider" class="org.springframework.security.providers.dao.DaoAuthenticationProvider"> <beans:property name="userDetailsService" ref="userDetailsService" /> <beans:property name="hideUserNotFoundExceptions" value="false" /> </beans:bean> <beans:bean id="authenticationManager" class="org.springframework.security.providers.ProviderManager"> <beans:property name="providers"> <beans:list> <beans:ref local="daoAuthenticationProvider" /> </beans:list> </beans:property> <beans:property name="sessionController" ref="defaultConcurrentSessionController" /> </beans:bean> <beans:bean id="customAuthenticationProcessingFilter" class="org.springframework.security.ui.webapp.AuthenticationProcessingFilter"> <custom-filter position="AUTHENTICATION_PROCESSING_FILTER" /> <beans:property name="defaultTargetUrl" value="/home.htm" /> <beans:property name="authenticationManager" ref="authenticationManager" /> <beans:property name="authenticationFailureUrl" value="/login.jsp?authfailed=true" /> <beans:property name="allowSessionCreation" value="true" /> <beans:property name="targetUrlResolver" ref="roleBasedTargetUrlResolver" /> </beans:bean> <global-method-security secured-annotations="disabled"> </global-method-security> <beans:bean id="myAuthenticationEntryPoint" class="org.springframework.security.ui.webapp.AuthenticationProcessingFilterEntryPoint"> <beans:property name="loginFormUrl" value="/login.jsp" /> </beans:bean> <http entry-point-ref="myAuthenticationEntryPoint" auto-config="false"> <intercept-url pattern="/login.jsp" filters="none" /> <intercept-url pattern="/admin.htm" access="ROLE_ADMIN" /> <intercept-url pattern="/**" access="ROLE_USER" /> <!-- no longer needed as using custom authtication approach <form-login login-page="/login.jsp" default-target-url="/home.htm" always-use-default-target="false" authentication-failure-url="/login.jsp?authfailed=true" /> --> <logout invalidate-session="true" logout-url="/logout.htm" logout-success-url="/login.jsp?loggedout=true" /> <!-- make sure you have org.springframework.security.ui.session.HttpSessionEventPublisher registered in the web.xml file. --> <!-- no longer used - config set in bean --> <!-- <concurrent-session-control max-sessions="1" exception-if-maximum-exceeded="true" /> --> </http> <authentication-provider> <user-service id="userDetailsService"> <user name="admin" password="admin" authorities="ROLE_USER, ROLE_ADMIN" /> <user name="username" password="password" authorities="ROLE_USER" /> <user name="test" password="test" authorities="ROLE_USER" /> </user-service> </authentication-provider> </beans:beans>
As always, build, deploy run tests and verify all are passing.
Getting the code
The code for this part is tagged and available for viewing online at: http://code.google.com/p/spring-security-series/source/browse/#svn/tags/SpringSecuritySeriesWAR-Part7
SVN Url: https://spring-security-series.googlecode.com/svn/tags/SpringSecuritySeriesWAR-Part7
December 23, 2008 at 12:20 am |
WOW,great job. amazing.
some minor suggestions let you guys know.
this line private static final String ADMIN_ROLE_TARGET_URL = “/admin.htm”;
still seems hard code. if it can be injected as a property from spring config XML file will make it more flexible.
another idea is for test case. same idea
for example:
// try to get to home.htm
driver.get(“http://localhost:8080/springsecuritywebapp/home.htm”);
// check we are on our custom login page
assertThat(driver.getTitle(),
is(“Login: Spring Security Web Application”));
final WebElement usernameField = driver.findElement(By
.name(“j_username”));
usernameField.sendKeys(“username”);
final WebElement passwordField = driver.findElement(By
.name(“j_password”));
passwordField.sendKeys(“password”);
string
http://localhost:8080/springsecuritywebapp
“j_username”,”username”
“j_password”,”password”
can set by a property file or create a interface to hold these contants
but the whole codes you guys made are really beautiful.
thanks a lot.
Merry X’mas and Happy New Year.
heden
December 23, 2008 at 1:22 am |
Thanks for the comments heden. Agreed that the value for ADMIN_ROLE_TARGET_URL would be configured through some properties rather than hard coded as is. It was just done this way for convenience as I am trying to concentrate on talking about spring security features and working with spring security. I will add post sometime dealing with properties etc. As for the test code, we can certainly improve upon it but thats a post for a different topic..
as always any input into things you would like to discuss in the series are welcome.
Merry Christmas and Happy new year,
heraclitus
December 23, 2008 at 10:21 am |
Hi heraclitus:
totally agree.focusing on the main architecure ,then refactor later.actually,I do same thing.
and I am lucky to find your training here.
thanks
heden
May 30, 2009 at 8:23 am |
Hi
i nwant to redirect usre to a different page on credentials expired exception
auto config=false
September 2, 2009 at 11:15 pm |
Thanks for this post. I have a very similar requirement, and this got me headed in the right direction. I appreciate the time you’ve saved me having to work this out myself!
November 25, 2009 at 5:39 am |
Thanks for the post and the series. I thought I’d convert the unit test to use Mockito. Here is the code in case you’re interested:
package com.heraclitus.web;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.times;
import org.junit.Before;
import org.junit.Test;
import org.springframework.security.Authentication;
import org.springframework.security.GrantedAuthority;
import org.springframework.security.ui.TargetUrlResolver;
import com.heraclitus.domain.Role;
/**
* I test {@link RoleBasedTargetUrlResolverImpl}.
*/
public class RoleBasedTargetUrlResolverImplTest {
// collaborators/peers
private TargetUrlResolver fallbackTargetUrlResolver;
// class under test
private RoleBasedTargetUrlResolverImpl roleBasedTargetUrlResolver;
@Before
public void setupTestMethod() {
fallbackTargetUrlResolver = mock(TargetUrlResolver.class);
roleBasedTargetUrlResolver = new RoleBasedTargetUrlResolverImpl(
fallbackTargetUrlResolver);
}
@Test
public void shouldReturnAdminTargetUrlWhenAdminAuthorityExists() {
// setup
final GrantedAuthority adminAuthority = mock(GrantedAuthority.class);
when(adminAuthority.getAuthority()).thenReturn(
Role.ADMIN_ROLE.toString());
final GrantedAuthority[] authorities = new GrantedAuthority[] { adminAuthority };
final Authentication auth = mock(Authentication.class);
when(auth.getAuthorities()).thenReturn(authorities);
when(fallbackTargetUrlResolver.determineTargetUrl(null, null, auth))
.thenReturn(“/admin.htm”);
// exercise test
final String determinedTargetUrl = roleBasedTargetUrlResolver
.determineTargetUrl(null, null, auth);
// state verification
assertThat(determinedTargetUrl, is(“/admin.htm”));
}
@Test
public void shouldDelegateToFallbackTargetResolverWhenAdminAuthorityDoesNotExist() {
// setup
final GrantedAuthority userAuthority = mock(GrantedAuthority.class);
when(userAuthority.getAuthority())
.thenReturn(Role.USER_ROLE.toString());
final GrantedAuthority[] authorities = new GrantedAuthority[] { userAuthority };
final Authentication auth = mock(Authentication.class);
when(auth.getAuthorities()).thenReturn(authorities);
when(fallbackTargetUrlResolver.determineTargetUrl(null, null, auth))
.thenReturn(“/anyTargetUrl”);
// exercise test
final String determinedTargetUrl = roleBasedTargetUrlResolver
.determineTargetUrl(null, null, auth);
// behavior verification
verify(fallbackTargetUrlResolver, times(1)).determineTargetUrl(null,
null, auth);
// state verification
assertThat(determinedTargetUrl, is(“/anyTargetUrl”));
}
}
November 29, 2009 at 2:11 pm |
Thanks Christian, my preference now is for using mockito over jmock and if i was rewriting it again I would use mockito.
March 24, 2011 at 2:49 pm |
Hi,
It has been a while since you wrote this page, so maybe my comment is no longer required, but just in case, since I have seen this page quoted in a few places and you helped me get to working code:
There is (at least now) a way that I think is a lot easier:
Instead of deleting
change the default-target-url=”/home.htm” to something like authentication-success-handler-ref=”myAuthenticationSuccessHandler” where
“myAuthenticationSuccessHandler” is a Bean that implements “AuthenticationSuccessHandler” .
There is one required method
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication)
throws IOException, ServletException {
response.sendRedirect(getTheDesiredTargetURL(request, authentication));
}
As I said, without your site I would not have found the above solution. So thanks for your help.
March 3, 2012 at 10:57 am |
Can some body explain me what is the value 1 and 0 in both roles ADMIN_ROLE(“ROLE_ADMIN”, 1), USER_ROLE(“ROLE_USER”, 0); This is when I have one Admin role which also contain user role and one user role. What if I have more that 2 roles for admin role e.g. Admin, supervisor, user etc..? what will be the # in admin role will be?
April 22, 2012 at 7:55 am |
Thanks for this interesting post. I will likely be certain to tell others about this site 🙂 Excellent post. Cant wait to check out your next post.