Post-Authentication URL Restoration in Spring Security

Spring Security's authentication flow includes a mechanism to capture the originally requested URL before redirecting unauthenticated users to a login page. After successful authentication, the framework can automatically redirect the user back to their intended destination.

The RequestCache interface defines the contract for storing and retrieving these intercepted requests:

public interface RequestCache {
    void saveRequest(HttpServletRequest request, HttpServletResponse response);
    
    SavedRequest getRequest(HttpServletRequest request, HttpServletResponse response);
    
    HttpServletRequest getMatchingRequest(HttpServletRequest request, HttpServletResponse response);
    
    void removeRequest(HttpServletRequest request, HttpServletResponse response);
}

Three primary implementations ship with the framework:

  • HttpSessionRequestCache: Persists request metadata within the HTTP session
  • CookieRequestCache: Encodes request details into a browser cookie for stateless applications
  • NullRequestCache: Diasbles request storage entirely, useful for stateless APIs

When an unauthenticated user accesses a protected resource, the security filter chain triggers the caching mechanism. The ExceptionTranslationFilter detects authentication failures and delegates to the configured cache:

public class SecurityExceptionHandler {
    
    private RequestCache urlStorage = new HttpSessionRequestCache();
    
    public void commenceAuthentication(HttpServletRequest req, 
                                       HttpServletResponse resp,
                                       AuthenticationException authException) {
        // Clear any existing authentication context
        SecurityContextHolder.clearContext();
        
        // Persist the current request for post-login retrieval
        urlStorage.saveRequest(req, resp);
        
        // Redirect to authentication endpoint
        authenticationEntryPoint.commence(req, resp, authException);
    }
}

Upon successful credential validation, the SavedRequestAwareAuthenticationSuccessHandler retrieves the stored location and redirects accordingly:

public class RedirectAfterAuthHandler extends SimpleUrlAuthenticationSuccessHandler {
    
    private RequestCache urlStorage = new HttpSessionRequestCache();
    
    @Override
    public void onAuthenticationSuccess(HttpServletRequest req, 
                                        HttpServletResponse resp,
                                        Authentication auth) throws IOException {
        
        SavedRequest originalRequest = urlStorage.getRequest(req, resp);
        
        if (originalRequest == null) {
            super.onAuthenticationSuccess(req, resp, auth);
            return;
        }
        
        String targetParam = getTargetUrlParameter();
        if (isAlwaysUseDefaultTargetUrl() || 
            (targetParam != null && StringUtils.hasText(req.getParameter(targetParam)))) {
            
            urlStorage.removeRequest(req, resp);
            super.onAuthenticationSuccess(req, resp, auth);
            return;
        }
        
        clearAuthenticationAttributes(req);
        
        String destination = originalRequest.getRedirectUrl();
        getRedirectStrategy().sendRedirect(req, resp, destination);
    }
}

For distributed architectures where session affinity isn't guaranteed, implement a custom RequestCache using external storage:

@Component
public class DistributedRequestCache implements RequestCache {
    
    private static final String CACHE_PREFIX = "auth:redirect:";
    
    private RequestMatcher eligibleMatcher = AnyRequestMatcher.INSTANCE;
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    @Override
    public void saveRequest(HttpServletRequest request, HttpServletResponse response) {
        if (!eligibleMatcher.matches(request)) {
            return;
        }
        
        String fullPath = UrlUtils.buildFullRequestUrl(request);
        String encodedPath = Base64.getEncoder().encodeToString(fullPath.getBytes());
        String sessionId = request.getSession().getId();
        
        redisTemplate.opsForValue().set(
            CACHE_PREFIX + sessionId, 
            encodedPath, 
            Duration.ofMinutes(30)
        );
    }
    
    @Override
    public SavedRequest getRequest(HttpServletRequest request, HttpServletResponse response) {
        String sessionId = request.getSession().getId();
        String cachedValue = redisTemplate.opsForValue().get(CACHE_PREFIX + sessionId);
        
        if (!StringUtils.hasLength(cachedValue)) {
            return null;
        }
        
        String decodedUrl = new String(Base64.getDecoder().decode(cachedValue));
        UriComponents components = UriComponentsBuilder.fromUriString(decodedUrl).build();
        
        return new DefaultSavedRequest.Builder()
            .setScheme(components.getScheme())
            .setServerName(components.getHost())
            .setServerPort(resolvePort(components))
            .setRequestURI(components.getPath())
            .setQueryString(components.getQuery())
            .setMethod(request.getMethod())
            .build();
    }
    
    @Override
    public void removeRequest(HttpServletRequest request, HttpServletResponse response) {
        String sessionId = request.getSession().getId();
        redisTemplate.delete(CACHE_PREFIX + sessionId);
    }
    
    @Override
    public HttpServletRequest getMatchingRequest(HttpServletRequest request, 
                                                  HttpServletResponse response) {
        SavedRequest saved = getRequest(request, response);
        if (saved == null) {
            return null;
        }
        
        if (!urlMatches(request, saved)) {
            return null;
        }
        
        removeRequest(request, response);
        return request;
    }
    
    private boolean urlMatches(HttpServletRequest current, SavedRequest stored) {
        String currentUrl = UrlUtils.buildFullRequestUrl(current);
        return stored.getRedirectUrl().equals(currentUrl);
    }
    
    private int resolvePort(UriComponents components) {
        int port = components.getPort();
        if (port != -1) return port;
        return components.getScheme().equalsIgnoreCase("https") ? 443 : 80;
    }
}

Register the custom implementation through the security configuration:

@Configuration
public class WebSecurityConfig {
    
    @Autowired
    private DistributedRequestCache distributedCache;
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.requestCache(cache -> 
            cache.requestCache(distributedCache)
        );
        return http.build();
    }
}

Tags: Spring Security Authentication java web development Request Caching

Posted on Thu, 07 May 2026 04:09:25 +0000 by jonabomer