Google OAuth2认证成功后Spring Boot返回401至Angular前端问题
问题描述
后端采用Spring Boot REST API,前端为Angular框架。完成Google OAuth2认证后,用户数据已存入数据库,但访问后端/api/user/资源时返回401未授权状态码,提示需完全认证。Spring Boot调试日志显示:o.s.web.cors.DefaultCorsProcessor : Skip: response already contains "Access-Control-Allow-Origin"。
相关配置代码如下:
WebConfig.java
import java.util.Locale; import org.springframework.context.MessageSource; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.support.ReloadableResourceBundleMessageSource; import org.springframework.validation.Validator; import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; import org.springframework.web.servlet.LocaleResolver; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.i18n.CookieLocaleResolver; @Configuration public class WebConfig implements WebMvcConfigurer { private final long MAX_AGE_SECS = 3600; @Override public void addCorsMappings(CorsRegistry registry) { registry .addMapping("/**") .allowedOrigins("*") .allowedMethods( "HEAD", "OPTIONS", "GET", "POST", "PUT", "PATCH", "DELETE" ) .maxAge(MAX_AGE_SECS); } @Bean public MessageSource messageSource() { ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource(); messageSource.setBasename("classpath:messages"); messageSource.setDefaultEncoding("UTF-8"); return messageSource; } @Bean public LocaleResolver localeResolver() { final CookieLocaleResolver cookieLocaleResolver = new CookieLocaleResolver(); cookieLocaleResolver.setDefaultLocale(Locale.ENGLISH); return cookieLocaleResolver; } @Override public Validator getValidator() { LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); validator.setValidationMessageSource(messageSource()); return validator; } }
SecurityConfig.java
import java.util.Arrays; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.converter.FormHttpMessageConverter; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.BeanIds; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.oauth2.client.endpoint.DefaultAuthorizationCodeTokenResponseClient; import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; import org.springframework.security.oauth2.client.http.OAuth2ErrorResponseErrorHandler; import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.client.RestTemplate; import com.springboot.dashboard.security.jwt.TokenAuthenticationFilter; import com.springboot.dashboard.security.oauth2.DashBoardOAuth2UserService; import com.springboot.dashboard.security.oauth2.DashBoardOidcUserService; import com.springboot.dashboard.security.oauth2.HttpCookieOAuth2AuthorizationRequestRepository; import com.springboot.dashboard.security.oauth2.OAuth2AccessTokenResponseConverterWithDefaults; import com.springboot.dashboard.security.oauth2.OAuth2AuthenticationFailureHandler; import com.springboot.dashboard.security.oauth2.OAuth2AuthenticationSuccessHandler; @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity( prePostEnabled = true, securedEnabled = true, jsr250Enabled = true ) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsService userDetailsService; @Autowired private DashBoardOAuth2UserService dashBoardOAuth2UserService; @Autowired private DashBoardOidcUserService dashBoardOidcUserService; @Autowired private OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler; @Autowired private OAuth2AuthenticationFailureHandler oAuth2AuthenticationFailureHandler; @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth .userDetailsService(userDetailsService) .passwordEncoder(passwordEncoder()); } @Override protected void configure(HttpSecurity http) throws Exception { http .cors() .and() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .csrf() .disable() .formLogin() .disable() .httpBasic() .disable() .exceptionHandling() .authenticationEntryPoint(new RestAuthenticationEntryPoint()) .and() .authorizeRequests() .antMatchers("/", "/error", "/api/all", "/api/auth/**", "/oauth2/**") .permitAll() .anyRequest() .authenticated() .and() .oauth2Login() .authorizationEndpoint() .authorizationRequestRepository(cookieAuthorizationRequestRepository()) .and() .redirectionEndpoint() .and() .userInfoEndpoint() .oidcUserService(dashBoardOidcUserService) .userService(dashBoardOAuth2UserService) .and() .tokenEndpoint() .accessTokenResponseClient(authorizationCodeTokenResponseClient()) .and() .successHandler(oAuth2AuthenticationSuccessHandler) .failureHandler(oAuth2AuthenticationFailureHandler); // Add our custom Token based authentication filter http.addFilterBefore( tokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class ); } @Bean public TokenAuthenticationFilter tokenAuthenticationFilter() { return new TokenAuthenticationFilter(); } /* * By default, Spring OAuth2 uses * HttpSessionOAuth2AuthorizationRequestRepository to save the authorization * request. But, since our service is stateless, we can't save it in the * session. We'll save the request in a Base64 encoded cookie instead. */ @Bean public HttpCookieOAuth2AuthorizationRequestRepository cookieAuthorizationRequestRepository() { return new HttpCookieOAuth2AuthorizationRequestRepository(); } // This bean is load the user specific data when form login is used. @Override public UserDetailsService userDetailsService() { return userDetailsService; } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(10); } @Bean(BeanIds.AUTHENTICATION_MANAGER) @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } private OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> authorizationCodeTokenResponseClient() { OAuth2AccessTokenResponseHttpMessageConverter tokenResponseHttpMessageConverter = new OAuth2AccessTokenResponseHttpMessageConverter(); tokenResponseHttpMessageConverter.setTokenResponseConverter( new OAuth2AccessTokenResponseConverterWithDefaults() ); RestTemplate restTemplate = new RestTemplate( Arrays.asList( new FormHttpMessageConverter(), tokenResponseHttpMessageConverter ) ); restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler()); DefaultAuthorizationCodeTokenResponseClient tokenResponseClient = new DefaultAuthorizationCodeTokenResponseClient(); tokenResponseClient.setRestOperations(restTemplate); return tokenResponseClient; } }
解决方案
1. 修复CORS重复配置问题
当前同时通过WebConfig的addCorsMappings和Spring Security的.cors()配置CORS,导致重复添加Access-Control-Allow-Origin响应头,触发日志中的跳过提示。
修改步骤:
- 删除
WebConfig中的addCorsMappings方法,统一由Spring Security处理CORS。 - 在
SecurityConfig中添加CORS配置Bean,并关联到HttpSecurity:
@Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); // 替换为你的Angular实际域名,生产环境避免用* config.setAllowedOrigins(Arrays.asList("http://localhost:4200")); config.setAllowedMethods(Arrays.asList("HEAD", "OPTIONS", "GET", "POST", "PUT", "PATCH", "DELETE")); config.setAllowedHeaders(Arrays.asList("*")); config.setAllowCredentials(true); config.setMaxAge(3600L); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return source; }
然后修改configure(HttpSecurity http)中的.cors()部分:
http .cors(cors -> cors.configurationSource(corsConfigurationSource())) .and() // 其余配置保持不变
2. 排查自定义Token认证过滤器有效性
项目依赖TokenAuthenticationFilter处理无状态认证,若过滤器未正确解析凭证,会导致请求无认证信息返回401。
检查点:
- 确认
OAuth2AuthenticationSuccessHandler在认证成功后,正确生成并返回JWT(或其他认证凭证)给前端,且前端后续请求在Authorization头中携带该凭证(格式通常为Bearer {token})。 - 检查
TokenAuthenticationFilter核心逻辑:是否正确从请求提取凭证、验证有效性并设置SecurityContextHolder的认证信息,示例逻辑如下:
@Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String token = extractTokenFromRequest(request); if (token != null && jwtTokenValidator.validateToken(token)) { Authentication auth = jwtTokenParser.getAuthentication(token); SecurityContextHolder.getContext().setAuthentication(auth); } chain.doFilter(request, response); }
- 确认过滤器执行顺序正确,当前配置
addFilterBefore(tokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)已满足要求,需排查过滤器是否因异常导致认证流程中断。
3. 验证权限配置与无状态会话
- 确认
/api/user/**未被antMatchers排除在认证外,当前anyRequest().authenticated()配置正确覆盖该路径。 - 因设置了
sessionCreationPolicy(SessionCreationPolicy.STATELESS),Spring Security不会通过会话保存认证状态,必须确保前端每次请求都正确携带认证凭证,且过滤器能正常处理。
内容的提问来源于stack exchange,提问作者sylla
相关产品推荐
相关产品推荐

