Angular对接Spring Security会话认证失败问题排查
问题背景
我有一个Angular前端和Spring后端应用。目前登录时能获取到JSessionId Cookie,但注册时无法获取;请求受保护接口时,尽管前端已带上Cookie,却始终弹出「请登录」弹窗。
登录时,UserService打印的用户认证信息如下:
UsernamePasswordAuthenticationToken [Principal=User(userId=1, name=Maksym Riabov, username=MRiabov, password={bcrypt}$2a$10$W0XJRQdfxV5XXORkr2bTluIHvFetIVBzmVp51l39T5zLCQk12RV1i, company=null, enabled=true), Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=null], Granted Authorities=[ADMIN]]
这里的SessionId为null,这是为什么?
已确认的配置
- Angular所有请求都添加了
{withCredentials: true} - 已查阅官方文档并照搬示例代码,问题仍未解决
相关代码
登录/注册控制器
@GetMapping("/login") public ResponseEntity<String> login() { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); return ResponseEntity.ok("123123"); } @PostMapping("/register") public ResponseEntity<Map<String, String>> register(@RequestBody UserRegisterDto userDto) { //todo check if name taken User user = userMapper.toEntity2(userDto); user.setPassword(passwordEncoder.encode(user.getPassword())); user.setEnabled(true); //todo remove Authority authority = authorityRepository.save(new Authority("ADMIN")); user.setAuthorities(Set.of(authority)); //todo REMOVE!!!! User savedUser = userRepository.save(user); System.out.println("registration works!"); return ResponseEntity.ok(Map.of("result",authority.getAuthority().getAuthority())); }
受保护接口(触发登录弹窗)
@PreAuthorize("hasRole('ADMIN')") @GetMapping("/create") public ResponseEntity<OnboardingPathDto> createOnboardingPath() { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); // erased a bit of code here return ResponseEntity.ok().build(); }
Spring Security配置类
@Component @EnableWebSecurity @EnableMethodSecurity(securedEnabled = true, jsr250Enabled = true) @RequiredArgsConstructor public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http, UserDetailsService userDetailsService) throws Exception { http .csrf().disable().cors().disable() .authorizeHttpRequests() .anyRequest().permitAll() //todo this is unsafe .and().sessionManagement(session -> session. sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) .maximumSessions(1))//to force only one session per user //here I tried sessionManagement to do something, but did it do something? .rememberMe((rememberMe) -> rememberMe.userDetailsService(userDetailsService)) .httpBasic(); return http.build(); } @Bean public AuthenticationManager authenticationManager(DaoAuthenticationProvider daoAuthenticationProvider) throws Exception { return new ProviderManager(daoAuthenticationProvider); } @Bean public DaoAuthenticationProvider prov(PasswordEncoder passwordEncoder, UserDetailsService userDetailService) throws Exception { DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider(); daoAuthenticationProvider.setPasswordEncoder(passwordEncoder); daoAuthenticationProvider.setUserDetailsService(userDetailService); return daoAuthenticationProvider; } @Bean public PasswordEncoder passwordEncoder() { return PasswordEncoderFactories.createDelegatingPasswordEncoder(); } @Bean public HttpSessionEventPublisher httpSessionEventPublisher() {//to force only one session per user return new HttpSessionEventPublisher(); } }
核心疑问
为什么Spring已经配置了会话认证,却无法通过会话完成认证?问题出在哪里?
补充问题
- 直接将Session传递给Angular(非Cookie方式)是否存在安全风险?目前我依赖Cookie实现会话。
- 我打算改用OAuth2认证,是否更合适?
问题分析与解决方案
1. SessionId为null的原因
你看到的SessionId=null是因为认证时还没创建HTTP会话。WebAuthenticationDetails里的SessionId是认证请求发起时已存在的会话ID,但如果认证过程中才首次创建会话,这个值就会是null——这不代表会话没创建,只是认证细节对象初始化时还没拿到会话ID。你可以在登录接口里主动获取request.getSession().getId(),就能看到实际的会话ID。
2. 受保护接口认证失效的核心问题
你的Security配置有几个关键错误:
anyRequest().permitAll()会绕过所有安全校验:包括方法级的@PreAuthorize。因为Spring Security的过滤器链是先执行authorizeHttpRequests,如果所有请求都被允许,后续的方法级安全校验会被忽略(过滤器链不会触发认证流程,导致方法校验时SecurityContext里没有认证信息)。- HttpBasic认证的会话绑定问题:HttpBasic默认是每次请求都携带用户名密码,而不是依赖会话。你虽然配置了
sessionCreationPolicy(IF_REQUIRED),但因为permitAll的存在,登录接口的请求不会触发HttpBasic的认证流程——你写的/login接口只是获取当前认证信息,但并没有实际完成登录动作!HttpBasic的认证是由Spring Security的过滤器自动处理的,不需要自己写登录接口。
修复步骤:
(1)修正Security配置,正确拦截受保护请求
把anyRequest().permitAll()改成仅放行登录、注册等公开接口,其余请求需要认证:
http .csrf().disable() .cors() // 建议不要禁用CORS,而是配置CorsConfigurationSource确保Cookie跨域传递 .and() .authorizeHttpRequests(auth -> auth .requestMatchers("/login", "/register").permitAll() .anyRequest().authenticated() ) .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) .maximumSessions(1) ) .httpBasic() .and() .rememberMe(rememberMe -> rememberMe.userDetailsService(userDetailsService));
跨域场景需补充CORS配置,否则Cookie无法传递:
@Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); config.setAllowedOrigins(List.of("http://localhost:4200")); // 你的Angular地址 config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE")); config.setAllowedHeaders(List.of("*")); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return source; }
(2)移除自定义的/login接口
HttpBasic认证由Spring Security自动处理,当请求需要认证的接口时,浏览器会自动弹出登录弹窗,或者前端在请求时携带Authorization: Basic <base64编码的用户名:密码>头。自定义的/login接口没有实际完成认证,无需保留。
(3)确保方法级安全生效
因为你已经配置了@EnableMethodSecurity,修正过滤器链后,方法上的@PreAuthorize("hasRole('ADMIN')")会在过滤器认证通过后,进一步校验权限。
3. 补充问题解答
- 非Cookie方式传递Session的安全风险:直接传递Session ID(比如放在请求头、URL参数里)风险很高——URL参数会被记录在浏览器历史、服务器日志里;请求头如果没有HTTPS加密,也会被拦截窃取。Cookie是最安全的会话传递方式,只要配置
HttpOnly和Secure属性(Spring Security默认会设置),能有效防止XSS和CSRF攻击。 - 是否改用OAuth2:OAuth2适合分布式系统、第三方登录场景,或者需要令牌授权的多客户端场景(比如小程序、APP)。如果你的应用是单体应用,Session认证足够简单高效;如果未来要做微服务扩展,OAuth2是更好的选择。
内容的提问来源于stack exchange,提问作者MRiabov

