在reactive spring security中,当构造一个新的JwtIssuerReactiveAuthenticationManagerResolver时,为什么发行者的集合必须非空?

ifmq2ha2  于 2023-08-05  发布在  Spring
关注(0)|答案(2)|浏览(102)

在React式Spring安全中,有一个关于spring documentation中称为“动态租户”的多租户用例的说明。这解释了如何在身份验证管理器解析器的帮助下动态(即在运行时)添加身份验证管理器,该解析器是一个对象,将查询身份验证管理器的Map(由颁发者字符串键控)或颁发者列表。在后一种情况下,将使用默认的身份验证管理器。可以在配置SecurityWebFilterChain期间创建解析器,如下所示...

@Bean
    public SecurityWebFilterChain securityFilterChain(ServerHttpSecurity http) {
        this.addExistingIssuers();

        JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver =
                new JwtIssuerReactiveAuthenticationManagerResolver(allIssuers);

        return http
                .securityContextRepository(NoOpServerSecurityContextRepository.getInstance())
                .csrf(spec -> spec.disable())
                .cors(cors -> cors.configurationSource(corsConfigurationSource()))
                .authorizeExchange(exchanges -> exchanges
                        .pathMatchers(HttpMethod.GET, "/actuator/**").permitAll()
                        .pathMatchers(HttpMethod.POST, "/public/login").permitAll()
                        .anyExchange().authenticated())
                .oauth2ResourceServer(oauth2 -> oauth2
                        .authenticationManagerResolver(authenticationManagerResolver))
                .build();
    }

字符串
此方法的第一行添加任何预先存在的发行者(例如对于已经在应用数据库中的租户)。当添加新租户时,可能需要将新的颁发者添加到传递(通过引用)到JwtIssuerReactiveAuthenticationMangerResolver的颁发者集合中,这可以通过提供这样的方法来实现,该方法可以在新租户创建期间调用:

public void addNewIssuer(Account acct) {
        log.debug("Adding issuer manager for account {}", acct.getName());
        this.addIssuer(
            getIssuerUrl(this.awsRegion, acct.getUserPoolId()),
            acct.getAppClientId(),
            acct.getUserPoolId());
    }


这一切都很好,但这里的事情...
想象一下,您正在建立一个新的多租户平台,您的租户可以在其中通过租赁创建自助服务。当然,最初的发行人列表应该是空的(不是null,请注意......只是空的)。当spring服务启动时,它应该运行得很好,拒绝任何包含JWT的调用,因为发布者列表是空的。然后,一旦租户注册,他们的JWT发布者将使用上面的方法添加到列表中,并且需要身份验证的非公开调用将工作-只要它们来自这个新的发布者。
实际发生的是抛出异常(似乎是因为Assert!)。因此服务无法启动。下面是JwtIssuerReactiveAuthenticationManagerResolver的源代码-注意Assert.notEmpty(...)的调用:

/**
     * Construct a {@link JwtIssuerReactiveAuthenticationManagerResolver} using the
     * provided parameters
     * @param trustedIssuers a collection of trusted issuers
     */
    public JwtIssuerReactiveAuthenticationManagerResolver(Collection<String> trustedIssuers) {
        Assert.notEmpty(trustedIssuers, "trustedIssuers cannot be empty");
        this.authenticationManager = new ResolvingAuthenticationManager(
                new TrustedIssuerJwtAuthenticationManagerResolver(new ArrayList<>(trustedIssuers)::contains));
    }


