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

如何为不同类型用户请求不同OAuth2权限范围集合?

问题描述

现有React前端+Spring Boot后端应用,通过Spring Security的OAuth2授权码流实现Google和Azure用户认证。当前仅支持一类用户(USER1),需要请求邮箱、日历等权限,功能正常运行。

现在新增USER2用户类型,这类用户仅需获取基本信息,无需日历等集成功能,需通过React应用的不同页面/按钮触发Google/Azure登录,且登录时请求的权限范围与USER1不同。需要在Spring Security中配置实现两种用户触发登录时请求不同的权限集合。

当前安全配置代码:

@EnableWebSecurity
public class OAuth2LoginSecurityConfig {

    private final long MAX_AGE = 3600;
    private final CustomOAuth2UserService customOAuth2UserService; //用于Google认证
    private final CustomOidcUserService customOidcUserService; //用于Azure认证
    
    private final MyUserRepository myUserRepository;
    private final OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler;
    private final OAuth2AuthenticationFailureHandler oAuth2AuthenticationFailureHandler;
    private final AppConfig appConfig;

    public OAuth2LoginSecurityConfig(CustomOAuth2UserService customOAuth2UserService, CustomOidcUserService customOidcUserService, MyUserRepository myUserRepository, OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler, OAuth2AuthenticationFailureHandler oAuth2AuthenticationFailureHandler, AppConfig appConfig) {
        this.customOAuth2UserService = customOAuth2UserService;
        this.customOidcUserService = customOidcUserService;
        this.myUserRepository = myUserRepository;
        this.oAuth2AuthenticationSuccessHandler = oAuth2AuthenticationSuccessHandler;
        this.oAuth2AuthenticationFailureHandler = oAuth2AuthenticationFailureHandler;
        this.appConfig = appConfig;
    }

    @Bean
    public HttpCookieOAuth2AuthorizationRequestRepository cookieAuthorizationRequestRepository() {
        return new HttpCookieOAuth2AuthorizationRequestRepository();
    }

    @Bean
    public AuthenticateRequestsFilter tokenAuthenticationFilter() {
        return new AuthenticateRequestsFilter(myUserRepository);
    }

    @Bean
    public OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient() {
        DefaultAuthorizationCodeTokenResponseClient accessTokenResponseClient =
                new DefaultAuthorizationCodeTokenResponseClient();

        OAuth2AccessTokenResponseHttpMessageConverter tokenResponseHttpMessageConverter =
                new OAuth2AccessTokenResponseHttpMessageConverter();
        tokenResponseHttpMessageConverter.setTokenResponseConverter(new CustomTokenResponseConverter());
        RestTemplate restTemplate = new RestTemplate(Arrays.asList(
                new FormHttpMessageConverter(), tokenResponseHttpMessageConverter));
        restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());

