Spring Security认证:浏览器不存储JSESSIONID Cookie致401错误
我基于Spring Security为REST API实现用户名密码认证,自定义控制器将认证信息存入SecurityContext并保存到SecurityContextRepository。Postman登录后调用其他接口正常,但浏览器登录成功后调用接口返回401错误。
观察到登录接口响应返回Set-Cookie: JSESSIONID=...头,Postman能自动携带该Cookie发起请求,但浏览器未存储此Cookie(已通过开发者工具确认)。
登录接口响应头示例
access-control-allow-credentials: true access-control-allow-origin: http://localhost:3000 cache-control: no-cache, no-store, max-age=0, must-revalidate connection: keep-alive content-length: 0 date: Mon, 30 Dec 2024 18:28:02 GMT expires: 0 keep-alive: timeout=60 pragma: no-cache set-cookie: JSESSIONID=E2AE75224A50A04D4E64790FA8C2AE46; Path=/; HttpOnly vary: Origin vary: Access-Control-Request-Method vary: Access-Control-Request-Headers x-content-type-options: nosniff x-frame-options: DENY x-xss-protection: 0
已尝试的无效方案
- 配置CORS允许所有头、方法和凭证
- 手动创建Cookie
- 调整Cookie的Secure和HttpOnly属性
- 更换浏览器
- 在React请求中添加
withCredentials: true
两个应用同域(localhost)但端口不同(Spring:8080,React:3000)。
相关代码
CampaignList.jsx
import { List } from "antd"; import axios from "axios"; import { useLoaderData } from "react-router"; export const loader = async () => { const url = "http://localhost:8080/api/campaigns/get"; try { const response = await axios.get(url, { headers: { withCredentials: true, }, }); console.log(response); return response.data; } catch (err) { alert(err.message); } }; const CampaignList = () => { const campaigns = useLoaderData(); return ( <List header={<div>Кампании</div>} dataSource={campaigns} renderItem={(item) => ( <List.Item> <a href={item}>{item.name}</a> </List.Item> )} /> ); }; export default CampaignList;
SpringConfig.java
@EnableWebSecurity(debug = true) @RequiredArgsConstructor @Configuration(proxyBeanMethods = false) public class SecurityConfig { @Autowired private CorsConfigurationSource corsConfigurationSource; @Autowired private SecurityContextRepository securityContextRepository; @Bean public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception { http .cors( cors -> cors .configurationSource(corsConfigurationSource) ) .csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests(requests -> requests .requestMatchers("/api/users/create", "/auth/login", "/error").permitAll() .anyRequest().authenticated() ) .securityContext(securityContext -> securityContext .securityContextRepository(securityContextRepository) .requireExplicitSave(true) ) .httpBasic(Customizer.withDefaults()); return http.build(); } @Bean public StrictHttpFirewall httpFirewall() { StrictHttpFirewall firewall = new StrictHttpFirewall(); firewall.setAllowUrlEncodedDoubleSlash(true); return firewall; } }
CorsConfig.java
@Configuration public class CorsConfig { @Bean public UrlBasedCorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOrigins(List.of("http://localhost:3000/")); configuration.setAllowedMethods(List.of("*")); configuration.setAllowedHeaders(List.of("*")); configuration.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); return source; } }
AuthenticationController.java
@RestController @RequestMapping("/auth") public class AuthenticationController { @Autowired private AuthenticationManager authenticationManager; @Autowired private SecurityContextRepository securityContextRepository; @PostMapping("/login") public ResponseEntity<?> login( @RequestBody SignInRequest signInRequest, HttpServletRequest request, HttpServletResponse response) { SecurityContext context = SecurityContextHolder.createEmptyContext(); Authentication authentication = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(signInRequest.getUsername(), signInRequest.getPassword()) ); context.setAuthentication(authentication); SecurityContextHolder.setContext(context); securityContextRepository.saveContext(context, request, response); return new ResponseEntity<>(HttpStatus.OK); } }
SecurityContextRepositoryConfig.java
@Configuration public class SecurityContextRepositoryConfig { @Bean public SecurityContextRepository securityContextRepository() { return new HttpSessionSecurityContextRepository(); } }
问题分析与解决方案
1. React请求中withCredentials配置错误
你的axios请求把withCredentials放在了headers对象里,这是错误的——它属于axios的顶级配置属性,不是请求头的一部分。
修改CampaignList.jsx的loader函数:
export const loader = async () => { const url = "http://localhost:8080/api/campaigns/get"; try { const response = await axios.get(url, { withCredentials: true, // 移到顶级配置,移除headers包裹 }); console.log(response); return response.data; } catch (err) { alert(err.message); } };
同时确保登录请求也添加该配置,否则登录接口返回的Cookie不会被浏览器存储。
2. CORS配置中AllowedOrigins的斜杠问题
CorsConfig里setAllowedOrigins(List.of("http://localhost:3000/"))末尾多了斜杠,浏览器发送的Origin是http://localhost:3000(无斜杠),匹配失败会导致浏览器拒绝存储Cookie。
修改CorsConfig.java:
configuration.setAllowedOrigins(List.of("http://localhost:3000")); // 去掉末尾斜杠
3. 显式配置Spring Security的Session规则
在SecurityConfig的http配置中添加Session管理,确保跨端口场景下Session Cookie正常工作:
http // ... 现有配置 .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) .sessionFixation().migrateSession() );
4. 确认登录请求携带withCredentials
如果登录请求是在React中发送的,必须同样添加withCredentials: true,示例:
axios.post("http://localhost:8080/auth/login", loginData, { withCredentials: true });
完成以上修改后,浏览器将正确存储JSESSIONID Cookie,并在后续请求中自动携带,解决401问题。
内容的提问来源于stack exchange,提问作者Vladek

