Reputation: 31300
In my Spring MVC web application, there are certain areas accessible only to users with sufficient privileges. Rather than just have a "access denied" message, I need to be able to allow users to log in as a different user in order to use these pages (sort of like an override).
How can I do this with Spring Security?
Here's the flow I am looking to have, with a bit more detail:
Note: Page X has a big, long query string that needs to be preserved.
How can I do this with Spring Security?
Here's my spring security config 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-3.0.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-3.1.xsd">
<debug />
<global-method-security pre-post-annotations="enabled">
<!-- AspectJ pointcut expression that locates our "post" method and applies
security that way <protect-pointcut expression="execution(* bigbank.*Service.post*(..))"
access="ROLE_TELLER"/> -->
</global-method-security>
<!-- Allow anyone to get the static resources and the login page by not applying the security filter chain -->
<http pattern="/resources/**" security="none" />
<http pattern="/css/**" security="none" />
<http pattern="/img/**" security="none" />
<http pattern="/js/**" security="none" />
<!-- Lock everything down -->
<http
auto-config="true"
use-expressions="true"
disable-url-rewriting="true">
<!-- Define the URL access rules -->
<intercept-url pattern="/login" access="permitAll" />
<intercept-url pattern="/about/**" access="permitAll and !hasRole('blocked')" />
<intercept-url pattern="/users/**" access="hasRole('user')" />
<intercept-url pattern="/reviews/new**" access="hasRole('reviewer')" />
<intercept-url pattern="/**" access="hasRole('user')" />
<form-login
login-page="/login" />
<logout logout-url="/logout" />
<access-denied-handler error-page="/login?reason=accessDenied"/>
<!-- Limit the number of sessions a user can have to only 1 -->
<session-management>
<concurrency-control max-sessions="1" />
</session-management>
</http>
<authentication-manager>
<authentication-provider ref="adAuthenticationProvider" />
<authentication-provider>
<user-service>
<user name="superadmin" password="superadminpassword" authorities="user" />
</user-service>
</authentication-provider>
</authentication-manager>
<beans:bean id="adAuthenticationProvider" class="[REDACTED Package].NestedGroupActiveDirectoryLdapAuthenticationProvider">
<beans:constructor-arg value="[REDACTED FQDN]" />
<beans:constructor-arg value="[REDACTED LDAP URL]" />
<beans:property name="convertSubErrorCodesToExceptions" value="true" />
<beans:property name="[REDACTED Group Sub-Tree DN]" />
<beans:property name="userDetailsContextMapper" ref="peerReviewLdapUserDetailsMapper" />
</beans:bean>
<beans:bean id="peerReviewLdapUserDetailsMapper" class="[REDACTED Package].PeerReviewLdapUserDetailsMapper">
<beans:constructor-arg ref="UserDAO" />
</beans:bean>
</beans:beans>
I'm using a slightly modified version of the Spring Security 3.1 Active Directory connection capabilities. The modifications simply load all of a user's groups, including those reached by group nesting, rather than only the ones the user is directly a member of. I'm also using a custom user object that has my application's User object embedded in it, and a custom LDAP mapper that does the normal LDAP mapping, and then adds in my user.
There is a special authentication scenario that has not been implemented yet where the user is authenticated based on a username passed from an external application (or via Kerberos) in a Single-Sign-On fashion.
Upvotes: 6
Views: 3781
Reputation: 3131
Solution 1
Register an application wide ExeptionResolver using anyway you like. For ex.
public class MyApplicationErrorResolver extends SimpleMappingExceptionResolver {
@Autowired
private List<LogoutHandler> logoutHandlers;
@Override
protected ModelAndView doResolveException(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex) {
if(ex instanceof AccessDeniedException) {
for(LogoutHandler lh : logoutHandlers) {
lh.logout(request, response, SecurityContextHolder.getContext().getAuthentication());
}
// Not present as a bean. So create it manually.
SecurityContextLogoutHandler logoutHandler = new SecurityContextLogoutHandler();
logoutHandler.setInvalidateHttpSession(true);
logoutHandler.logout(request, response, SecurityContextHolder.getContext().getAuthentication());
return new ModelAndView(new RedirectView(request.getRequestURL().toString()));
}
return super.doResolveException(request, response, handler, ex);
}
}
register it as a bean:
<bean class="package.path.MyApplicationErrorResolver" />
(that's all you need to register it). This will work for your configuration. Bu you will probably need to remove the <access-denied-handler>
element from the config.
Solution 2
Another way is to use an AccessDeniedHandler. For ex:
public class MyAccessDeniedExceptionHandler implements AccessDeniedHandler {
@Autowired
private List<LogoutHandler> logoutHandlers;
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
for(LogoutHandler lh : logoutHandlers) {
lh.logout(request, response, SecurityContextHolder.getContext().getAuthentication());
}
SecurityContextLogoutHandler logoutHandler = new SecurityContextLogoutHandler();
logoutHandler.setInvalidateHttpSession(true);
logoutHandler.logout(request, response, SecurityContextHolder.getContext().getAuthentication());
response.sendRedirect(request.getRequestURL().toString());
}
}
register it as a bean:
<bean id="accesssDeniedHandler" class="package.path.MyAccessDeniedExceptionHandler" />
and specify it in your config:
<access-denied-handler ref="accesssDeniedHandler" />
Upvotes: 0
Reputation: 7792
How do you check for roles ?
If you define them in your security context like this:
<intercept-url pattern="/adminStuff.html**" access="hasRole('ROLE_ADMIN')" />
You can set the defaultFailureUrl
in your SimpleUrlAuthenticationFailureHandler
and when a user with lesser privileges tries to access a secured URL the FaliureHandler
should redirect you to the defaultFailureUrl
which could be your login page.
You can inject a FaliureHandler
in the filter at the FORM_LOGIN_FILTER
position.
<bean id="myFaliureHandler"
class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler">
<property name="defaultFailureUrl" value="http://yourdomain.com/your-login.html"/>
</bean>
<bean id="myFilter"
class="org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter">
<property name="authenticationFailureHandler" ref="myFaliureHandler"/>
</bean>
<http>
<custom-filter position="FORM_LOGIN_FILTER" ref="myFilter" />
</http>
Answering 1) in the comment.
This would be a little more work than I thought given your namespace configuration.
What you need to do is remove the <form-login>
definition and instead of it add a 'custom' UsernamePasswordAuthenticationFilter
(this is the filter that handles the <form-login>
element).
You also need to remove the <access-denied-handler>
.
So your configuration would look something like:
<bean id="myFaliureHandler"
class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler">
<property name="defaultFailureUrl" value="http://yourdomain.com/your-login.html"/>
</bean>
<bean id="myFilter"
class="org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter">
<property name="authenticationFailureHandler" ref="myFaliureHandler"/>
<!-- there are more required properties, but you can read about them in the docs -->
</bean>
<bean id="loginUrlAuthenticationEntryPoint"
class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
<property name="loginFormUrl" value="/login"/>
</bean>
<http entry-point-ref="authenticationEntryPoint" auto-config="false">
<!-- your other http config goes here, just omit the form-login element and the access denied handler -->
<custom-filter position="FORM_LOGIN_FILTER" ref="myFilter" />
</http>
Generally also have a look at the spring docs on custom filters, if you haven't already. We currently use this config in my current company forcing users to relogin if the don't have required privileges on a page.
Upvotes: 2