具有多个授权服务器的单个资源服务器,每个租户一个

Single resource server with multiple authorisation servers, one for each tenant

我正在开发 Spring 引导应用程序,它基本上是一个资源服务器。截至目前,我的应用程序有一个租户,它通过我的应用程序外部的授权服务器进行身份验证。

为了达到同样的目的,截至目前,我在我的应用程序中进行了以下更改:

配置变更如下:

spring.security.oauth2.client.registration.tenant1.client-id=abcd
spring.security.oauth2.client.registration.tenant1.client-authentication-method=basic
spring.security.oauth2.client.registration.tenant1.authorization-grant-type=authorization_code
myapp.oauth2.path=https://external.authorization.server/services/oauth2/
spring.security.oauth2.client.provider.tenant1.token-uri=${myapp.oauth2.path}token
spring.security.oauth2.client.provider.tenant1.authorization-uri=${myapp.oauth2.path}authorize
spring.security.oauth2.client.provider.tenant1.user-info-uri=${myapp.oauth2.path}userinfo
spring.security.oauth2.client.provider.tenant1.user-name-attribute=name

截至目前,我正在从 Vault 获取客户端机密,因此我必须按如下方式定义 OAuth2 配置:

@EnableConfigurationProperties(OAuth2ClientProperties.class)
@Conditional(ClientsConfiguredCondition.class)
@Configuration
public class OAuth2Configuration {

  static final String OAUTH2_CLIENT_SECRET_KEY = "oauth2_client_secret";
  private static final Logger log = LoggerFactory.getLogger(OAuth2Configuration.class);
  private static final String OAUTH2_REGISTRATION_MISSING =
      "oAuth2 registration properties are missing";
  private final ApplicationSecretProvider applicationSecretProvider;
  private final Map<String, ClientAuthenticationMethod> clientAuthenticationMethodMap =
      new HashMap<>();
  private final String authenticationMethod;

  public OAuth2Configuration(
      @Value("${spring.security.oauth2.client.registration.tenant1.client-authentication-method}")
      final String authenticationMethod,
      final ApplicationSecretProvider applicationSecretProvider) {
    this.authenticationMethod = authenticationMethod;
    this.applicationSecretProvider = applicationSecretProvider;
    this.clientAuthenticationMethodMap
        .put(ClientAuthenticationMethod.POST.getValue(), ClientAuthenticationMethod.POST);
    this.clientAuthenticationMethodMap
        .put(ClientAuthenticationMethod.BASIC.getValue(), ClientAuthenticationMethod.BASIC);
    this.clientAuthenticationMethodMap
        .put(ClientAuthenticationMethod.NONE.getValue(), ClientAuthenticationMethod.NONE);
  }

  @Bean
  public InMemoryClientRegistrationRepository getClientRegistrationRepository(
      OAuth2ClientProperties properties) {

    List<ClientRegistration> registrations = new ArrayList<>(
        OAuth2ClientPropertiesRegistrationAdapter.getClientRegistrations(properties).values());
    //We will have only one client registered for oAuth
    if (CollectionUtils.isEmpty(registrations)) {
      log.error(OAUTH2_REGISTRATION_MISSING);
      throw new IllegalStateException(OAUTH2_REGISTRATION_MISSING);
    }
    ClientRegistration registration = registrations.get(0);
    ClientRegistration.Builder builder = ClientRegistration.withClientRegistration(registration);

    ClientAuthenticationMethod clientAuthenticationMethod =
        getClientAuthenticationMethod(authenticationMethod);
    
    ClientRegistration completeRegistration = builder
        .clientSecret(applicationSecretProvider.getSecretForKey(OAUTH2_CLIENT_SECRET_KEY))
        .clientAuthenticationMethod(clientAuthenticationMethod)
        .build();
    return new InMemoryClientRegistrationRepository(completeRegistration);
  }

  protected ClientAuthenticationMethod getClientAuthenticationMethod(String grantType) {
    ClientAuthenticationMethod retValue = clientAuthenticationMethodMap.get(grantType);
    if (retValue == null) {
      return ClientAuthenticationMethod.NONE;
    }
    return retValue;
  }
}

然后我扩展了 DefaultOAuth2UserService 以便在我的应用程序中保存用户详细信息,如下所示:

@Component
public class CustomOAuth2UserService extends DefaultOAuth2UserService {

  private UserRepository userRepository;
  private AuthorityRepository authRepository;    
  
  @Autowired
  public void setUserRepository(UserRepository userRepository) {
    this.userRepository = userRepository;
  }    
  
  @Autowired
  public void setAuthorityRepository(AuthorityRepository
                                                  authorityRepository) {
    this.authorityRepository = authorityRepository;
  }

