or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

authentication-filters.mdauthorization.mdcsrf-protection.mdfilter-chain.mdfirewall.mdindex.mdlogout.mdreactive-security.mdrequest-matching.mdsaved-requests.mdsecurity-context.mdsecurity-headers.mdservlet-integration.mdsession-management.md
tile.json

csrf-protection.mddocs/

CSRF Protection

Cross-Site Request Forgery (CSRF) protection prevents unauthorized commands from being transmitted from a user that the web application trusts.

Core CSRF Components

CsrfToken

Represents expected CSRF token information.

package org.springframework.security.web.csrf;

public interface CsrfToken {
    /**
     * Gets the HTTP header name for the CSRF token.
     *
     * @return the header name (e.g., "X-CSRF-TOKEN")
     */
    String getHeaderName();

    /**
     * Gets the HTTP parameter name for the CSRF token.
     *
     * @return the parameter name (e.g., "_csrf")
     */
    String getParameterName();

    /**
     * Gets the CSRF token value.
     *
     * @return the token value
     */
    String getToken();
}

DefaultCsrfToken

Default implementation of CsrfToken.

package org.springframework.security.web.csrf;

public final class DefaultCsrfToken implements CsrfToken {
    /**
     * Creates a CSRF token.
     *
     * @param headerName the HTTP header name
     * @param parameterName the HTTP parameter name
     * @param token the token value
     */
    public DefaultCsrfToken(String headerName, String parameterName, String token);

    public String getHeaderName();

    public String getParameterName();

    public String getToken();

    public boolean equals(Object o);

    public int hashCode();
}

CsrfTokenRepository

Manages CSRF token generation, storage, and retrieval.

package org.springframework.security.web.csrf;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

public interface CsrfTokenRepository {
    /**
     * Generates a new CSRF token.
     *
     * @param request the HTTP request
     * @return the generated token
     */
    CsrfToken generateToken(HttpServletRequest request);
    
    /**
     * Saves the CSRF token.
     *
     * @param token the token to save
     * @param request the HTTP request
     * @param response the HTTP response
     */
    void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response);
    
    /**
     * Loads the CSRF token.
     *
     * @param request the HTTP request
     * @return the token or null if not found
     */
    CsrfToken loadToken(HttpServletRequest request);
}

HttpSessionCsrfTokenRepository

Stores CSRF tokens in the HTTP session.

package org.springframework.security.web.csrf;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

public final class HttpSessionCsrfTokenRepository implements CsrfTokenRepository {
    public static final String DEFAULT_CSRF_PARAMETER_NAME = "_csrf";
    public static final String DEFAULT_CSRF_HEADER_NAME = "X-CSRF-TOKEN";
    public static final String DEFAULT_CSRF_TOKEN_ATTR_NAME = "org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository.CSRF_TOKEN";
    
    public HttpSessionCsrfTokenRepository();
    
    public CsrfToken generateToken(HttpServletRequest request);
    
    public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response);
    
    public CsrfToken loadToken(HttpServletRequest request);
    
    /**
     * Sets the HTTP parameter name (default: "_csrf").
     */
    public void setParameterName(String parameterName);
    
    /**
     * Sets the HTTP header name (default: "X-CSRF-TOKEN").
     */
    public void setHeaderName(String headerName);
    
    /**
     * Sets the session attribute name for storing the token.
     */
    public void setSessionAttributeName(String sessionAttributeName);
}

CookieCsrfTokenRepository

Stores CSRF tokens in cookies.

package org.springframework.security.web.csrf;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

public final class CookieCsrfTokenRepository implements CsrfTokenRepository {
    public static final String DEFAULT_CSRF_COOKIE_NAME = "XSRF-TOKEN";
    public static final String DEFAULT_CSRF_PARAMETER_NAME = "_csrf";
    public static final String DEFAULT_CSRF_HEADER_NAME = "X-XSRF-TOKEN";
    
    public CookieCsrfTokenRepository();
    
    public CsrfToken generateToken(HttpServletRequest request);
    
