If your web application uses a form-based login through Spring security, and the same application uses ajax, you likely have a problem. Say your user opens two tabs for your application in the browser and logs out from one of them, while the other is periodically updating via ajax. Or, say your user comes back after lunch, to a page whose session has long timed out, and does something that attempts ajax. Or, say the user just pushes the Back button after logging out or timing out, and that pages uses some ajax. The default behavior is to treat the ajax request like any other web page and redirect to the login form. Not good.
Aside from the obvious absurdity, there are some real problems. First, it is not straightforward for your JavaScript code to determine what went wrong; it just sees a redirect followed by a successful GET of the login page, which looks fine and dandy apart from being HTML instead of JSON. Second, if the user, in another tab, tries to access a secured page, and an ajax call hits while they are typing in their password, the user will end up looking at a page full of JSON instead of their intended destination. That’s because Spring Security, by default, determines the destination after login from whatever was most recently stored in the session. (Really? The session??? I guess that’s because if you submit a form on an expired session, you don’t want to lose all your typing, but there’s no better place to keep the form data.)
Umar offers a solution based upon writing a custom ExceptionTranslationFilter
, which ends up being really cumbersome because the <http>
security namespace does not play nicely with it and has to be expanded into all its gory innards.
My solution is similar but overcomes this difficulty. The two steps are to configure the request cache to reject ajax requests and to replace the authentication entry point with one that rejects ajax requests.
<bean id="nonAjaxRequestMatcher" class="org.example.NonAjaxRequestMatcher" /> <bean id="loginUrlAuthenticationEntryPoint" class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint"> <constructor-arg value="/login" /> </bean> <bean id="ajaxAuthenticationEntryPoint" class="org.springframework.security.web.authentication.Http403ForbiddenEntryPoint" /> <bean id="authenticationRequestCache" class="org.springframework.security.web.savedrequest.HttpSessionRequestCache"> <property name="requestMatcher" ref="nonAjaxRequestMatcher" /> </bean> <bean id="authenticationEntryPoint" class="org.springframework.security.web.authentication.DelegatingAuthenticationEntryPoint"> <constructor-arg> <map> <entry key-ref="nonAjaxRequestMatcher" value-ref="loginUrlAuthenticationEntryPoint" /> </map> </constructor-arg> <property name="defaultEntryPoint" ref="ajaxAuthenticationEntryPoint" /> </bean> <security:http entry-point-ref="authenticationEntryPoint" ...> <security:request-cache ref="authenticationRequestCache" /> ... </security:http>
This works by defining a criterion nonAjaxRequestMatcher
that will identify only non-ajax pages that should be redirected as needed to the login page. This can be plugged directly into the authentication request cache. The authentication entry point is a bit more complicated, but it can all be set up in Spring as shown. A DelegatingAuthenticationEntryPoint
is used to ask which type of request we’re dealing with and then send interactive requests to a standard loginUrlAuthenticationEntryPoint
and ajax requests to ajaxAuthenticationEntryPoint
, which returns 403 Forbidden.
The NonAjaxRequestMatcher
can be implemented simply:
public class NonAjaxRequestMatcher implements RequestMatcher { @Override public boolean matches(HttpServletRequest request) { return !"XmlHttpRequest".equalsIgnoreCase(request.getHeader("X-Requested-With")); } }
This bit of magic relies on a convention built into most modern JavaScript frameworks (Dojo, JQuery, etc.) to identify ajax requests by a special header.
Lastly, you can also go one step further and write a custom replacement for Http403ForbiddenEntryPoint
that will return JSON instead of HTML, as many JavaScript frameworks seem surprised when you say to expect JSON and the error page has HTML instead (!).
2012-11-02 at 06:29
Reblogged this on Sutoprise Avenue, A SutoCom Source.
2013-02-14 at 20:41
Another nice one on Spring Security with AJAX and JSON
http://www.cavalr.com/blog/Spring_MVC_-_Spring_Security_with_AJAX_and_JSON
2014-06-24 at 11:50
This is a great solution! Thanks a lot for sharing this.
2017-02-13 at 22:48
Nice and Neat solution
Pingback: ajax - Printemps de sécurité + Ajax d'expiration de la session problème