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

Spring Security认证:浏览器不存储JSESSIONID Cookie致401错误

问题:Spring Security REST API认证在浏览器中返回401,Postman正常

我基于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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 07:28:13