    public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response);
    
    public CsrfToken loadToken(HttpServletRequest request);
    
    /**
     * Creates a repository with HttpOnly cookie disabled.
     * Useful for JavaScript access.
     */
    public static CookieCsrfTokenRepository withHttpOnlyFalse();
    
    /**
     * Sets the cookie name (default: "XSRF-TOKEN").
     */
    public void setCookieName(String cookieName);
    
    /**
     * Sets the parameter name (default: "_csrf").
     */
    public void setParameterName(String parameterName);
    
    /**
     * Sets the header name (default: "X-XSRF-TOKEN").
     */
    public void setHeaderName(String headerName);
    
    /**
     * Sets the cookie path.
     */
    public void setCookiePath(String cookiePath);
    
    /**
     * Sets the cookie domain.
     */
    public void setCookieDomain(String cookieDomain);
    
    /**
     * Sets whether cookie is HTTP-only.
     */
    public void setCookieHttpOnly(boolean cookieHttpOnly);
    
    /**
     * Sets whether cookie is secure (HTTPS only).
     */
    public void setSecure(Boolean secure);
    
    /**
     * Sets the cookie max age in seconds.
     */
    public void setCookieMaxAge(int cookieMaxAge);
}

CSRF Filter

CsrfFilter

Applies CSRF protection to incoming requests.

package org.springframework.security.web.csrf;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;

import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.web.filter.OncePerRequestFilter;

public final class CsrfFilter extends OncePerRequestFilter {
    public static final RequestMatcher DEFAULT_CSRF_MATCHER = new DefaultRequiresCsrfMatcher();
    
    /**
     * Creates a CSRF filter with the given repository.
     */
    public CsrfFilter(CsrfTokenRepository tokenRepository);
    
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException;
    
    /**
     * Sets the matcher for requests requiring CSRF protection.
     * Default protects: POST, PUT, DELETE, PATCH.
     */
    public void setRequireCsrfProtectionMatcher(RequestMatcher requireCsrfProtectionMatcher);
    
    /**
     * Sets the access denied handler for CSRF failures.
     */
    public void setAccessDeniedHandler(AccessDeniedHandler accessDeniedHandler);
    
    /**
     * Sets the CSRF token request handler.
     */
    public void setCsrfTokenRequestHandler(CsrfTokenRequestHandler requestHandler);
}

CSRF Token Handlers

CsrfTokenRequestHandler

Handles CSRF tokens in requests.

package org.springframework.security.web.csrf;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

public interface CsrfTokenRequestHandler {
    /**
     * Handles the CSRF token, making it available to the application.
     *
     * @param request the HTTP request
     * @param response the HTTP response
     * @param csrfToken the CSRF token (may be a DeferredCsrfToken)
     */
    void handle(HttpServletRequest request, HttpServletResponse response,
                Supplier<CsrfToken> csrfToken);
}

CsrfTokenRequestAttributeHandler

Makes CSRF token available as a request attribute.

package org.springframework.security.web.csrf;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.util.function.Supplier;

public class CsrfTokenRequestAttributeHandler implements CsrfTokenRequestHandler {
    public CsrfTokenRequestAttributeHandler();

    public void handle(HttpServletRequest request, HttpServletResponse response,
                       Supplier<CsrfToken> csrfToken);

    /**
     * Sets the request attribute name for the CSRF token.
     * Default: "org.springframework.security.web.csrf.CsrfToken"
     */
    public final void setCsrfRequestAttributeName(String csrfRequestAttributeName);
}

XorCsrfTokenRequestAttributeHandler

CSRF token handler that uses XOR masking for additional protection against BREACH attacks.

package org.springframework.security.web.csrf;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.util.function.Supplier;

public final class XorCsrfTokenRequestAttributeHandler implements CsrfTokenRequestHandler, CsrfTokenRequestResolver {
    public XorCsrfTokenRequestAttributeHandler();

    /**
     * Handles the CSRF token, applying XOR masking.
     */
    public void handle(HttpServletRequest request, HttpServletResponse response,
                       Supplier<CsrfToken> csrfToken);

    /**
     * Resolves the CSRF token value from the request.
     */
    public String resolveCsrfTokenValue(HttpServletRequest request, CsrfToken csrfToken);

    /**
     * Sets the request attribute name for the CSRF token.
     */
    public void setCsrfRequestAttributeName(String csrfRequestAttributeName);
}

CsrfTokenRequestResolver

Resolves the token value from the request.

package org.springframework.security.web.csrf;

import jakarta.servlet.http.HttpServletRequest;

