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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.13 08:05:25