我不明白为什么一个服务不应该在没有受信任的发布者的情况下启动,然后动态地增量获取受信任的发布者。当我(还)没有任何租户时,我应该与哪个“特殊”发行商初始化列表?
我尝试用空列表启动一个服务;我希望它能工作,我可以动态地添加新的发行者作为租户(动态地)添加到服务。
实际发生的情况是服务无法启动:

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'org.springframework.security.config.annotation.web.reactive.WebFluxSecurityConfiguration': Unsatisfied dependency expressed through method 'setSecurityWebFilterChains' parameter 0: Error creating bean with name 'securityFilterChain' defined in class path resource [uk/co/govbuddy/account/config/SecurityConfig.class]: Failed to instantiate [org.springframework.security.web.server.SecurityWebFilterChain]: Factory method 'securityFilterChain' threw exception with message: trustedIssuers cannot be empty
    at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredMethodElement.resolveMethodArguments(AutowiredAnnotationBeanPostProcessor.java:819) ~[spring-beans-6.0.10.jar:6.0.10]
    at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredMethodElement.inject(AutowiredAnnotationBeanPostProcessor.java:771) ~[spring-beans-6.0.10.jar:6.0.10]
    at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:145) ~[spring-beans-6.0.10.jar:6.0.10]
    at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessProperties(AutowiredAnnotationBeanPostProcessor.java:483) ~[spring-beans-6.0.10.jar:6.0.10]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1416) ~[spring-beans-6.0.10.jar:6.0.10]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:597) ~[spring-beans-6.0.10.jar:6.0.10]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:520) ~[spring-beans-6.0.10.jar:6.0.10]
    at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:326) ~[spring-beans-6.0.10.jar:6.0.10]
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-6.0.10.jar:6.0.10]
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:324) ~[spring-beans-6.0.10.jar:6.0.10]
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:200) ~[spring-beans-6.0.10.jar:6.0.10]
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:973) ~[spring-beans-6.0.10.jar:6.0.10]
    at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:941) ~[spring-context-6.0.10.jar:6.0.10]
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:608) ~[spring-context-6.0.10.jar:6.0.10]
    at org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext.refresh(ReactiveWebServerApplicationContext.java:66) ~[spring-boot-3.1.1.jar:3.1.1]
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:734) ~[spring-boot-3.1.1.jar:3.1.1]
    at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:436) ~[spring-boot-3.1.1.jar:3.1.1]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:312) ~[spring-boot-3.1.1.jar:3.1.1]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1306) ~[spring-boot-3.1.1.jar:3.1.1]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1295) ~[spring-boot-3.1.1.jar:3.1.1]
    at uk.co.govbuddy.account.AccountApplication.main(AccountApplication.java:17) ~[classes/:na]
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
    at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
    at org.springframework.boot.devtools.restart.RestartLauncher.run(RestartLauncher.java:50) ~[spring-boot-devtools-3.1.1.jar:3.1.1]
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'securityFilterChain' defined in class path resource [uk/co/govbuddy/account/config/SecurityConfig.class]: Failed to instantiate [org.springframework.security.web.server.SecurityWebFilterChain]: Factory method 'securityFilterChain' threw exception with message: trustedIssuers cannot be empty
    at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:659) ~[spring-beans-6.0.10.jar:6.0.10]
    at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:647) ~[spring-beans-6.0.10.jar:6.0.10]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1332) ~[spring-beans-6.0.10.jar:6.0.10]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1162) ~[spring-beans-6.0.10.jar:6.0.10]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:560) ~[spring-beans-6.0.10.jar:6.0.10]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:520) ~[spring-beans-6.0.10.jar:6.0.10]
    at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:326) ~[spring-beans-6.0.10.jar:6.0.10]
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-6.0.10.jar:6.0.10]
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:324) ~[spring-beans-6.0.10.jar:6.0.10]
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:200) ~[spring-beans-6.0.10.jar:6.0.10]
    at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:254) ~[spring-beans-6.0.10.jar:6.0.10]
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.addCandidateEntry(DefaultListableBeanFactory.java:1633) ~[spring-beans-6.0.10.jar:6.0.10]
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.findAutowireCandidates(DefaultListableBeanFactory.java:1597) ~[spring-beans-6.0.10.jar:6.0.10]
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveMultipleBeans(DefaultListableBeanFactory.java:1488) ~[spring-beans-6.0.10.jar:6.0.10]
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1375) ~[spring-beans-6.0.10.jar:6.0.10]
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1337) ~[spring-beans-6.0.10.jar:6.0.10]
    at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredMethodElement.resolveMethodArguments(AutowiredAnnotationBeanPostProcessor.java:811) ~[spring-beans-6.0.10.jar:6.0.10]
    ... 25 common frames omitted
Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.security.web.server.SecurityWebFilterChain]: Factory method 'securityFilterChain' threw exception with message: trustedIssuers cannot be empty
    at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:171) ~[spring-beans-6.0.10.jar:6.0.10]
    at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:655) ~[spring-beans-6.0.10.jar:6.0.10]
    ... 41 common frames omitted
