【问题标题】:CSRF token not generated with Webflux未使用 Webflux 生成 CSRF 令牌
【发布时间】:2019-04-08 04:51:28
【问题描述】:

我有一个由 Spring Security 保护的 Webflux 应用程序,其中 CSRF 保护默认启用。但是,我无法将 CSRF 令牌保存在会话中。

经过一番调查,我注意到它可能来自WebSessionServerCsrfTokenRepository.class。在这个类中,有一个 generateToken 方法应该从生成的 CSRF 令牌创建一个 Mono:

public Mono<CsrfToken> generateToken(ServerWebExchange exchange) {
        return Mono.fromCallable(() -> {
            return this.createCsrfToken();
        });
    }

private CsrfToken createCsrfToken() {
        return new DefaultCsrfToken(this.headerName, this.parameterName, this.createNewToken());
    }

    private String createNewToken() {
        return UUID.randomUUID().toString();
    }

但是,即使CsrfWebFilter 调用了generateToken 方法,也永远不会调用createCsrfToken 方法,并且我从未获得要保存在会话中的CSRF 令牌。我的断点永远不会进入createCsrfToken 方法,这可能意味着它永远不会被订阅。

我正在使用 Spring Boot 2.1.0.RELEASE 和 Spring Security 5.1.1.RELEASENetty 上运行。

我在一个仅包含以下依赖项的空示例应用程序上重现了该问题:

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-oauth2-client</artifactId>
        </dependency>

是我遗漏了什么还是 Spring Security 有问题?

更新

通过进一步调查,我认为问题出在Spring Security CsrfWebFilter.class中的这种方法上:

private Mono<Void> continueFilterChain(ServerWebExchange exchange, WebFilterChain chain) {
        return Mono.defer(() -> {
            Mono<CsrfToken> csrfToken = this.csrfToken(exchange);
            exchange.getAttributes().put(CsrfToken.class.getName(), csrfToken);
            return chain.filter(exchange);
        });
    }

在这里,csrfToken Mono 从未订阅过。当我以这种方式重写过滤器时,我设法在会话中添加了令牌:

private Mono<Void> continueFilterChain(ServerWebExchange exchange, WebFilterChain chain) {
        return Mono.defer(() -> {
            return this.csrfToken(exchange)
                    .map(csrfToken -> exchange.getAttributes().put(CsrfToken.class.getName(), csrfToken))
                    .then(chain.filter(exchange));
        });
    }

但是,_csrf 参数从未添加到我的 Thymeleaf 模型中,因此以下测试不起作用:

<form name="test-csrf" action="/test" method="post">
            <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
            <button type="submit">Escape!</button>
        </form>

【问题讨论】:

  • 好吧,确实看起来您从未订阅过generateToken() 电话。如果您在服务或配置中使用此方法,请尝试在调用该方法时添加.block()。这肯定会执行createNewToken(),但是.block() 被认为是不好的做法。
  • 这段代码其实来自WebSessionServerCsrfTokenRepository.class,它是一个Spring Security类,我没有直接使用。

标签: java spring spring-boot spring-security spring-webflux


【解决方案1】:

如果有人遇到这个问题,我与 Spring Team 的某个人进行了讨论。

实际上不打算直接订阅csrfToken Mono,只在需要时才这样做。在应用中触发订阅是开发者的责任,有两种方式。

方法一:显式订阅。

通过某些@ControllerAdvice 或抽象控制器类中的@ModelAttribute 提供订阅:

@ModelAttribute(CsrfRequestDataValueProcessor.DEFAULT_CSRF_ATTR_NAME)
    public Mono<CsrfToken> getCsrfToken(final ServerWebExchange exchange) {

        return exchange.getAttributeOrDefault(CsrfToken.class.getName(), Mono.empty());
    }

方法2:使用Thymeleaf自动处理CSRF。

确保您的 POM 中有以下依赖项以使用 Thymeleaf 和 Spring Security:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>

这将自动将 CSRF 令牌添加到您的模型中,并通过表单隐藏输入传递它(仍然需要将其添加到 POST Ajax 请求的标头中)。

有关更多信息,这是我在 Spring 上打开的问题:https://github.com/spring-projects/spring-security/issues/6046

【讨论】:

    【解决方案2】:

    当我检查exchangeattributes 属性时,我看不到CsrfToken 那里,所以我无法订阅它。在浏览器会话中检查 cookie 后,我确实看到了 XSRF-TOKEN

    我最终在我们的项目中使用它作为过滤器:

    @Component
    @Slf4j
    class CsrfHeaderFilter implements WebFilter {
       
        @Override
        Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
            def xsrfToken = exchange.getRequest().getCookies().getFirst("XSRF-TOKEN").value
            exchange = exchange.mutate().request({
                it.header("X-XSRF-TOKEN", xsrfToken)
            }).build()
            log.debug(xsrfToken)
            chain.filter(exchange)
        }
    }
    

    然后修改了我的安全配置,以便将这个过滤器放在CsrfWebFilter之前:

    @EnableWebFluxSecurity
    @EnableReactiveMethodSecurity
    class WebSecurityConfiguration {
    
        @Autowired
        CsrfHeaderFilter csrfHeaderFilter
    
        @Bean
        SecurityWebFilterChain SecurityWebFilterChain(ServerHttpSecurity http) {
            http
                    .addFilterBefore(csrfHeaderFilter, SecurityWebFiltersOrder.CSRF)
                    .httpBasic().disable()
                    .formLogin().disable()
                    .oauth2Login().and()
                    .csrf({
                        it.csrfTokenRepository(new CookieServerCsrfTokenRepository())
                    })
                    .authorizeExchange()
                    .pathMatchers("/actuator/health").permitAll()
                    .pathMatchers("/**").authenticated()
                    .and().build()
        }
    }
    

    这对我们有用,因为 CsrfWebFilter 期望找到令牌的位置之一是在请求标头 X-XSRF-TOKEN 中。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2010-12-20
      • 2014-01-21
      • 2015-08-15
      • 1970-01-01
      • 2016-11-08
      • 2014-08-31
      • 2021-10-19
      • 1970-01-01
      相关资源
      最近更新 更多