17

I'm using Spring 3.2.4 and am unable to get Spring Security to redirect to my access-denied-handler when using Annotation based method level security. I have found several different posts about this, but to date, there does not seem to be any solutions that I have found.

My security.xml file:

<!-- need this here to be able to secure methods in components other than controllers (as scanned in applicationContext.xml) -->
<global-method-security secured-annotations="enabled" pre-post-annotations="enabled" jsr250-annotations="enabled" ></global-method-security>

<!-- Annotation/JavaConfig examples http://stackoverflow.com/questions/7361513/spring-security-login-page -->
<http use-expressions="true" entry-point-ref="authenticationEntryPoint">
    <access-denied-handler ref="accessDeniedHandler"/>

    <intercept-url pattern="/secure/login" access="permitAll" />
    <intercept-url pattern="/secure/logout" access="permitAll" />
    <intercept-url pattern="/secure/denied" access="permitAll" />
    <session-management session-fixation-protection="migrateSession" session-authentication-error-url="/login.jsp?authFailed=true"> 
        <concurrency-control max-sessions="10" error-if-maximum-exceeded="true" expired-url="/login.html" session-registry-alias="sessionRegistry"/>
    </session-management>

    <intercept-url pattern="/**" access="isAuthenticated()" />
    <form-login  default-target-url="/" authentication-failure-url="/secure/denied" />
    <logout logout-url="/secure/logout" logout-success-url="/" />
    <expression-handler ref="defaultWebSecurityExpressionHandler" />
</http>

<beans:bean id="authenticationEntryPoint" class="com.ia.security.LoginUrlAuthenticationEntryPoint">
    <beans:constructor-arg name="loginFormUrl" value="/secure/login"/>
</beans:bean>

<beans:bean id="accessDeniedHandler" class="com.ia.security.AccessDeniedHandlerImpl">
    <beans:property name="errorPage" value="/secure/denied"/>
</beans:bean>

My AccessDeniedHandlerImpl.java :

public class AccessDeniedHandlerImpl extends org.springframework.security.web.access.AccessDeniedHandlerImpl {
    // SLF4J logger
    private static final Logger logger = LoggerFactory.getLogger(AccessDeniedHandlerImpl.class);

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        logger.log("AccessDeniedException triggered!");
        super.handle(request, response, accessDeniedException);

    }
}

My Annotated Method:

@PreAuthorize("hasAuthority('ROLE_ZZZZ')")
public ModelAndView getUserInfo( @PathVariable long userId ){
    ModelAndView mv = new ModelAndView();
    User u = userService.findUser( userId );
    mv.addObject("user", u);
    return mv;
}

Is there anything special I need to do such that my access-denied-handler is called?

Eric B.
  • 23,425
  • 50
  • 169
  • 316

4 Answers4

21

After several hours of searching around and tracing Spring code, I finally discovered what was happening. I am listing this here in case it is of value to someone else.

The access-denied-handler is used by the ExceptionTranslationFilter in case of an AccessDeniedException. However, the org.springframework.web.servlet.DispatcherServlet was first trying the handle the exception. Specifically, I had a org.springframework.web.servlet.handler.SimpleMappingExceptionResolver defined with a defaultErrorView. Consequently, the SimpleMappingExceptionResolver was consuming the exception by redirecting to an appropriate view, and consequently, there was no exception left to bubble up to the ExceptionTranslationFilter.

The fix was rather simple. Configure the SimpleMappingExceptionResolver to ignore all AccessDeniedException.

<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
    <property name="defaultErrorView" value="uncaughtException" />
    <property name="excludedExceptions" value="org.springframework.security.access.AccessDeniedException" />

    <property name="exceptionMappings">
        <props>
            <prop key=".DataAccessException">dataAccessFailure</prop>
            <prop key=".NoSuchRequestHandlingMethodException">resourceNotFound</prop>
            <prop key=".TypeMismatchException">resourceNotFound</prop>
            <prop key=".MissingServletRequestParameterException">resourceNotFound</prop>
        </props>
    </property>
</bean>

Now, whenever an AccessDeniedException is thrown, the resolver ignores it and allows it to bubble up the stack to the ExceptionTranslationFilter which then calls upon the access-denied-handler to handle the exception.

Eric B.
  • 23,425
  • 50
  • 169
  • 316
  • I didn't get yet. I also faced the problem as you descirbed exactly but not get yet :( – Cataclysm May 31 '14 at 10:18
  • 4
    javaconfig for this configuration? – vivex Jul 02 '15 at 13:21
  • @Eric Can you please share the Java Config for this solution? – Sahil Chhabra Nov 24 '17 at 10:06
  • @mav3n I never bothered writing the Java Config for this solution, but can't imagine it being too difficult given that it is just a simple bean definition. At the time (with Spring 3), Java Config required more effort than in Spring 5 for some classes. I would suggest just looking at the signatures of the different methods in SimpleMappingExceptionResolver and ensure the correct class types are respected. – Eric B. Nov 26 '17 at 19:58
  • @Eric, yes i figured out the java config, but it seems that was not the issue in my case. My ControllerAdvice was not getting called because of some other ControllerAdvice with higher Order precedence. Thanks for your reply. – Sahil Chhabra Nov 27 '17 at 06:16
  • I'd like to do the same with Struts 2.5... My exception is intercepted by `org.apache.struts2.dispatcher.DefaultDispatcherErrorHandler`. I'm investigating... – Guillaume Husta Nov 29 '17 at 16:04
8

I run into the same issue. In my case there was already a @ControllerAdvise definied which should handle exceptions - so I added the AccessDeniedException directly:

@Component
@ControllerAdvice
public class ControllerBase {

...

  @ExceptionHandler(value = AccessDeniedException.class)
    public ModelAndView accessDenied() {
        return new ModelAndView("redirect:login.html");
    }
}

Good luck with it!

Jessi
  • 91
  • 1
  • 3
2

Extending the Erics answer with JavaConfig for SimpleMappingExceptionResolver to ignore AccessDeniedException so that it can be thrown as response and doesn't get swallowed by the SimpleMappingExceptionResolver.

@Configuration
@EnableWebMvc
public class AppConfig extends WebMvcConfigurerAdapter {

  @Bean
  public SimpleMappingExceptionResolver exceptionResolver() {
    SimpleMappingExceptionResolver exceptionResolver = new SimpleMappingExceptionResolver();
    exceptionResolver.setExcludedExceptions(AccessDeniedException.class);
    return exceptionResolver;
  }

}
Sahil Chhabra
  • 10,621
  • 4
  • 63
  • 62
0

Added on to Jessi answer above (https://stackoverflow.com/a/25948861/13215486). Note that if you want to tell the difference between Access Denied, and Access Forbidden, then you need to do a little more work.

@Component
@ControllerAdvice
public class ControllerBase {

...

  @ExceptionHandler(value = AccessDeniedException.class)
    public ModelAndView accessDenied(HttpServletRequest request) {
        ModelAndView mav = new ModelAndView("redirect:login.html");
        mav.setStatus(request.getRemoteUser() != null ? HttpStatus.FORBIDDEN : HttpStatus.UNAUTHORIZED);
        return mav;
    }
}