Caused by: java.lang.IllegalArgumentException: trustedIssuers cannot be empty
    at org.springframework.util.Assert.notEmpty(Assert.java:479) ~[spring-core-6.0.10.jar:6.0.10]
    at org.springframework.security.oauth2.server.resource.authentication.JwtIssuerReactiveAuthenticationManagerResolver.<init>(JwtIssuerReactiveAuthenticationManagerResolver.java:83) ~[spring-security-oauth2-resource-server-6.1.1.jar:6.1.1]
    at uk.co.govbuddy.account.config.SecurityConfig.securityFilterChain(SecurityConfig.java:67) ~[classes/:na]
    at uk.co.govbuddy.account.config.SecurityConfig$$SpringCGLIB$$0.CGLIB$securityFilterChain$1(<generated>) ~[classes/:na]
    at uk.co.govbuddy.account.config.SecurityConfig$$SpringCGLIB$$2.invoke(<generated>) ~[classes/:na]
    at org.springframework.cglib.proxy.MethodProxy.invokeSuper(MethodProxy.java:258) ~[spring-core-6.0.10.jar:6.0.10]
    at org.springframework.context.annotation.ConfigurationClassEnhancer$BeanMethodInterceptor.intercept(ConfigurationClassEnhancer.java:331) ~[spring-context-6.0.10.jar:6.0.10]
    at uk.co.govbuddy.account.config.SecurityConfig$$SpringCGLIB$$0.securityFilterChain(<generated>) ~[classes/:na]
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
    at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
    at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:139) ~[spring-beans-6.0.10.jar:6.0.10]
    ... 42 common frames omitted

Process finished with exit code 0

mv1qrgav

mv1qrgav1#

好的,我在再次阅读spring docs后得到了这个工作(!),还有一点实验。
总结一下

  • JwtIssuerReactiveAuthenticationManagerResolver有多个构造函数
  • 我使用的构造函数接受发行者字符串的集合
  • 这是一种处理多租户验证不记名令牌的好方法,但**不是 * 动态 * 多租户,其中租户(因此发行者)可以在服务启动后来去;该构造函数(接受字符串集合)获取该集合的内部深层副本
  • 因此,对输入集合的后续修改是没有意义的,因此,传入的集合中必须至少有一个发行者字符串
  • 好消息是,还有另一个接受“策略”的构造函数(即。一个函数),可以从由发行者键入的Map中检索Authentication Manager。
  • 此Map是可变的,可用于实现动态多租户

以下是安全配置需要具备的内容:
1.身份验证管理器Map的声明:

private Map<String, ReactiveAuthenticationManager> allManagers = new ConcurrentHashMap<>();

字符串

  1. SecurityWebFilterChain bean看起来像这样:
@Bean
    public SecurityWebFilterChain securityFilterChain(ServerHttpSecurity http) {
        this.addExistingManagers();

        JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver =
                new JwtIssuerReactiveAuthenticationManagerResolver(issuer -> Mono.justOrEmpty(allManagers.get(issuer)));

        return http
                .securityContextRepository(NoOpServerSecurityContextRepository.getInstance())
                .csrf(spec -> spec.disable())
                .cors(cors -> cors.configurationSource(corsConfigurationSource()))
                .authorizeExchange(exchanges -> exchanges
                        .pathMatchers(HttpMethod.GET, "/actuator/**").permitAll()
                        .pathMatchers(HttpMethod.POST, "/public/login").permitAll()
                        .pathMatchers(HttpMethod.POST, "/public/accounts").permitAll()
                        .anyExchange().authenticated())
                .oauth2ResourceServer(oauth2 -> oauth2
                        .authenticationManagerResolver(authenticationManagerResolver))
                .build();
    }


