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();
}
}