You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Springboot OAuth2启用CSRF后登出提示XSRF Token不匹配求助

Spring Boot OAuth2 登出时CSRF Token验证失败问题

环境配置

1. application.yml 配置

spring:
  security:
    oauth2:
      client:
        registration:
          github:
            clientId: ***
            clientSecret: ***

2. Gradle 依赖

implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.springframework.boot:spring-boot-starter-web'

3. 安全过滤器链配置

@Slf4j
@Configuration
@EnableWebSecurity
public class OAuth2LoginSecurityConfig {

    @Autowired
    private CustomOAuth2AuthenticationFailureHandler failureHandler;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        CookieCsrfTokenRepository cRepo = CookieCsrfTokenRepository.withHttpOnlyFalse();
        http
            .addFilterBefore(new PrintCsrfTokenFilter(cRepo), CsrfFilter.class)
            .csrf(csrf ->  csrf.csrfTokenRepository(cRepo))
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers("/", "/error", "/webjars/**","/index.html").permitAll()
                .anyRequest().authenticated()
            )
            .logout((logout) -> logout.logoutSuccessUrl("/").permitAll())
            .oauth2Login(t -> t.failureHandler((request, response, exception) -> {
                log.error(exception.getMessage());
                request.getSession().setAttribute("error.message", exception.getMessage());
                failureHandler.onAuthenticationFailure(request, response, exception);
            }));
        return http.build();
    }
}

4. CSRF 调试过滤器

@Slf4j
public final class PrintCsrfTokenFilter extends OncePerRequestFilter {

    private CsrfTokenRepository tokenRepository;

    public PrintCsrfTokenFilter(CsrfTokenRepository csrfTokenRepository) {
        Assert.notNull(csrfTokenRepository, "csrfTokenRepository cannot be null");
        this.tokenRepository = csrfTokenRepository;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        DeferredCsrfToken deferredCsrfToken = this.tokenRepository.loadDeferredToken(request, response);
        CsrfToken csrfToken = deferredCsrfToken.get();
        String actualToken = this.resolveCsrfTokenValue(request, csrfToken);
        log.info("csrfToken: {} , actualToken: {}", csrfToken.getToken(), actualToken);

        String xcsrfToken = request.getHeader("X-XSRF-TOKEN");
        log.info("xcsrfToken Token: " + xcsrfToken);

        filterChain.doFilter(request, response);
    }

    private String resolveCsrfTokenValue(HttpServletRequest request, CsrfToken csrfToken) {
        Assert.notNull(request, "request cannot be null");
        Assert.notNull(csrfToken, "csrfToken cannot be null");
        String actualToken = request.getHeader(csrfToken.getHeaderName());
        log.info("header {}: {} ", csrfToken.getHeaderName(), actualToken);
        if (actualToken == null) {
            actualToken = request.getParameter(csrfToken.getParameterName());
            log.info("param {}: {} ", csrfToken.getParameterName(), actualToken);
        }
        return actualToken;
    }
}

5. 前端页面代码

<body>
<script type="text/javascript">
// Declare logout function in the global scope
window.logout = function() {
    $.post("/logout", function() {
        $("#user").html('');
        $(".unauthenticated").show();
        $(".authenticated").hide();
    })
    return true;
}

$.ajaxSetup({
  beforeSend : function(xhr, settings) {
    if (settings.type == 'POST' || settings.type == 'PUT'
        || settings.type == 'DELETE') {
      if (!(/^http:.*/.test(settings.url) || /^https:.*/
        .test(settings.url))) {
        // Only send the token to relative URLs i.e. locally.
        xhr.setRequestHeader("X-XSRF-TOKEN",
          Cookies.get('XSRF-TOKEN'));
      }
    }
  }
});

$(document).ready(function() {
    $.get("/user", function(data, status, xhr) {
        if (xhr.status == 200 && data.name != null && data.name != "") {
            $("#user").html(data.name);
            $(".unauthenticated").hide();
            $(".authenticated").show();
        } else {
            $(".authenticated").hide();
            $(".unauthenticated").show();
        }
    }).fail(function() {
        $(".authenticated").hide();
        $(".unauthenticated").show();
    });
});

</script>

<div class="container">
    <h1>Demo</h1>
    <div class="container unauthenticated">
        With GitHub: <a href="/oauth2/authorization/github">click here</a>
    </div>
    <div class="container authenticated">
        Logged in as: <span id="user"></span>
        <div>
            <button onClick="logout()" class="btn btn-primary">Logout</button>
        </div>
    </div>
</div>
</body>

问题现象

已实现GitHub OAuth2认证,但执行登出操作时始终提示CSRF Token无效,关键日志如下:

2023-07-31T18:17:31.904+08:00  INFO 10277 --- [nio-8080-exec-9] c.mark.oauth2.Demo.PrintCsrfTokenFilter  : csrfToken: afc6a080-363c-4d9f-87e2-187314baf11a , actualToken: afc6a080-363c-4d9f-87e2-187314baf11a
2023-07-31T18:17:31.905+08:00 DEBUG 10277 --- [nio-8080-exec-9] o.s.security.web.csrf.CsrfFilter         : Invalid CSRF token found for http://localhost:8080/logout

通过断点调试确认,actualToken与csrfToken.getToken()的值完全一致,但Spring Security的CsrfFilter仍判定Token无效。

浏览器请求情况:

  • 登出请求详情:浏览器登出请求情况
  • 请求头信息:请求头信息

求助需求

希望排查OAuth2配置中是否存在遗漏,解决登出时的CSRF Token验证失败问题,后续会更新解决方案供大家参考学习。

内容的提问来源于stack exchange,提问作者mark ortiz

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.14 10:24:52