public interface CsrfTokenRequestResolver {
    /**
     * Resolves the CSRF token value from the request.
     *
     * @param request the HTTP request
     * @param csrfToken the expected CSRF token
     * @return the resolved token value or null
     */
    String resolveCsrfTokenValue(HttpServletRequest request, CsrfToken csrfToken);
}

Deferred Token Loading

DeferredCsrfToken

Allows delayed access to a CSRF token.

package org.springframework.security.web.csrf;

import java.util.function.Supplier;

public interface DeferredCsrfToken {
    /**
     * Gets the CSRF token, generating it if necessary.
     *
     * @return the CSRF token
     */
    CsrfToken get();
    
    /**
     * Returns true if get() refers to a generated token.
     *
     * @return true if newly generated
     */
    boolean isGenerated();
}

CSRF Exceptions

CsrfException

Base exception for CSRF-related failures.

package org.springframework.security.web.csrf;

import org.springframework.security.access.AccessDeniedException;

public class CsrfException extends AccessDeniedException {
    public CsrfException(String message);
}

InvalidCsrfTokenException

Thrown when a CSRF token is present but invalid.

package org.springframework.security.web.csrf;

public class InvalidCsrfTokenException extends CsrfException {
    public InvalidCsrfTokenException(CsrfToken expectedToken, String actualToken);
}

MissingCsrfTokenException

Thrown when no CSRF token is found in the request.

package org.springframework.security.web.csrf;

public class MissingCsrfTokenException extends CsrfException {
    public MissingCsrfTokenException(String missingHeaderName);
}

Usage Examples

Basic CSRF Configuration

import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class CsrfConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
            );
        
        return http.build();
    }
}

Disabling CSRF for Specific Endpoints

import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;

@Configuration
public class SelectiveCsrfConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf
                .ignoringRequestMatchers(
                    PathPatternRequestMatcher.pathPattern("/api/**"),
                    PathPatternRequestMatcher.pathPattern("/webhooks/**")
                )
            );

        return http.build();
    }
}

Custom CSRF Token Repository

import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.security.web.csrf.CsrfTokenRepository;
import org.springframework.security.web.csrf.DefaultCsrfToken;

public class RedisCsrfTokenRepository implements CsrfTokenRepository {
    
    private final RedisTemplate<String, String> redisTemplate;
    private final Duration tokenDuration = Duration.ofHours(1);
    
    public RedisCsrfTokenRepository(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }
    
    @Override
    public CsrfToken generateToken(HttpServletRequest request) {
        String token = UUID.randomUUID().toString();
        return new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", token);
    }
    
    @Override
    public void saveToken(CsrfToken token, HttpServletRequest request, 
                          HttpServletResponse response) {
        if (token == null) {
            String sessionId = request.getSession().getId();
            redisTemplate.delete("csrf:" + sessionId);
        } else {
            String sessionId = request.getSession().getId();
            redisTemplate.opsForValue().set(
                "csrf:" + sessionId,
                token.getToken(),
                tokenDuration
            );
        }
    }
    
    @Override
    public CsrfToken loadToken(HttpServletRequest request) {
        String sessionId = request.getSession().getId();
        String token = redisTemplate.opsForValue().get("csrf:" + sessionId);
        
        if (token == null) {
            return null;
        }
        
        return new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", token);
    }
}

Including CSRF Token in Forms

<!-- Thymeleaf -->
<form method="post" th:action="@{/api/submit}">
    <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
    <!-- form fields -->
    <button type="submit">Submit</button>
</form>

<!-- JSP -->
<form method="post" action="/api/submit">
    <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
    <!-- form fields -->
    <button type="submit">Submit</button>
</form>

Including CSRF Token in AJAX Requests

// Using jQuery
$(function () {
    var token = $("meta[name='_csrf']").attr("content");
    var header = $("meta[name='_csrf_header']").attr("content");
    
    $(document).ajaxSend(function(e, xhr, options) {
        xhr.setRequestHeader(header, token);
    });
});

// Using Fetch API
const csrfToken = document.querySelector('meta[name="_csrf"]').content;
const csrfHeader = document.querySelector('meta[name="_csrf_header"]').content;

fetch('/api/data', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        [csrfHeader]: csrfToken
    },
    body: JSON.stringify(data)
});

Error Handling for CSRF Failures

