在使用 google 成功进行 oauth2 身份验证后,Spring Boot 后端将 401 发送到 Angular 前端

问题描述 投票:0回答:1

Spring Boot Rest api 在后面,Angular 在前面。 大家好,我在使用 google 成功进行 oauth2 身份验证后遇到了问题。 在 srping boot debug 中我可以读到以下内容: o.s.web.cors.DefaultCorsProcessor :跳过:响应已包含“Access-Control-Allow-Origin”。 然后,401 被发送到 Angular,需要完全身份验证才能访问 /api/user/ 资源,该资源是访问后端用户详细信息的根。 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;
  }
}

安全配置.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;
  }
}

预先感谢您的帮助。

oauth2登录成功导致用户数据成功写入数据库,但可以访问完整的认证资源。

spring spring-security oauth-2.0 angular-material
1个回答
-1
投票

WebSecurityConfigurerAdapter
已弃用,不要使用它(在 spring-boot 3 中甚至不再存在)。改为暴露一个
SecurityFilterChain
bean:

    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) {
        // http configuration
        return http.build();
    }

授权服务器

除非您将资源服务器部署到 Google 云,否则它很可能不会为您提供 JWT。这意味着您必须进行内省:

  • 不同数据中心的资源和授权服务器很难接受(太多的延迟和网络调用)
  • 需要“黑客”才能使用用户信息端点作为内省端点,因为 Google OpenID 配置不会公开内省端点(Google 确实不希望您将他们的授权服务器用于您自己的资源服务器)

出于这个原因,您应该在 Google 之前使用另一台能够进行身份联合的授权服务器。 Keycloak 做得很好:

  • 运行 Keycloak 实例并将其配置为 Google 作为身份提供商
  • 配置您的资源服务器以使用 Keycloak 作为授权服务器(如上面代码中所做的那样)
  • 配置 Angular 以使用 Keycloak 作为授权服务器

我在这里指的是 Keycloak,但大多数严肃的 OIDC 提供商(无论是在本地还是 SaaS,如 Auth0、Okta、Amazon Cognito 等)都支持“社交”登录,并允许 Google 用户登录(以及 Facebook、Github、Tweeter)等)

资源服务器(使用 OAuth2 访问令牌保护的 REST API)

Spring 有一个资源服务器的启动器:

spring-boot-starter-oauth2-resource-server
,我已经围绕它编写了自己的启动器,以进一步推动属性的自动配置。 这里有两个入门教程

基于 JS 的前端(Angular 应用程序)

在浏览器中运行的 JS 应用程序有两种不同的 OAuth2 配置选项:

  • 在 JS 应用程序和资源服务器之间使用 BFF
  • 使 JS 应用程序成为 OAuth2 公共客户端

BFF 模式

前端后端是服务器上配置为 OAuth2 客户端的中间件。

从前端到 BFF 的请求通过会话(cookie)进行保护,从 BFF 到资源服务器的请求通过访问令牌(承载授权标头)进行保护。

BFF 负责在会话中存储令牌,并在将请求从浏览器转发到 REST API(资源服务器)之前,用会话中的 accès 令牌替换会话 cookie。

Spring Cloud Gateway 可以配置为 BFF。 Baeldung 的完整教程

JS 应用程序配置为 OAuth2 公共客户端

使用 OAuth2 客户端库 为此使用 OAuth2 客户端库。我最喜欢的 Angular 是 angular-auth-oidc-client。它将为您节省大量精力:

  • 将用户重定向到授权服务器进行登录
  • 使用授权代码处理从授权服务器重定向回来
  • 用授权代码交换令牌(当然是访问令牌,如果您请求
    offline_access
    openid
    范围,还可以刷新和 ID 令牌)
  • 在过期之前自动刷新访问令牌(如果您有刷新令牌)
  • 自动授权与配置模式匹配的请求(添加
    Authorization
    带有访问令牌的承载标头)
  • 提供 Angular 路线守卫
  • ...
© www.soinside.com 2019 - 2024. All rights reserved.