Updated 17/3/2011 with a solution to Spring Security redirecting.
I had a bit of a fight against Grails to get security exceptions handled the way I wanted, but having figured it out I thought I'd write it up. It's really very simple - I had just had the wrong end of the stick.
I want to be able to throw an org.springframework.security.access.AccessDeniedException from anywhere in my code and have that appear to the user as a custom error page of my design, but with a 403 response code (that's 403 Forbidden, rather than 500 Internal Server Error) and maintaining the originally requested URL rather than redirecting to an error URL as Spring Security sometimes prefers to do (because then you can't refresh to retry).
My main confusion arose from the way Grails' UrlMappings.groovy handles custom exception mapping. It turns out that you need to do it like this:
// Handling specific exceptions requires a 500 code on the left for the // mapping to pick them up, but we can send back another code in the // controller that sends back the response. "500"(controller:"error", action:"error403", exception:AccessDeniedException)
The only surprising thing is that you need a "500" code at the left rather than "403". The reasoning seems to be that this is the incoming error code to the mapping function, and all exceptions are deemed to represent a 500 error code at this point, i.e. they represent an internal server error, which on reflection isn't entirely unreasonable. To then send the error back to the user I have an ErrorController and an associated view in /views/error/error403.gsp.
@Secured(['permitAll']) class ErrorController { def error403 = { // Ensure that the correct response code gets sent. If we don't do // this, it may send a 500 (internal server error) response because // of the way we had to configure the UrlMappings for handling specific // exception types with 500. return response.sendError(javax.servlet.http.HttpServletResponse.SC_FORBIDDEN) } }
I also figured out how to stop Spring redirecting to a new URL when it decided to deny access. I'd rather the originally requested URL remain in the address bar. The trick is to put the following in Config.groovy.
grails.plugins.springsecurity.adh.errorPage = null
The major remaining annoyance is that these exceptions get reported in the logs whereas I'd rather they weren't (they represent the system working correctly) but I imagine I have to reconfigure logging to resolve that.