import org.springframework.security.web.csrf.CsrfException;
import org.springframework.security.web.csrf.MissingCsrfTokenException;
import org.springframework.security.web.csrf.InvalidCsrfTokenException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.access.AccessDeniedException;

public class CsrfErrorHandler implements AccessDeniedHandler {
    
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response,
                       AccessDeniedException accessDeniedException) throws IOException {
        
        if (accessDeniedException instanceof CsrfException) {
            CsrfException csrfException = (CsrfException) accessDeniedException;
            
            if (csrfException instanceof MissingCsrfTokenException) {
                response.setStatus(HttpServletResponse.SC_FORBIDDEN);
                response.setContentType("application/json");
                response.getWriter().write(
                    "{\"error\": \"CSRF token missing\", \"message\": \"Please include CSRF token in request\"}"
                );
            } else if (csrfException instanceof InvalidCsrfTokenException) {
                response.setStatus(HttpServletResponse.SC_FORBIDDEN);
                response.setContentType("application/json");
                response.getWriter().write(
                    "{\"error\": \"Invalid CSRF token\", \"message\": \"CSRF token validation failed\"}"
                );
            }
        } else {
            // Handle other access denied cases
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        }
    }
}

// Configuration
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .csrf(csrf -> csrf
            .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
            .accessDeniedHandler(new CsrfErrorHandler())
        );
    return http.build();
}

Stateless API with CSRF Disabled

import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class StatelessApiConfig {
    
    @Bean
    public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
        http
            .securityMatchers(matchers -> matchers
                .requestMatchers("/api/**")
            )
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )
            .csrf(csrf -> csrf.disable()) // CSRF not needed for stateless token-based auth
            .authorizeHttpRequests(authorize -> authorize
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2.jwt()); // JWT-based authentication
        
        return http.build();
    }
}

CSRF Token with Deferred Loading

import org.springframework.security.web.csrf.DeferredCsrfToken;
import org.springframework.security.web.csrf.CsrfToken;

@RestController
public class CsrfTokenController {
    
    @GetMapping("/api/csrf-token")
    public Map<String, String> getCsrfToken(
            @RequestAttribute("org.springframework.security.web.csrf.CsrfToken") 
            DeferredCsrfToken deferredToken) {
        
        CsrfToken token = deferredToken.get();
        
        return Map.of(
            "headerName", token.getHeaderName(),
            "parameterName", token.getParameterName(),
            "token", token.getToken()
        );
    }
}

CSRF Token with Custom Repository and Error Handling

import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.security.web.csrf.CsrfTokenRepository;
import org.springframework.security.web.csrf.DefaultCsrfToken;
import org.springframework.security.web.csrf.CsrfException;
import org.springframework.security.web.csrf.MissingCsrfTokenException;
import org.springframework.security.web.csrf.InvalidCsrfTokenException;
import org.springframework.security.web.access.AccessDeniedHandler;

@Configuration
public class CustomCsrfConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf
                .csrfTokenRepository(customCsrfTokenRepository())
                .accessDeniedHandler(csrfAccessDeniedHandler())
            );
        
        return http.build();
    }
    
    @Bean
    public CsrfTokenRepository customCsrfTokenRepository() {
        return new CsrfTokenRepository() {
            @Override
            public CsrfToken generateToken(HttpServletRequest request) {
                String token = UUID.randomUUID().toString();
                return new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", token);
            }
            
            @Override
            public void saveToken(CsrfToken token, HttpServletRequest request, 
                                HttpServletResponse response) {
                if (token == null) {
                    request.getSession().removeAttribute("CSRF_TOKEN");
                } else {
                    request.getSession().setAttribute("CSRF_TOKEN", token.getToken());
                }
            }
            
            @Override
            public CsrfToken loadToken(HttpServletRequest request) {
                String token = (String) request.getSession().getAttribute("CSRF_TOKEN");
                if (token == null) {
                    return null;
                }
                return new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", token);
            }
        };
    }
    
    @Bean
    public AccessDeniedHandler csrfAccessDeniedHandler() {
        return (request, response, accessDeniedException) -> {
            if (accessDeniedException instanceof CsrfException) {
                CsrfException csrfException = (CsrfException) accessDeniedException;
                
                // Log CSRF violation
                logger.warn("CSRF token violation: {} from {}", 
                    csrfException.getMessage(), 
                    request.getRemoteAddr());
                
                // For AJAX requests, return JSON
                if ("XMLHttpRequest".equals(request.getHeader("X-Requested-With"))) {
                    response.setStatus(HttpServletResponse.SC_FORBIDDEN);
                    response.setContentType("application/json");
                    
                    String errorType = "unknown";
                    if (csrfException instanceof MissingCsrfTokenException) {
                        errorType = "missing_token";
                    } else if (csrfException instanceof InvalidCsrfTokenException) {
                        errorType = "invalid_token";
                    }
                    
                    response.getWriter().write(String.format(
                        "{\"error\": \"CSRF token %s\", \"type\": \"%s\"}",
                        csrfException.getMessage(), errorType
                    ));
                } else {
                    // For regular requests, redirect to error page
                    response.sendRedirect("/error/csrf");
                }
            } else {
                // Handle other access denied exceptions
                response.sendError(HttpServletResponse.SC_FORBIDDEN);
            }
        };
    }
}