  @Override
  public OAuth2User loadUser(OAuth2UserRequest userRequest)  {
    DefaultOAuth2User oAuth2User = (DefaultOAuth2User) super.loadUser(userRequest);
    Collection<GrantedAuthority> authorities = new HashSet<>(oAuth2User.getAuthorities());
    Map<String, Object> attributes = oAuth2User.getAttributes();
    ...
    return new DefaultOAuth2User(authorities, oAuth2User.getAttributes(), userNameAttributeName);
  }
}

安全配置如下:

@EnableWebSecurity
@Import(SecurityProblemSupport.class)
@ConditionalOnProperty(
        value = "myapp.authentication.type",
        havingValue = "oauth",
        matchIfMissing = true
)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
  private final CustomOAuth2UserService customoAuth2UserService;
 
  public SecurityConfiguration(CustomOAuth2UserService customoAuth2UserService) {
    this.customoAuth2UserService = customoAuth2UserService;
  }

  public void configure(HttpSecurity http) throws Exception {

    http
        .csrf()
        .authorizeRequests()
            .antMatchers("/login**").permitAll()
            .antMatchers("/manage/**").permitAll()
            .antMatchers("/api/auth-info").permitAll()
            .antMatchers("/api/**").authenticated()
            .antMatchers("/management/health").permitAll()
            .antMatchers("/management/info").permitAll()
            .antMatchers("/management/prometheus").permitAll()
            .antMatchers("/management/**").hasAuthority(AuthoritiesConstants.ADMIN)
            .anyRequest().authenticated()
            //.and().oauth2ResourceServer().jwt()
            .and()
            //.and()
        .oauth2Login()
            .redirectionEndpoint()
                .baseUri("/oauth2**")
                .and()
            .failureUrl("/api/redirectToHome")
            .userInfoEndpoint().userService(customoAuth2UserService);
    http.cors().disable();
  }
}

现在,我也想使用 OAuth2 加入多个租户。假设我想加入另一个租户 tenant2。为了实现这一点,我认为,我需要对现有代码库进行如下更改:

  1. 如上所述在属性文件中添加配置条目:

     spring.security.oauth2.client.registration.tenant2.client-id=efgh
     spring.security.oauth2.client.registration.tenant2.client-authentication-method=basic
     spring.security.oauth2.client.registration.tenant2.authorization-grant-type=authorization_code
     spring.security.oauth2.client.provider.tenant2.token-uri=${myapp.oauth2.path}token
     spring.security.oauth2.client.provider.tenant2.authorization-uri=${myapp.oauth2.path}authorize
     spring.security.oauth2.client.provider.tenant2.user-info-uri=${myapp.oauth2.path}userinfo
     spring.security.oauth2.client.provider.tenant2.user-name-attribute=name
    
  2. 我需要更改安全配置 class:

    SecurityConfiguration 和 OAuth2 配置 class OAuth2Configuration 也是如此。但是我不明白我应该在那里添加什么才能让我的应用程序无缝地为多个租户工作。

在这种情况下,我发现了这个相关的 post:Dynamically register OIDC client with Spring Security OAuth in a multi-tenant stup,但对于我应该在现有代码库中进行哪些更改以使我的应用程序在多模式下工作,我无法得到任何具体的想法-租户设置。

有人可以帮忙吗?

我认为这可能有助于澄清一些困惑。

首先,您似乎并没有真正构建资源服务器,因为资源服务器需要访问令牌进行身份验证。使用 .oauth2Login() 用于 OAuth 2.0 或 OpenID Connect 1.0 登录,这在大多数方面都是常规应用程序,除了您的登录方式。登录成功后您仍然有一个浏览器会话,这在资源中是没有的服务器。

其次,配置静态数量的客户端注册与构建多租户应用程序并不完全相同。也许您稍后会通过展示两个客户来建立它。当使用静态配置属性配置两个客户端时,与单个配置没有什么不同,除了有两个可能的 registrationIds.

首先构建一个简单的 hello world 应用程序,例如 OAuth 2.0 Login Sample. If you add a second client registration to your properties, you'll notice that the auto-generated login page (/login) simply shows two links, one for each client. See docs 了解更多信息。

启动 authorization_code 流程的默认 URI 是 /oauth2/authorization/{registrationId},这意味着导航到 /oauth2/authorization/abcd 会启动第一个客户端的登录流程。导航到 /oauth2/authorization/efgh 会启动第二个客户端的登录流程。除了了解如何启动登录之外,实际上不需要任何其他东西来支持多个登录客户端。

如果您希望支持完全的多租户登录配置,那么您可以提供自定义的 ClientRegistrationRepository,您已经这样做了。唯一的区别是您不应再寻求通过 Spring 引导属性配置客户端,因为这似乎是您的示例中令人困惑的地方。如果您想为某些配置使用属性,请为您的自定义存储库实现创建您自己的配置属性。通常在那个时候,所有这些配置都来自数据库。

我将从该进程开始(hello world、两个静态配置的客户端、自定义 ClientRegistrationRepository),然后继续添加其他自定义组件。这将有助于说明每个点的差异。