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(); } }
问题分析
- POST请求变GET:大概率是axios请求被全局拦截器修改,或者前端请求配置存在问题(比如错误设置
withCredentials);另外,后端配置中requestMatchers("/**").permitAll()后接anyRequest().denyAll()逻辑矛盾,导致权限规则失效,可能引发异常重定向。 - 400错误与重定向:Spring Security默认的
/logout端点基于session实现,但你的配置中sessionCreationPolicy设为STATELESS(无状态),服务器不维护会话,默认登出逻辑无法生效,因此会返回400并重定向到登录页。 - 手动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
相关产品推荐
相关产品推荐