        accessTokenResponseClient.setRestOperations(restTemplate);
        return accessTokenResponseClient;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .cors()
                .and()
            .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
            .csrf().disable()
            .formLogin().disable()
            .httpBasic().disable()
            .exceptionHandling()
                .authenticationEntryPoint(new RestAuthenticationEntryPoint())
                .and()
            .authorizeHttpRequests()
                .antMatchers("/", "/error", "/data", "/data/*").permitAll()
                .antMatchers(HttpMethod.GET, "/someurl/*/thingx", "/someurl/*/thingy").permitAll()
                .antMatchers("/auth/**", "/oauth2/**", "/user-logout").permitAll()
                .anyRequest().authenticated()
                .and()
            .oauth2Login()
                .authorizationEndpoint()
                .baseUri("/oauth2/authorize")
                .authorizationRequestRepository(cookieAuthorizationRequestRepository())
                .and()
                .redirectionEndpoint()
                .baseUri("/oauth2/callback/*")
                .and()
                .tokenEndpoint()
                .accessTokenResponseClient(accessTokenResponseClient())
                .and()
                .userInfoEndpoint()
                .userService(customOAuth2UserService)
                .oidcUserService(customOidcUserService)
                .and()
                .successHandler(oAuth2AuthenticationSuccessHandler)
                .failureHandler(oAuth2AuthenticationFailureHandler)
                .and()
            .addFilterBefore(tokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

    @Bean
    public CorsFilter corsFilter() {

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        config.addAllowedOrigin(appConfig.getClientBaseUrl().toString());
        config.addAllowedHeader("*");
        config.addAllowedMethod("GET");
        config.addAllowedMethod("POST");
        config.addAllowedMethod("PUT");
        config.addAllowedMethod("PATCH");
        config.addAllowedMethod("DELETE");
        config.addAllowedMethod("OPTIONS");
        config.setMaxAge(MAX_AGE);
        source.registerCorsConfiguration("/**", config);
        return new CorsFilter(source);
    }
}

当前application.yml配置:

spring:
  mvc:
    format:
      date: yyyy-MM-dd
      time: HH:mm:ss
  security:
    oauth2:
      client:
        provider:
          azure:
            token-uri: https://login.microsoftonline.com/common/oauth2/v2.0/token
            authorization-uri: https://login.microsoftonline.com/common/oauth2/v2.0/authorize
            user-info-uri: https://graph.microsoft.com/oidc/userinfo
            jwk-set-uri: https://login.microsoftonline.com/common/discovery/v2.0/keys
            user-name-attribute: name
            user-info-authentication-method: header
          google:
            authorization-uri: https://accounts.google.com/o/oauth2/v2/auth?prompt=consent&access_type=offline
        registration:
          azure:
            client-id: FROM_ENV
            client-secret: FROM_ENV
            redirect-uri: "{baseUrl}/oauth2/callback/{registrationId}"
            authorization-grant-type: authorization_code
            scope:
              - openid
              - email
              - profile
              - offline_access
              - https://graph.microsoft.com/Calendars.ReadWrite
              - https://graph.microsoft.com/User.Read
          google:
            client-id: FROM_ENV
            client-secret: FROM_ENV
            redirect-uri: "{baseUrl}/oauth2/callback/{registrationId}"
            scope:
              - email
              - profile
              - https://www.googleapis.com/auth/gmail.send
              - https://www.googleapis.com/auth/calendar

app:
  client-base-url: SOME_URL
  server-base-url: SOME_URL
解决方案

核心思路是为同一身份提供商配置多组客户端注册,并通过自定义逻辑根据登录入口切换对应的权限范围,具体步骤如下:

1. 扩展application.yml中的客户端注册

为Google和Azure分别添加针对USER2的客户端注册,仅保留基础权限:

spring:
  security:
    oauth2:
      client:
        provider:
          azure:
            # 保持原配置不变
          google:
            # 保持原配置不变
        registration:
          # USER1的Azure配置(原配置)
          azure:
            client-id: FROM_ENV
            client-secret: FROM_ENV
            redirect-uri: "{baseUrl}/oauth2/callback/{registrationId}"
            authorization-grant-type: authorization_code
            scope:
              - openid
              - email
              - profile
              - offline_access
              - https://graph.microsoft.com/Calendars.ReadWrite
              - https://graph.microsoft.com/User.Read
          # USER2的Azure配置(新增)
          azure-basic:
            client-id: FROM_ENV  # 复用同一个Azure客户端ID
            client-secret: FROM_ENV  # 复用同一个客户端密钥
            redirect-uri: "{baseUrl}/oauth2/callback/{registrationId}"
            authorization-grant-type: authorization_code
            scope:
              - openid
              - email
              - profile
              - offline_access
              - https://graph.microsoft.com/User.Read
          # USER1的Google配置(原配置)
          google:
            client-id: FROM_ENV
            client-secret: FROM_ENV
            redirect-uri: "{baseUrl}/oauth2/callback/{registrationId}"
            scope:
              - email
              - profile
              - https://www.googleapis.com/auth/gmail.send
              - https://www.googleapis.com/auth/calendar
          # USER2的Google配置(新增)
          google-basic:
            client-id: FROM_ENV  # 复用同一个Google客户端ID
            client-secret: FROM_ENV  # 复用同一个客户端密钥
            redirect-uri: "{baseUrl}/oauth2/callback/{registrationId}"
            scope:
              - email
              - profile

2. 自定义AuthorizationRequestResolver

通过自定义解析器,根据前端传入的参数选择对应的客户端注册:

@Component
public class CustomAuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver {

