Spring Boot 2升级至3后认证接口未返回Set-Cookie引发401问题
问题描述
将Angular+Spring Boot 2.x.x应用升级至Spring Boot 3.x.x后,通过Basic Auth头发起的用户端点REST请求认证成功返回200状态码,但响应中缺少Set-Cookie头。由于未设置会话Cookie,后续所有受保护的REST请求均返回401状态码。
在Spring Boot 2.x.x版本中,该请求会正常返回Set-Cookie,此实现方案升级前一直正常工作。
以下是认证请求的请求/响应头:
===> 请求头
GET /api/users/current HTTP/1.1 Host: retrospect.wspfeiffer.com:8443 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/110.0 Accept: application/json, text/plain, */* Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate, br authorization: Basic XXXXXXXXXXXXXXXXXXXX Access-Control-Allow-Origin: http://localhost:4200 X-Requested-With: XMLHttpRequest Connection: keep-alive Referer: https://retrospect.wspfeiffer.com:8443/login Cookie: SESSION=N2IzZmU4MDEtZjUwMC00MGU1LTllNDItMzVjODkzZWY1NGE2 Sec-Fetch-Dest: empty Sec-Fetch-Mode: cors Sec-Fetch-Site: same-origin Pragma: no-cache Cache-Control: no-cache
===> 响应头
HTTP/1.1 200 Vary: Origin Vary: Access-Control-Request-Method Vary: Access-Control-Request-Headers Cache-Control: no-cache X-Content-Type-Options: nosniff X-XSS-Protection: 0 Strict-Transport-Security: max-age=31536000 ; includeSubDomains Content-Type: application/json Transfer-Encoding: chunked Date: Wed, 08 Mar 2023 14:48:29 GMT Keep-Alive: timeout=60 Connection: keep-alive
安全配置代码如下:
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.DelegatingPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.StandardPasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.csrf.CookieCsrfTokenRepository; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.filter.CorsFilter; import java.util.HashMap; import java.util.Map; import static org.springframework.security.config.Customizer.withDefaults; @Configuration @EnableWebSecurity(debug = true) @EnableMethodSecurity(prePostEnabled = true) public class WebSecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth .requestMatchers(HttpMethod.OPTIONS).permitAll() .requestMatchers("/open/api/**").permitAll() .requestMatchers("/index.html").permitAll() .requestMatchers(HttpMethod.GET, "", "/", "/*.html", "/favicon.ico", "/*.css", "/*.js", "/*.map", "/assets/**", "/error").permitAll() .requestMatchers("/api/**").authenticated() .anyRequest().authenticated()) .httpBasic() .and() .cors() .and() .csrf() .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) .and() .logout() .logoutUrl("/logout"); http.headers().cacheControl().disable(); http.headers().frameOptions().disable(); return http.build(); } @Bean public CorsFilter corsFilter() { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); CorsConfiguration config = new CorsConfiguration(); config.setAllowCredentials(true); config.addAllowedOrigin("http://localhost:4200"); config.addAllowedHeader("*"); config.addAllowedMethod("OPTIONS"); config.addAllowedMethod("GET"); config.addAllowedMethod("POST"); config.addAllowedMethod("PUT"); config.addAllowedMethod("DELETE"); config.addExposedHeader("Content-Type"); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); } @Bean public PasswordEncoder passwordEncoder() { String idForEncode = "bcrypt"; Map<String,PasswordEncoder> encoders = new HashMap<>(); encoders.put(idForEncode, new BCryptPasswordEncoder()); encoders.put("sha256", new StandardPasswordEncoder()); return new DelegatingPasswordEncoder(idForEncode, encoders); } }
解决方案
问题根源在于Spring Boot 3.x.x对应的Spring Security 6.x版本中,HttpBasic认证默认不再主动创建会话,而Spring Boot 2.x.x中的Spring Security 5.x默认会创建会话。要恢复原有行为,需显式配置会话创建策略。
修改安全配置
在filterChain方法中添加会话管理配置,指定会话创建策略为IF_REQUIRED(和Spring Boot 2.x默认行为一致):
@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth .requestMatchers(HttpMethod.OPTIONS).permitAll() .requestMatchers("/open/api/**").permitAll() .requestMatchers("/index.html").permitAll() .requestMatchers(HttpMethod.GET, "", "/", "/*.html", "/favicon.ico", "/*.css", "/*.js", "/*.map", "/assets/**", "/error").permitAll() .requestMatchers("/api/**").authenticated() .anyRequest().authenticated()) .httpBasic() .and() // 新增会话管理配置 .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) ) .cors() .and() .csrf() .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) .and() .logout() .logoutUrl("/logout"); http.headers().cacheControl().disable(); http.headers().frameOptions().disable(); return http.build(); }
同时需要导入SessionCreationPolicy类:
import org.springframework.security.config.http.SessionCreationPolicy;
补充说明
SessionCreationPolicy.IF_REQUIRED:仅在需要时创建会话,和Spring Boot 2.x默认逻辑一致- 如果需要强制每次认证都创建新会话,可替换为
SessionCreationPolicy.ALWAYS - 需确保前端请求配置了
withCredentials: true(Angular中HttpClient请求时设置),否则Cookie无法被正确保存和发送
内容的提问来源于stack exchange,提问作者Bill Pfeiffer
相关产品推荐
相关产品推荐