1.注意上面方法的第一行,它调用了这个方法(这里我从一个自动连接的accountRepository中检索租户):

private void addExistingManagers() {
        accountRepository.findAll().stream()
                .filter(account -> !account.isDisabled())
                .forEach(account ->
                    addAuthenticationManager(allManagers, getIssuerUrl(this.awsRegion, account.getUserPoolId()))
                );
    }

  1. addAuthenticationManager方法看起来像这样(沿着一个帮助器方法来获取发行者URL):
private String getIssuerUrl(String region, String userPoolId) {
        return String.format("https://cognito-idp.%s.amazonaws.com/%s",region, userPoolId);
    }

    private void addAuthenticationManager(
            Map<String, ReactiveAuthenticationManager> authenticationManagers, String issuer) {

        Mono.fromCallable(() -> ReactiveJwtDecoders.fromIssuerLocation(issuer))
                .subscribeOn(Schedulers.boundedElastic())
                .map(JwtReactiveAuthenticationManager::new)
                .doOnNext(authenticationManager -> authenticationManagers.put(issuer, authenticationManager))
                .subscribe();
    }


1.最后,当你想在以后的某个时候添加一个新的AuthenticationManager时,只需调用这样的方法-在本例中,Account代表租户,在我的例子中,它包括AWS Cognito用户池:

public void addNewAuthenticationManager(Account acct) {
        this.addAuthenticationManager(
                allManagers,
                getIssuerUrl(this.awsRegion, acct.getUserPoolId()));
    }


希望这可以帮助其他人尝试在响应式Spring安全中使用动态多租户。值得注意的是,对JwtIssuerReactiveAuthenticationManagerResolver的构造函数调用与spring docs中显示的略有不同,但上面的版本对我来说是有效的。

w7t8yxp5

w7t8yxp52#

只需要定义你自己的ReactiveAuthenticationManagerResolver<ServerWebExchange>实现,而没有非空列表限制:从JwtIssuerReactiveAuthenticationManagerResolver复制或将其用作您自己的代理中的委托(在空的发行者列表上使用保护)。
可能是这样的:

@Component
public class MultiTenantReactiveAuthenticationManagerResolver implements ReactiveAuthenticationManagerResolver<ServerWebExchange> {
    private final Collection<String> trustedIssuers = new HashSet<>();

    private JwtIssuerReactiveAuthenticationManagerResolver delegate;

    @Override
    public Mono<ReactiveAuthenticationManager> resolve(ServerWebExchange context) {
        if (delegate == null) {
            return Mono.empty();
        }
        return delegate.resolve(context);
    }
    
    public synchronized void addIssuer(String issuer) {
        trustedIssuers.add(issuer);
        delegate = new JwtIssuerReactiveAuthenticationManagerResolver(trustedIssuers);
    }
    
    public synchronized void removeIssuer(String issuer) {
        trustedIssuers.remove(issuer);
        delegate = trustedIssuers.size() == 0 ? null : new JwtIssuerReactiveAuthenticationManagerResolver(trustedIssuers);
    }

}

字符串
然后,您可以将此身份验证管理器解析程序注入以进行配置:

@Bean
public SecurityWebFilterChain securityFilterChain(
        ServerHttpSecurity http,
        ReactiveAuthenticationManagerResolver<ServerWebExchange> authenticationManagerResolver) {

    http.oauth2ResourceServer(oauth2 -> oauth2.authenticationManagerResolver(authenticationManagerResolver));

    ...

    return http.build();
}


也可以在@Controller@Service中添加和删除受信任的发行方。

@RequiredArgsConstructor
@RestController
public class TenantsController {
    private final MultiTenantReactiveAuthenticationManagerResolver authManagerResolver;

    @PostMapping("/tenants")
    @PreAuthorize("hasAuthority('TENANTS_ADMIN')")
    public ResponseEntity<Void> addTenant(@RequestBody @Valid TenantCreationDto dto) {
        authManagerResolver.addIssuer(dto.issuerUri().toString());
        return ResponseEntity.created(dto.issuerUri()).build();
    }
    
    public static record TenantCreationDto(@NotNull URI issuerUri) {}
}

相关问题