    private final OAuth2AuthorizationRequestResolver defaultResolver;

    public CustomAuthorizationRequestResolver(ClientRegistrationRepository clientRegistrationRepository,
                                              String authorizationRequestBaseUri) {
        this.defaultResolver = new DefaultOAuth2AuthorizationRequestResolver(
                clientRegistrationRepository, authorizationRequestBaseUri);
    }

    @Override
    public OAuth2AuthorizationRequest resolve(HttpServletRequest request) {
        OAuth2AuthorizationRequest authorizationRequest = this.defaultResolver.resolve(request);
        return customizeAuthorizationRequest(request, authorizationRequest);
    }

    @Override
    public OAuth2AuthorizationRequest resolve(HttpServletRequest request, String clientRegistrationId) {
        OAuth2AuthorizationRequest authorizationRequest = this.defaultResolver.resolve(request, clientRegistrationId);
        return customizeAuthorizationRequest(request, authorizationRequest);
    }

    private OAuth2AuthorizationRequest customizeAuthorizationRequest(HttpServletRequest request,
                                                                     OAuth2AuthorizationRequest authorizationRequest) {
        if (authorizationRequest == null) {
            return null;
        }

        // 从请求参数中获取用户类型
        String userType = request.getParameter("user_type");
        if (userType == null) {
            return authorizationRequest;
        }

        // 根据用户类型切换对应的客户端注册ID
        String originalClientId = authorizationRequest.getClientRegistration().getRegistrationId();
        String targetClientId;
        if ("USER2".equals(userType)) {
            targetClientId = originalClientId + "-basic";
        } else {
            // 默认使用USER1的配置
            targetClientId = originalClientId;
        }

        // 重新构建授权请求,使用目标客户端的权限范围
        ClientRegistration targetClient = authorizationRequest.getClientRegistration().getClientRegistrationRepository()
                .findByRegistrationId(targetClientId);
        if (targetClient == null) {
            return authorizationRequest;
        }

        return OAuth2AuthorizationRequest.from(authorizationRequest)
                .clientRegistration(targetClient)
                .scopes(targetClient.getScopes())
                .build();
    }
}

3. 修改SecurityFilterChain配置

在安全配置中替换默认的AuthorizationRequestResolver为自定义实现:

@Bean
public SecurityFilterChain filterChain(HttpSecurity http, CustomAuthorizationRequestResolver customAuthorizationRequestResolver) throws Exception {
    http
        // 保持原有配置不变...
        .oauth2Login()
            .authorizationEndpoint()
            .baseUri("/oauth2/authorize")
            .authorizationRequestRepository(cookieAuthorizationRequestRepository())
            .authorizationRequestResolver(customAuthorizationRequestResolver) // 添加自定义解析器
            .and()
            // 保持后续配置不变...
        .addFilterBefore(tokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);

    return http.build();
}

4. 前端触发不同登录流程

React前端通过不同按钮/页面,触发登录时携带user_type参数:

  • USER1登录请求:/oauth2/authorize/google?user_type=USER1 或 /oauth2/authorize/azure?user_type=USER1
  • USER2登录请求:/oauth2/authorize/google?user_type=USER2 或 /oauth2/authorize/azure?user_type=USER2

补充说明

  • 同一身份提供商的多组客户端注册可复用同一个客户端ID和密钥,仅需调整scope字段。
  • 自定义解析器也可通过请求路径、Header等其他方式区分用户类型,不限于请求参数。
  • 若需在登录成功后区分用户类型,可在OAuth2AuthenticationSuccessHandler中根据客户端注册ID(如是否包含-basic后缀)标记用户类型。

内容的提问来源于stack exchange,提问作者cowley05

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.03 19:05:44