CSRF Token with Deferred Loading

import org.springframework.security.web.csrf.DeferredCsrfToken;
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;

@Configuration
public class DeferredCsrfConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
                .csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler())
            );
        
        return http.build();
    }
}

// Using deferred token in controller
@RestController
public class CsrfTokenController {
    
    @GetMapping("/api/csrf-token")
    public Map<String, String> getCsrfToken(
            @RequestAttribute("_csrf") DeferredCsrfToken deferredToken) {
        
        CsrfToken token = deferredToken.get();
        return Map.of(
            "headerName", token.getHeaderName(),
            "parameterName", token.getParameterName(),
            "token", token.getToken()
        );
    }
}

CSRF Protection with Custom Request Matcher

import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.security.web.util.matcher.NegatedRequestMatcher;
import org.springframework.security.web.util.matcher.AndRequestMatcher;
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
import org.springframework.http.HttpMethod;

@Configuration
public class SelectiveCsrfConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf
                .requireCsrfProtectionMatcher(customCsrfMatcher())
            );
        
        return http.build();
    }
    
    private RequestMatcher customCsrfMatcher() {
        // Protect all POST, PUT, DELETE, PATCH except webhooks
        RequestMatcher webhookMatcher = 
            PathPatternRequestMatcher.pathPattern("/webhooks/**");
        
        RequestMatcher stateChangingMethods = new RequestMatcher() {
            @Override
            public boolean matches(HttpServletRequest request) {
                String method = request.getMethod();
                return "POST".equals(method) || 
                       "PUT".equals(method) || 
                       "DELETE".equals(method) || 
                       "PATCH".equals(method);
            }
        };
        
        // Match state-changing methods but exclude webhooks
        return new AndRequestMatcher(
            stateChangingMethods,
            new NegatedRequestMatcher(webhookMatcher)
        );
    }
}

CSRF Token with Redis Storage

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.security.web.csrf.CsrfTokenRepository;

@Component
public class RedisCsrfTokenRepository implements CsrfTokenRepository {
    
    private final RedisTemplate<String, String> redisTemplate;
    private static final String CSRF_TOKEN_PREFIX = "csrf:";
    private static final Duration TOKEN_TTL = Duration.ofHours(1);
    
    public RedisCsrfTokenRepository(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }
    
    @Override
    public CsrfToken generateToken(HttpServletRequest request) {
        String token = UUID.randomUUID().toString();
        return new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", token);
    }
    
    @Override
    public void saveToken(CsrfToken token, HttpServletRequest request, 
                         HttpServletResponse response) {
        String sessionId = getSessionId(request);
        if (token == null) {
            redisTemplate.delete(CSRF_TOKEN_PREFIX + sessionId);
        } else {
            redisTemplate.opsForValue().set(
                CSRF_TOKEN_PREFIX + sessionId,
                token.getToken(),
                TOKEN_TTL
            );
        }
    }
    
    @Override
    public CsrfToken loadToken(HttpServletRequest request) {
        String sessionId = getSessionId(request);
        String token = redisTemplate.opsForValue().get(CSRF_TOKEN_PREFIX + sessionId);
        
        if (token == null) {
            return null;
        }
        
        return new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", token);
    }
    
    private String getSessionId(HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        return session != null ? session.getId() : "anonymous";
    }
}