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

React+Spring Boot登出异常:调用POST请求却触发GET请求

React + Spring Boot 登出功能问题排查与解决

问题现象

  • 前端通过axios发起POST请求到/logout,但浏览器日志显示实际发送的是GET请求
  • Postman测试POST /logout返回400错误,请求路径被重定向到/login
  • 手动访问localhost:8080/logout会被重定向到localhost:8080/login?logout,已删除自定义登出逻辑

相关代码

React 前端代码

import React from 'react';
import axios from "axios";
import { useState } from 'react';

function Logout() {
    const [userIsLoggedIn, setUserIsLoggedIn] = useState(false);

    const handleLogout = async () => {
        try {
            await axios.post('http://localhost:8080/logout');
            localStorage.removeItem('jwtToken');
            setUserIsLoggedIn(false);
        } catch (error) {
            console.error('Error during logout:', error);
        }
    };

    return (
        <div>
            <button onClick={handleLogout}>Log uit</button>
        </div>
    );
}

export default Logout;

Spring Security 配置代码

package com.example.solarproject.config;

import com.example.solarproject.filter.JwtRequestFilter;
import com.example.solarproject.service.CustomUserDetailsService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
public class SpringSecurityConfig {

    public final CustomUserDetailsService customUserDetailsService;
    private final JwtRequestFilter jwtRequestFilter;

    public SpringSecurityConfig(CustomUserDetailsService customUserDetailsService, JwtRequestFilter jwtRequestFilter) {
        this.customUserDetailsService = customUserDetailsService;
        this.jwtRequestFilter = jwtRequestFilter;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
        return http.getSharedObject(AuthenticationManagerBuilder.class)
                .userDetailsService(customUserDetailsService)
                .passwordEncoder(passwordEncoder())
                .and()
                .build();
    }

    @Bean
    protected SecurityFilterChain filter (HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .httpBasic().disable()
                .cors().and()
                .authorizeHttpRequests()
                .requestMatchers("/**").permitAll()
                .requestMatchers(HttpMethod.POST, "/users").permitAll()
                .anyRequest().denyAll()
                .and()
                .logout()
                .logoutSuccessUrl("/")
                .permitAll()
                .and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }
}

问题分析

  1. POST请求变GET:大概率是axios请求被全局拦截器修改,或者前端请求配置存在问题(比如错误设置withCredentials);另外,后端配置中requestMatchers("/**").permitAll()后接anyRequest().denyAll()逻辑矛盾,导致权限规则失效,可能引发异常重定向。
  2. 400错误与重定向:Spring Security默认的/logout端点基于session实现,但你的配置中sessionCreationPolicy设为STATELESS(无状态),服务器不维护会话,默认登出逻辑无法生效,因此会返回400并重定向到登录页。
  3. 手动GET访问/logout:Spring Security默认只允许POST请求访问登出端点,GET访问会触发重定向,这是默认行为,但不适用于JWT无状态认证场景。

解决方案

1. 修正前端请求配置

确保axios发送POST请求时无额外干扰,可显式配置withCredentials: false(JWT存储在localStorage,无需携带cookie),同时检查是否存在全局axios拦截器修改请求方法:

const handleLogout = async () => {
    try {
        await axios.post('http://localhost:8080/logout', {}, {
            withCredentials: false // 无状态认证无需携带cookie
        });
        localStorage.removeItem('jwtToken');
        setUserIsLoggedIn(false);
    } catch (error) {
        console.error('Error during logout:', error);
    }
};

2. 调整Spring Security配置

移除默认的session-based登出逻辑,适配JWT无状态场景,同时修正权限规则:

@Bean
protected SecurityFilterChain filter(HttpSecurity http) throws Exception {
    http
            .csrf().disable()
            .httpBasic().disable()
            .cors().and()
            .authorizeHttpRequests(auth -> auth
                    .requestMatchers(HttpMethod.POST, "/users", "/logout").permitAll() // 允许公共端点
                    .anyRequest().authenticated() // 其他请求需认证
            )
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
    return http.build();
}

3. 自定义JWT登出逻辑

由于无状态场景下服务器不存储会话,需通过JWT黑名单机制实现登出(示例用Redis存储黑名单,需提前配置Redis依赖):

@RestController
public class AuthController {

    private final StringRedisTemplate redisTemplate;
    // JWT过期时间,需与生成token时的配置一致
    private static final long JWT_EXPIRATION = 86400000L;

    public AuthController(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @PostMapping("/logout")
    public ResponseEntity<String> logout(HttpServletRequest request) {
        String authHeader = request.getHeader("Authorization");
        if (authHeader != null && authHeader.startsWith("Bearer ")) {
            String token = authHeader.substring(7);
            // 将token加入黑名单,有效期设为JWT剩余过期时间
            redisTemplate.opsForValue().set("blacklist:" + token, "invalid", JWT_EXPIRATION, TimeUnit.MILLISECONDS);
        }
        return ResponseEntity.ok("Logout successful");
    }
}

4. 更新JWT过滤器验证逻辑

在JwtRequestFilter中添加黑名单检查,确保已登出的token无法继续使用:

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
    String authHeader = request.getHeader("Authorization");
    String username = null;
    String jwtToken = null;

    if (authHeader != null && authHeader.startsWith("Bearer ")) {
        jwtToken = authHeader.substring(7);
        // 检查token是否在黑名单中
        if (redisTemplate.hasKey("blacklist:" + jwtToken)) {
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Token has been invalidated");
            return;
        }
        // 原有JWT解析逻辑...
    }
    // 后续过滤器逻辑...
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.12 23:54:59