如何为不同类型用户请求不同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
相关产品推荐
相关产品推荐

