Detect Session Timeout in Ajax Request in Spring MVC
Asked Answered
G

4

23

I can't see seem to find a good example/answer on how to send back some data from an ajax request when a session has timed out. It sends back the login page HTML and I want to either send json or a status code I can intercept.

Giaimo answered 10/2, 2011 at 23:56 Comment(0)
G
11

The simplest way for doing this is using a filter on URLs of your AJAX requests.

In the example below I'm just sending HTTP 500 response code with a response body indicating the session timeout, but you can easily set the response code and body to what is more suitable for your case..

package com.myapp.security.authentication;

import org.springframework.web.filter.GenericFilterBean;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class ExpiredSessionFilter extends GenericFilterBean {

    static final String FILTER_APPLIED = "__spring_security_expired_session_filter_applied";

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        if (request.getAttribute(FILTER_APPLIED) != null) {
            chain.doFilter(request, response);
            return;
        }

        request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
        if (request.getRequestedSessionId() != null && !request.isRequestedSessionIdValid()) {               
            response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "SESSION_TIMED_OUT");
            return;
        }

        chain.doFilter(request, response);
    }
}
Georgenegeorges answered 11/2, 2011 at 8:20 Comment(7)
Where do I add this in the bean configuration file? I tried this earlier and had issues with it. I am not sure if I configured it right the first time. This seems to be on the right track.Giaimo
Assuming you have Spring Security properly configured, you just need to add this filter into the security filters chain in your applicationContext-security.xml: define your filter as a <bean> and add it to the chain using <security:custom-filter>Georgenegeorges
In your opinion what is the best place to inject this filter with the other filters. The custom filter attribute has a before/after that contains built in filters, static.springsource.org/spring-security/site/docs/3.0.x/…. I don't want it to run on every page via ajax, but only on pages that are secured like an admin section. I definitely have it hitting the filter now, just need to make sure it only runs on thosr secured pages. After that you got an ANSWER check from me!Giaimo
Starting from Spring Security 3.1 you can define separate configuration only for URLs you'r interested in (using pattern attribute of <security:http>, as described in static.springsource.org/spring-security/site/docs/3.1.x/…). In version 3.0, however, there is no namespace support for this. As alternative you can check the URL match inside the doFilter and that decide whether to apply it. If you have multiple URL patterns to deal with I'd suggest creating a FilterWrapper class for this.Georgenegeorges
Boris, I really liked your approach and tried to implement it but isRequestedSessionIdValid is still true even after I log out. Any idea what could cause the session to still be valid after logging out? If I try to access any secured page, I do get a redirect to the login, so my security settings workOrthocephalic
Martin, you need to make sure HttpSession.invalidate method is called when you log out. You can do this either by using <security:logout invalidate-session="true"> (this causes SecurityContextLogoutHandler to invalidate your session on logout) or by implementing a custom filter/handler that invalidates the session.Georgenegeorges
I'm curious why you need the FILTER_APPLIED set/check.Hike
M
3

Here's an approach that I think is quite simple. It's a combination of approaches that I've observed on this site. I wrote a blog post about it: http://yoyar.com/blog/2012/06/dealing-with-the-spring-security-ajax-session-timeout-problem/

The basic idea is to use an api url prefix (i.e. /api/secured) as suggested above along with an authentication entry point. It's simple and works.

Here's the authentication entry point:

package com.yoyar.yaya.config;

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;

import javax.servlet.ServletException;
import javax.servlet.http.*;
import java.io.IOException;

public class AjaxAwareAuthenticationEntryPoint 
             extends LoginUrlAuthenticationEntryPoint {

    public AjaxAwareAuthenticationEntryPoint(String loginUrl) {
        super(loginUrl);
    }

    @Override
    public void commence(
        HttpServletRequest request, 
        HttpServletResponse response, 
        AuthenticationException authException) 
            throws IOException, ServletException {

        boolean isAjax 
            = request.getRequestURI().startsWith("/api/secured");

        if (isAjax) {
            response.sendError(403, "Forbidden");
        } else {
            super.commence(request, response, authException);
        }
    }
}

And here's what goes in your spring context xml:

<bean id="authenticationEntryPoint"
  class="com.yoyar.yaya.config.AjaxAwareAuthenticationEntryPoint">
    <constructor-arg name="loginUrl" value="/login"/>
</bean>

<security:http auto-config="true"
  use-expressions="true"
  entry-point-ref="authenticationEntryPoint">
    <security:intercept-url pattern="/api/secured/**" access="hasRole('ROLE_USER')"/>
    <security:intercept-url pattern="/login" access="permitAll"/>
    <security:intercept-url pattern="/logout" access="permitAll"/>
    <security:intercept-url pattern="/denied" access="hasRole('ROLE_USER')"/>
    <security:intercept-url pattern="/" access="permitAll"/>
    <security:form-login login-page="/login"
                         authentication-failure-url="/loginfailed"
                         default-target-url="/login/success"/>
    <security:access-denied-handler error-page="/denied"/>
    <security:logout invalidate-session="true"
                     logout-success-url="/logout/success"
                     logout-url="/logout"/>
</security:http>
Morten answered 19/6, 2012 at 17:9 Comment(1)
Looks like the linked site is DOA.Boss
F
1

I use the same solution by @Matt in backend. If you're using angularJs on front end, add below interceptor in angular $http to let browser actually do a redirect to login page.

var HttpInterceptorModule = angular.module('httpInterceptor', [])
.config(function ($httpProvider) {
  $httpProvider.interceptors.push('myInterceptor');
  $httpProvider.defaults.headers.common["X-Requested-With"] = 'XMLHttpRequest'; 
})
 .factory('myInterceptor', function ($q) {
return {
    'responseError': function(rejection) {
      // do something on error
        if(rejection.status == 403 || rejection.status == 401) window.location = "login";   
        return $q.reject(rejection);
    }
  };

});

Note that below line is needed only if you're using AngularJs after version 1.1.1 (angularJS removed header "X-Requested-With" from that version onward)

$httpProvider.defaults.headers.common["X-Requested-With"] = 'XMLHttpRequest';
Fernand answered 13/11, 2013 at 8:50 Comment(0)
R
1

Seeing as all of the present answers are a few years old now, I'll share my solution which I currently have working in a Spring Boot REST application:

@Configuration
@EnableWebSecurity
public class UISecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        ...
        http.exceptionHandling.authenticationEntryPoint(authenticationEntryPoint());
        ...
    }

    private AuthenticationEntryPoint authenticationEntryPoint() {
        // As a REST service there is no 'authentication entry point' like MVC which can redirect to a login page
        // Instead just reply with 401 - Unauthorized
        return (request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
    }
}

The basic premise here is that I override the authentication entry point which by default was issuing a redirect to my non-existent login page. It now responds by sending a 401. Spring also implicitly creates an standard error response JSON object that it returns as well.

Ravelment answered 9/12, 2015 at 22:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.